# Our plugins Source: https://docs.lerian.studio/en/about-plugins Explore the plugins available across Lerian products — what each plugin does, when to enable it, and how it fits with Midaz and other core services. Instead of building custom features from scratch, you can plug in what you need and **keep everything else stable, scalable, and secure**. Plugins run alongside the product platform, adding new logic, endpoints, or workflows to support specific business needs. Plugin availability is product-specific. The currently documented plugins below extend Midaz. Plugins are premium features currently available only to **Enterprise customers** and are licensed separately. **CRM** is no longer a plugin. It is now an embedded component of Midaz, available out of the box with no additional license required. See [CRM Overview](/en/midaz/crm/crm-overview) for details. **Fees Engine** is part of Midaz rather than a standalone plugin — a licensed capability embedded in the unified Midaz Ledger process. See [Fees Engine overview](/en/midaz/fees/fees-engine-overview) for details. To understand how plugins work under the hood, see [What are plugins?](/en/platform/plugins/what-are-plugins) ## Available plugins for Midaz ***
Integrate Pix with Midaz, including SPI and DICT services, to manage keys, transactions, claims, and regulatory workflows. Process electronic transfers between banks through TED, managing transaction validation, settlement, and regulatory compliance. Issue boletos and pay bills — DARF taxes and utility invoices — through the Payments module in Lerian Console, recorded in your Midaz ledger.
## Building your own plugin *** We provide deployment guides and design standards to help partners and internal teams build plugins that are reliable, secure, and easy to maintain. [Visit the Partners Hub →](/en/partners-hub/introduction) for technical documentation and contribution guidelines. If you're interested in building or enabling a plugin, talk to a Lerian representative. We'll help you evaluate, license, and deploy the right setup for your use case. # Our products Source: https://docs.lerian.studio/en/about-products Discover Lerian products, platform services, and Brazil Rails — and how modular building blocks compose into a complete core banking platform. Lerian is a complete, modular core banking platform. Each product is a modular building block that delivers end-to-end capabilities for a specific financial domain. Products can be adopted individually or combined into a composable ecosystem. [Midaz](https://github.com/LerianStudio/midaz) and [Fetcher](https://github.com/LerianStudio/fetcher) are source-available under the Elastic License 2.0 (ELv2). The other Lerian products are closed source and available to licensed customers, with their repositories maintained internally. ## Products, platform, and Brazil Rails ***
Create ledgers, manage transactions, and use core entities like accounts and assets — with CRM and the Fees Engine built in as part of Midaz. Connect rails like Pix and TED. Generate and download reports from reusable templates, filter their data, and manage recurring reporting deadlines. Supports HTML, XML, CSV, PDF, and TXT output. Validate external transactions against the ledger and surface mismatches. Built for accuracy, traceability, and automation. Orchestrate transaction flows using validations, approvals, and external triggers with logic and plugin coordination. Validate transactions in real time, apply rules and spending limits, and return ALLOW, DENY, or REVIEW decisions. Connect to external databases and extract data on schedule, feeding Reporter, Matcher, and your own services. Source-available, like Midaz. Run the credit journey end to end — loan products, origination, servicing, accounting, and Brazil-first regulatory support. Consume platform events and fan them out to your systems through signed webhooks, queues, or a pull API — the platform's event delivery edge. Use shared services for access, lifecycle management, observability, and deployment across the products you adopt. Reach Brazil's payment and regulatory rails through partner integrations and Lerian native messaging.
# Building a complete core banking Source: https://docs.lerian.studio/en/building-a-complete-core-banking How the Lerian pieces come together into one working core banking — what connects to what, and how much you wire yourself versus what comes pre-configured. In [Fundamentals](/en/fundamentals/core-banking/what-is-core-banking) you learned the concepts. This page is how the Lerian pieces come together into one working core banking. A complete Lerian core banking is a **deliberate composition**: Midaz, plus the products and plugins you choose. Products can be adopted on their own — that's a feature, not a missing integration. CRM and Fees are embedded in Midaz; other products and plugins deploy separately. By design, separately deployed pieces aren't pre-wired to each other, so connecting them is a setup step. How much of this you do depends on **who runs your infrastructure**: * **Managed** — Lerian runs your infrastructure and wires the pieces at deploy time. You open the Console, enable the products, and use them — no environment variables, no integration calls. (You won't need the rest of this page.) * **Self-hosted / BYOC** — you run the infrastructure, so you do the wiring: configure each product's URLs and environment, and deploy it. The rest of this page walks that. Once it's connected, you can operate from the Console or call the APIs directly — that's a UX choice, not extra wiring. **Enterprise** is a commercial tier, not a deployment model — Enterprise clients can be **managed or self-hosted**. See [Products, plugins & plans](/en/products-and-plugins) for what's included. ## Start with the ledger *** Midaz is the ledger foundation of a Lerian core banking. Stand it up first when the capabilities you adopt record to Midaz. **CRM is already part of Midaz.** The holders and aliases you learned about are built in, so there's nothing separate to wire — you just enable it. → [Set up Midaz](/en/midaz/midaz-setup) · [CRM in Midaz](/en/midaz/crm/crm-overview) ## Connect the pieces to the ledger *** Most other pieces are **standalone services** (Fees Engine is the exception; it runs inside the ledger process), and they don't all connect the same way. Here's who calls whom at runtime — that difference is the practical part: Runtime flow across your application, the payment plugins, the Fees Engine, Tracer, Reporter, and the Midaz ledger * **Payment plugins write to the ledger for you.** Your application calls a **payment plugin** (TED, Pix); the plugin moves the money and records the operations in Midaz — even from its own background workers — so your app doesn't touch the ledger for those. Its connection to Midaz is set at deploy time, invisible to your app. → [TED setup](/en/rails/ted/jd/ted-configuration) * **The Fees Engine runs inside the ledger.** The **Fees Engine** is a licensed capability that is part of the Midaz ledger process. When a configured fee package applies, Midaz incorporates the fee calculations into the transaction you submit; no separate fee service sits between your app and the ledger. Your application can still call the fees endpoints on the same process to estimate fees before it submits. (For any movement you record without a plugin, your application writes to Midaz's API directly.) → [Connect the Fees Engine to Midaz](/en/midaz/fees/connecting-fees-to-midaz) * **Reporter reads configured data sources.** It reads those sources (read-only, one direction) to build statements and reports — it never writes back. In a managed setup, Lerian configures the agreed data sources during onboarding. In self-hosted or BYOC deployments, configure a data source before you build reports that use it. → [Reporter](/en/reporter/reporter-quick-start) * **Tracer can connect to Midaz when configured.** When `TRACER_BASE_URL` is set, Midaz initializes a reservation client (gRPC by default; REST with `TRACER_TRANSPORT=rest`). It can call reserve, confirm, and release only when `tracer.mode` is configured and not `off`. A Tracer skip is honored only when the request asks for it and the ledger enables `allowTracerSkip`; otherwise the request is rejected. When the base URL is unset, the integration is disabled. Your application can still integrate with Tracer independently. → [Tracer integration guide](/en/tracer/integration-guide) Tracer checks your configured **policies and limits** — not account balances. The ledger stays the source of truth for what an account actually holds. The exact URLs, credentials, and environment settings live in each product's own setup docs (linked above). ## One login across the pieces *** So the pieces work as one product (and not as separate logins), they share authentication through **Access Manager**: it issues the tokens each service trusts. Each piece authenticates with credentials managed there — keep the details in one place and point the pieces at it. → [Access Manager](/en/platform/access-manager/access-manager) ## Deploy the stack *** There's no single "install everything" bundle — deploy each selected component with its supported distribution artifact and bring them up **in order**: **Midaz → Access Manager → products and plugins → Console** The Console comes last because it needs the others already running. → [Helm charts](/en/platform/helm/helm-overview) ## Operate it from the Console *** Once everything is running, the **Console** is your single pane of glass: enable the pieces and manage them with one login. Enabling a module in the Console turns it on in the interface and shares the login — the piece-to-ledger wiring is the deployment step above, not something the Console does for you. → [Lerian Console](/en/platform/console/about-lerian-console) ## On a managed setup *** If Lerian runs your infrastructure, all the wiring above is **already done for you**. From your side it's simple: **enable the modules in the Console and start using them** — no environment variables, no deployment. The integration journey on this page is what Lerian handles underneath. ## Going further (optional) *** A complete Lerian core banking is assembled from modular building blocks. Add **[Matcher](/en/matcher/what-is-matcher)** (reconciliation) and **[Flowker](/en/flowker/what-is-flowker)** (workflow automation) when their capabilities fit your operation. Both deploy as pieces of the same platform and share the Access Manager sign-in; connecting them follows the same deployment step described above. # Core Banking vision Source: https://docs.lerian.studio/en/core-banking-vision Understand Lerian's vision for modern core banking, structured around four pillars — Ledger, transactional services, governance, and connectivity. Core banking should not be a rigid service. It should be a programmable platform. Lerian’s vision is structured around four foundational pillars that work together to provide control, scalability, and long-term architectural flexibility. ## The four pillars of modern Core Banking *** ### 1. Ledger The ledger is the foundation of any financial system. It manages accounts, balances, and transactions with precision and consistency. Powered by Midaz, our ledger is designed for high throughput, reliability, and operational clarity. **Key capabilities:** * Real-time transaction processing * Multi-currency support * Immutable audit trails * High-performance architecture that scales as transaction volume grows ### 2. Transactional services Transactional services orchestrate how money moves. From instant payments to interbank transfers and card settlement, this layer ensures consistent, secure debit and credit execution across channels and networks. **Transaction types documented today:** * Instant payments — [Pix](/en/rails/pix/pix-overview), including scheduled (Pix Agendado) and agreement-based automatic debits (Pix Automático) * Interbank wire transfers — [TED](/en/rails/ted/ted-overview), settled through BACEN's STR * Boletos and bill payments — [issuing and paying](/en/rails/boleto/boleto-and-bill-payment) boletos, DARF taxes, and utility invoices * Card-settlement messaging — [Lerian SLC](/en/rails/native/slc/what-is-lerian-slc) operates Nuclea's ASLC messaging, while [Lerian SILOC](/en/rails/native/siloc/what-is-lerian-siloc) provides the participant-side gateway and card-domain relay ### 3. Governance Governance ensures that growth does not compromise control. This layer integrates compliance, risk management, accounting, and reporting into the operational flow, not as afterthoughts, but as structural components. **Governance capabilities:** * KYC/AML integrations through Flowker and third-party providers * Fraud detection and prevention * Regulatory reporting * Accounting integration * Audit and compliance tooling ### 4. Connectivity infrastructure No core system operates in isolation. Our connectivity infrastructure supports integration with external networks, partners, and internal systems. **Integration capabilities:** * Built for integration — every capability is accessible through standard interfaces * Runs on modern cloud infrastructure with automated scaling and recovery * Supports multiple communication protocols * Real-time data synchronization * Secure communication channels ## Why this matters *** Traditional core banking platforms are monolithic and difficult to evolve. Lerian’s modular architecture allows institutions to: * Adopt components incrementally * Integrate with existing systems * Scale based on operational demand * Avoid vendor lock-in You stay in control of your architecture, and your roadmap. # Lerian Cloud (SaaS) and BYOC Source: https://docs.lerian.studio/en/deployment-models Understand how Lerian can be deployed — in Lerian Cloud (SaaS) or through BYOC in your own infrastructure — and choose the model that fits your compliance and scale needs. Lerian supports two deployment models for its complete, modular platform: **Lerian Cloud (SaaS)** and **BYOC** (Bring Your Own Cloud). The deployment model defines who operates the infrastructure; your license and commercial agreement define which modules are available. Choose the model that fits your operational maturity, compliance requirements, and team structure. Availability depends on the permitted license and commercial arrangement. ## Lerian Cloud (SaaS) — Fully managed by Lerian *** In Lerian Cloud, Lerian hosts and operates the platform for you. Your team focuses on building financial products. We handle everything underneath. **What Lerian manages:** * Cloud infrastructure (compute, storage, networking) * Database provisioning, backups, and scaling * Security patches and OS-level updates * Application upgrades and version management * Monitoring, alerting, and incident response * High availability and disaster recovery **What you manage:** * Business configuration (organizations, ledgers, accounts, rules) * API integrations with your systems * User access and permissions within the platform * Compliance obligations specific to your institution Lerian Cloud is ideal if you want the fastest path to production without building or maintaining infrastructure. You get enterprise-grade operations from day one. ## BYOC — Bring Your Own Cloud *** In the BYOC model, you deploy and operate Lerian in your own infrastructure — whether that's a public cloud (AWS, GCP, Azure), a private cloud, or on-premises. You have full control over the environment. Lerian provides the software, Helm Charts, Terraform templates, and tooling to make deployment and lifecycle management straightforward. **What Lerian provides:** * Application software (container images, Helm Charts) * [Terraform templates](/en/midaz/midaz-terraform-foundation) for infrastructure provisioning * [Lifecycle Management](/en/platform/lifecycle-management) for deployment orchestration and version tracking * Security patches and application updates (delivered by Lerian, applied by you) * Technical support (Enterprise) **What you manage:** * Cloud infrastructure and Kubernetes clusters * Database administration, backups, and scaling * Network security (firewall rules, network isolation, and traffic routing) * OS and container-level patching * Monitoring and observability pipelines * Encryption, secrets management, and access control BYOC is ideal if your organization requires full data sovereignty, has strict regulatory constraints on where data lives, or has an established platform engineering team. ### Why Helm for BYOC deployments Lerian publishes official Helm charts for every product. Helm is the recommended way to deploy, configure, and manage the platform in your infrastructure. Here's why it matters: * **Reproducibility** — Every deployment from the same chart version produces the same result. No "it works on my cluster" surprises across staging, production, or disaster recovery environments. * **Rollback** — Helm keeps a history of every release. Rolling back to a previous version is a single command — no need to reconstruct the previous state manually. * **Reduced operational risk** — Charts encode configuration details (environment variables, ports, dependencies), so your team does not have to remember them. Review your values and rendered manifests; Helm does not eliminate operational risk. * **Faster time to production** — Instead of writing Kubernetes manifests from scratch, start with Lerian's official charts and adjust a single values file. Days of YAML editing become an afternoon of parameter tuning. * **GitOps-ready** — Charts integrate naturally with GitOps workflows (ArgoCD, Flux). Deployment configuration lives in version control, changes go through pull requests, and every deployment is traceable to a specific commit. | Product | What it deploys | Helm documentation | | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | | **Midaz** | Core ledger engine (unified Ledger service — onboarding, transactions, and Fees) with an embedded CRM capability that you enable when needed | [Midaz Helm guide](/en/platform/helm/midaz/midaz-helm-overview) | | **Plugins** | Pix, Pix Indirect, Bank Transfer | [Plugins Helm guide](/en/platform/helm/plugins/plugins-helm-overview) | | **Reporter** | Report generation service | [Reporter Helm guide](/en/platform/helm/reporter/reporter-helm) | | **Access Manager** | Authentication and identity | [Access Manager Helm guide](/en/platform/helm/access-manager/access-manager-helm) | | **Console** | Web UI for managing the platform | [Console Helm guide](/en/platform/helm/console/console-helm) | | **Infrastructure** | OTEL Collector, Fetcher, Flowker, Tracer | [Infrastructure Helm guide](/en/platform/helm/infrastructure/infrastructure-helm) | For the full version compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). For step-by-step installation instructions, start with the [Helm overview](/en/platform/helm/helm-overview) in the Platform section. ### BYOC variations For products and Enterprise entitlements that support multi-tenancy, BYOC supports two configurations: | Configuration | Description | Best for | | :---------------- | :------------------------------------------------------- | :------------------------------------------------------------ | | **Single-Tenant** | One dedicated environment per institution | Large institutions with strict isolation requirements | | **Multi-Tenant** | One environment serving multiple subsidiaries or clients | Institutions operating as a platform or managing subsidiaries | Lerian Cloud is operated as a multi-tenant environment managed by Lerian. If you need dedicated, isolated infrastructure, choose BYOC Single-Tenant. ## How your deployment model affects Midaz API integration *** The Midaz API surface is identical across deployment models — same endpoints, same payloads, same responses. The difference is in how authentication and tenant scoping work. **Lerian Cloud and BYOC Multi-Tenant (Midaz)** Your tenant context is established automatically through your authentication token. There is no tenant header to pass and no tenant ID to manage manually. * You authenticate via [Access Manager](/en/platform/access-manager/access-manager) and receive a JWT. * That token includes your tenant context. * Every API call is automatically scoped to your tenant — organizations, ledgers, accounts, and transactions are all isolated. * You never interact with data from other tenants, and they never interact with yours. **BYOC Single-Tenant (Midaz)** Configure authentication with `PLUGIN_AUTH_ENABLED`. It defaults to `false` for local and development onboarding, but production deployments must set it to `true`; BYOC Multi-Tenant deployments also require it. In a permitted non-production single-tenant environment with authentication disabled, API calls do not require a token. For a deeper look at how tenant isolation works, see [Multi-tenancy](/en/multi-tenancy). ## At a glance *** | | **Lerian Cloud (SaaS)** | **BYOC** | | :------------------------------ | :------------------------------------------------------ | :------------------------------------------------------------------ | | **Who manages infrastructure?** | Lerian | Your team | | **Where does data live?** | Lerian-managed cloud | Your cloud or on-premises | | **Time to production** | Weeks | Depends on your provisioning timeline | | **Operational effort** | Low — Lerian handles operations | Higher — your team operates the platform | | **Best for** | Teams that want to focus on product, not infrastructure | Organizations with strict data residency or compliance requirements | Not sure which model fits? Jump to ["Which model is right for you?"](#which-model-is-right-for-you) below. ## Comparing the models *** | | **Lerian Cloud (SaaS)** | **BYOC** | | :-------------------------- | :------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | | **Infrastructure** | Managed by Lerian | Managed by you | | **Data location** | Lerian-managed cloud | Your cloud or on-premises | | **Data sovereignty** | Data hosted in Lerian-managed cloud infrastructure | Data is hosted in your environment; you control residency and egress according to your architecture and contracted services. | | **Security responsibility** | Shared (Lerian manages infra + app) | Shared (you manage infra, Lerian manages app) | | **Time to production** | Weeks — no infrastructure setup required | Depends on your team's provisioning timeline | | **Operational overhead** | Low — Lerian handles operations | Higher — your team operates the platform | | **Customization** | Standard configuration | Full control over infrastructure and networking | | **Compliance** | Lerian's environment meets security standards | You ensure your environment meets your regulatory requirements | | **Updates** | Applied by Lerian | Applied by you via Lifecycle Management or Helm | | **Scalability** | Managed by Lerian | Managed by you | ## Which model is right for you? *** **Choose Lerian Cloud if:** * You want to go live quickly without provisioning infrastructure * Your team prefers to focus on product and integration, not operations * You don't have strict requirements for data to remain in your own environment **Choose BYOC if:** * Regulatory or compliance rules require data to stay in your infrastructure * You need full control over networking, encryption, and access policies * Your organization has a platform or DevOps team ready to operate Kubernetes workloads * You want to integrate Lerian into an existing cloud environment with specific architectural constraints ## Deployment and licensing *** Deployment and licensing are related dimensions, but not every pairing is available. Lerian Cloud is Enterprise-only; Community supports self-managed Midaz BYOC. See [Community and Enterprise](/en/midaz/midaz-models). | | **Community** | **Enterprise** | | :---------------------- | :-------------------------------------- | :------------------------------------------------ | | **Lerian Cloud (SaaS)** | Not available | Available | | **BYOC** | Available for Midaz only (self-managed) | Available (with Lifecycle Management and support) | Lerian Cloud requires Lerian to manage infrastructure, operations, and lifecycle on your behalf — capabilities that are part of the Enterprise offering. That's why it is not available under the Community model. * **Community + BYOC**: You deploy and manage the source-available, ELv2-licensed Midaz software using the published Midaz Helm charts and documentation. Community support via GitHub and Discord. Other Lerian products are available to licensed customers under the Enterprise model. * **Enterprise + BYOC**: Full BYOC with Lifecycle Management, dedicated support, SLA, onboarding, and access to plugins. * **Enterprise + Lerian Cloud**: Lerian manages everything. You get the licensed platform modules without infrastructure responsibilities. ## What's next *** Understand the licensing differences and what's included in each model. Follow the step-by-step guide to set up your first Ledger and run transactions. Learn how the shared responsibility model works for each deployment option. Explore Kubernetes, Helm, and Terraform options for BYOC deployments. # The accounting cycle Source: https://docs.lerian.studio/en/fundamentals/accounting/accounting-cycle How transactions, journal entries, the ledger, trial balance, and financial statements fit together as one repeating cycle that keeps the books trustworthy. You've met the pieces one at a time: [debits and credits](/en/fundamentals/accounting/debits-and-credits), [journal entries](/en/fundamentals/accounting/journal-entries), the [chart of accounts](/en/fundamentals/accounting/chart-of-accounts), the [financial statements](/en/fundamentals/accounting/financial-statements). This page is the one that puts them in a line. The **accounting cycle** is the repeating routine that takes a single thing that happened in the real world and turns it into a report you can trust — and then starts over. ## One event, all the way through *** Every figure on a financial statement started life as something tiny and concrete: a sale, a payment, a bill. The cycle is just the path that event travels, step by step, until it shows up in a report. * **Transaction** — something happens. A customer pays, you cover the rent, a bill arrives. * **Journal entry** — you write it down as matching debits and credits, so both sides of the movement are recorded. * **Ledger** — each entry is sorted into the right account, so every account gathers its own running history. * **Trial balance** — you add up all the accounts and check that total debits still equal total credits. * **Financial statements** — the balanced totals are arranged into the Balance Sheet, Income Statement, and Cash Flow Statement. Then the cycle repeats — for the next transaction, the next day, the next period. It never really stops; it just keeps turning. ```mermaid theme={null} flowchart LR T["Transaction
something happens"] --> J["Journal entry
debits = credits"] J --> L["Ledger
sorted by account"] L --> TB["Trial balance
check it still balances"] TB --> FS["Financial statements
the reports"] FS -.->|"next transaction · next period"| T classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class TB,FS bal; ``` ## Walking one transaction around the loop *** Say a customer pays you \$2,000 for a sale you make right then — so the revenue is earned at that moment. Follow it through: 1. **Transaction.** The sale happens and the customer pays — money arrives. 2. **Journal entry.** You record it: **Cash** debited \$2,000, **Revenue** credited \$2,000. Debits equal credits, so the entry balances. 3. **Ledger.** That \$2,000 debit joins the running history of the Cash account; the \$2,000 credit joins the Revenue account. Do this for every entry and each account always knows its own total. 4. **Trial balance.** Add every account's debits and credits across the whole book. If the two grand totals match, the records are internally consistent — a quick health check before you report anything. 5. **Financial statements.** The Cash balance feeds the Balance Sheet; the Revenue feeds the Income Statement. The same \$2,000, now part of the big picture. Nothing new is invented along the way. Each step just **reorganizes** the same balanced movement into a more useful shape — from a single event, to a record, to a sorted history, to a verified total, to a readable report. ## Why the order matters *** Each step depends on the one before it, which is exactly why the sequence is fixed: * You can't sort an entry into the **ledger** until you've written the **journal entry**. * The **trial balance** is only meaningful once every entry has landed in the ledger. * The **financial statements** are only trustworthy once the trial balance confirms debits equal credits. That dependency is the cycle's quiet safeguard. The balancing rule from [double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) rides along at every stage, so a mistake shows up early — at the trial balance — instead of hiding inside a finished report. **See also in Core Banking** See how this whole flow lands in a real ledger system in [Designing your ledger plan](/en/fundamentals/core-banking/designing-your-ledger-plan) and [Where to go next](/en/fundamentals/core-banking/where-to-go-next). ## In short *** * The **accounting cycle** is the repeating path from a real event to a finished report: **transaction → journal entry → ledger → trial balance → financial statements**. * Each step just **reorganizes** the same balanced movement into a more useful shape — nothing is invented or lost along the way. * The steps run in a **fixed order** because each depends on the last, and the **debits-equal-credits** rule is checked at the trial balance before anything is reported. * Then it **repeats**, transaction after transaction, period after period. **Next up** You've seen how these ideas connect in the abstract. Now see how they map onto a real ledger system in [Accounting in Lerian](/en/fundamentals/accounting/accounting-in-lerian). # Accounting in Lerian Source: https://docs.lerian.studio/en/fundamentals/accounting/accounting-in-lerian How core accounting ideas map onto real Lerian and Midaz entities — accounts, transactions, operations, and routes — so the concepts and the product line up. Everything in this guide has been about accounting in general. This page connects it to the system you're actually here for. The good news: **Midaz doesn't reinvent accounting — it implements it.** The terms you've already learned have direct counterparts in the product, and once you see the mapping, the documentation reads like a language you already speak. ## The same ideas, renamed for a ledger *** From here on, the words get more product-specific — but don't let that throw you. The accounting ideas stay exactly the same; only the names change. Here's the whole bridge in one table: each accounting idea on the left is something concrete in Midaz on the right. | Accounting idea | In Lerian / Midaz | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | | [Chart of accounts](/en/fundamentals/accounting/chart-of-accounts) | The **account hierarchy** inside a Ledger | | The five account families (assets, liabilities, equity, revenue, expense) | How you **structure and group** accounts in that hierarchy | | [Journal entry](/en/fundamentals/accounting/journal-entries) | A **transaction** made of **operations** | | A single debit or credit line | One **operation** | | [Double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) — debits must equal credits | A transaction's operations **always balance** | | Which accounts a kind of entry may touch | An **accounting route** | ```mermaid theme={null} flowchart LR subgraph ACC["Accounting"] direction TB CA["Chart of Accounts"]:::a JE["Journal Entry"]:::a DC["Debit / Credit rule"]:::a end subgraph MID["Lerian / Midaz"] direction TB AH["Account Hierarchy"]:::m TO["Transaction + Operations"]:::m AB["Always Balanced"]:::m end CA --> AH JE --> TO DC --> AB classDef a fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef m fill:#dcfce7,stroke:#16a34a,color:#14532d ``` The rest of this page walks through the rows that matter most. ## Chart of accounts → the account hierarchy *** A [chart of accounts](/en/fundamentals/accounting/chart-of-accounts) is the organized list of buckets a business sorts its money into. In Midaz, an [account](/en/fundamentals/core-banking/building-blocks) is tied to one asset code and can have one or more keyed [balance](/en/fundamentals/core-banking/building-blocks) records. Accounts aren't a flat list: they're arranged in a **hierarchy**, so you can nest and group them to mirror how a real business is structured. The five accounting families — assets, liabilities, equity, revenue, expenses — are exactly the kind of grouping that hierarchy is built to express. A [Ledger](/en/fundamentals/core-banking/building-blocks) is one self-contained book of records, and an Organization can hold several. So your chart of accounts lives as the structured set of accounts inside a Ledger — the same map of "where value lives," given room to scale. ## Journal entry → transaction + operations *** A [journal entry](/en/fundamentals/accounting/journal-entries) records one event as matching debits and credits. Midaz splits that single idea into two precise words you'll see everywhere in [how money moves](/en/fundamentals/core-banking/how-money-moves): * A **transaction** is one complete movement of value — the whole event, start to finish. That's the journal entry. * An **operation** is one part of that movement — the smallest Midaz record of a balance effect. Non-pending transfers can produce debit and credit operations; pending or canceled flows can also include `ON_HOLD` or `RELEASE`. So when you recorded "Cash debited \$2,000, Revenue credited \$2,000" as a journal entry, the Midaz version is **one transaction made of two operations**. Same event, same two sides, just the product's vocabulary for them. ```mermaid theme={null} flowchart TD A["Journal entry
accounting"]:::acc --> B["Transaction
the whole movement"]:::mid B --> C["Operation
debit"]:::mid B --> D["Operation
credit"]:::mid C --> E["Debits total"]:::tot D --> F["Credits total"]:::tot E --> G["Transaction balances"]:::bal F --> G classDef acc fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef mid fill:#dcfce7,stroke:#16a34a,color:#14532d classDef tot fill:#f3f4f6,stroke:#6b7280,color:#111827 classDef bal fill:#fde68a,stroke:#d97706,color:#78350f ``` ## Double-entry → transactions always balance *** The iron rule of accounting — [debits must equal credits](/en/fundamentals/accounting/debits-and-credits) — is built into Midaz processing. Midaz validates resolved source, destination, and transaction totals before processing; if they differ, the movement will not go through. Its source separates atomic hot-balance mutation from append-only transaction and operation persistence, so this page does not make an all-or-nothing persistence promise. This is the same trustworthiness promise the whole guide keeps coming back to — every accepted movement is validated with a clear source and a clear destination — now enforced by the system instead of by hand. Because balance mutation and history persistence are separate, reconciliation and recovery controls are what surface any persistence gap between them. The [core-banking explanation of how money is recorded](/en/fundamentals/core-banking/double-entry-explained) covers this from the ledger's side. ## Accounting routes → which accounts may take part *** In plain accounting you decide by judgment which accounts a given kind of entry should touch; in Midaz that judgment is captured once as an [accounting route](/en/fundamentals/core-banking/routes-and-fees) — a reusable rule that fixes who can send, who can receive, and which debits and credits get recorded — and a **fee** is just more balanced operations on the same transaction. For example, a wallet transfer's route fixes which customer account may be **debited**, which customer account may be **credited**, and which **fee accounts** join in when a fee applies. The [core-banking guide to routes and fees](/en/fundamentals/core-banking/routes-and-fees) walks through it in full. ## A note on holders and accounts *** In Midaz the [account](/en/fundamentals/core-banking/holders-accounts-aliases) is just the balance container, while the real-world owner is a separate **holder** record and friendly nicknames are **aliases** — kept deliberately separate so the Ledger's record of value stays clean. If that distinction matters for your work, [who owns what](/en/fundamentals/core-banking/holders-accounts-aliases) untangles it. ## In short *** * Midaz **implements** the accounting you've learned — it doesn't replace it. * A **chart of accounts** becomes the **account hierarchy** inside a Ledger; the five account families are how you group it. * A **journal entry** becomes a **transaction** (the whole movement) made of **operations** (its individual debits and credits), and those operations **always balance**. * An **accounting route** captures, once, which accounts may take part in a kind of movement — and a **fee** is just more balanced operations on the same transaction. **Go deeper** To see these entities in full, continue into the [Core banking fundamentals](/en/fundamentals/core-banking/what-is-core-banking), or revisit any idea from the [Accounting overview](/en/fundamentals/accounting/what-is-accounting). # Accrual vs. cash accounting Source: https://docs.lerian.studio/en/fundamentals/accounting/accrual-vs-cash Why revenue isn't always money in the bank and profit isn't the same as cash — the difference between accrual and cash accounting, explained in plain language. Here's a question that quietly confuses almost every beginner: **when does a sale count?** The moment you agree to it and send the bill — or the moment the money actually lands in your account? Your answer decides whether you're doing **accrual** or **cash** accounting, and it changes how every report reads. ## Two moments, not one *** Every sale really has *two* moments: ```mermaid theme={null} flowchart LR M1["📋 Moment 1
Work done · Invoice sent
March"]:::moment M2["💵 Moment 2
Cash received
May"]:::moment M1 -->|"time passes"| M2 ACC(["Accrual records the sale here"]):::accrual CASH(["Cash records the sale here"]):::cash ACC -.-> M1 CASH -.-> M2 classDef moment fill:#eef2ff,stroke:#4f46e5,color:#312e81 classDef accrual fill:#dcfce7,stroke:#16a34a,color:#14532d classDef cash fill:#fef3c7,stroke:#d97706,color:#78350f ``` * The moment you **earn** it — you deliver the goods, finish the work, send the invoice. * The moment you **get paid** — the cash actually arrives. Sometimes these happen together (you buy coffee, you pay at the counter). Often they don't — you finish a job in March but the customer pays in May. The two methods simply disagree about which moment to record. ## Cash accounting — record when money moves *** **Cash accounting** is the simple one: you record a transaction **only when the cash actually changes hands**. Money in the account? Write it down. Money out? Write it down. No money moved? Nothing to record yet. It works like your personal bank statement. You don't note that a friend *promised* to pay you back — you note it when they actually do. Small businesses like it because it's easy and it mirrors the bank balance you can see. The catch: it can mislead you about timing. Do \$50,000 of work in December but get paid in January, and cash accounting says December earned **nothing**. That's tidy for taxes but a poor picture of what the business actually did. ## Accrual accounting — record when it's earned *** **Accrual accounting** records a transaction **when it's earned or incurred — not when the cash moves.** Finish the work and send the invoice? That's revenue *now*, even if payment is weeks away. Receive a bill for electricity you've already used? That's an expense *now*, even if you pay it later. It captures *promises*, not just cash: * Money customers owe you (but haven't paid) is recorded as revenue and as an asset — **accounts receivable**. * Bills you owe (but haven't paid) are recorded as expenses and as a liability — **accounts payable**. This gives a truer picture of how the business is *really* doing in a period, because it matches the income to the work that earned it. That's why larger businesses — and the rules they answer to — use it. | | **Cash accounting** | **Accrual accounting** | | ------------------------- | ---------------------------- | --------------------------------- | | **Records revenue when** | Cash arrives | Work is done / invoice sent | | **Records expenses when** | Cash is paid | Bill is incurred | | **Shows you** | What's in the bank | What you've really earned and owe | | **Best for** | Simplicity, small operations | An accurate picture over time | Here's that same job made concrete — **\$50,000 of work done in December, paid in January** — booked under each method: | The \$50,000 job | December *(work done)* | January *(customer pays)* | | -------------------------------------- | ---------------------- | ------------------------- | | **Cash accounting** records revenue | \$0 | \$50,000 | | **Accrual accounting** records revenue | \$50,000 | — *(cash just arrives)* | Same sale, same \$50,000 — but the methods book it in different months. Accrual ties the revenue to the **work** in December; cash waits for the **money** in January. ## Why revenue isn't "money in the bank" *** This is the part that catches people out. Under accrual accounting, **revenue means money earned — not money received.** You can book \$10,000 of revenue this month and have **none of it** in your account yet, because the customers haven't paid. So when you read an [Income Statement](/en/fundamentals/accounting/financial-statements) and see revenue, don't picture a full bank account. Picture *work that has been done and billed*. The cash may already be here, may be arriving next month, or may never arrive at all if someone fails to pay. ## Why profit isn't cash *** The same gap explains accounting's most famous head-scratcher: **a profitable business can still run out of cash.** Profit is revenue minus expenses — and under accrual, both can include things where no cash has moved yet. So your Income Statement can show a healthy profit while your bank account runs dry, simply because customers are slow to pay. The reverse happens too: you can have plenty of cash (a big upfront payment) yet little profit. That's exactly why there are two different reports. The Income Statement shows **profit**; the [Cash Flow Statement](/en/fundamentals/accounting/financial-statements) shows the **actual cash**. As the saying goes: *profit is an opinion, cash is a fact.* You need both to understand a business. ## In short *** * **Cash accounting** records money only when it changes hands; **accrual accounting** records it when it's earned or incurred, even before the cash moves. * Under accrual, **revenue means "earned," not "in the bank"** — so an Income Statement reflects work done and billed, not your account balance. * Because of that gap, **profit and cash are not the same thing** — a profitable business can still be short of cash, which is why the profit and cash-flow reports are kept separate. **Next up** You now know every piece. See how they run together, period after period, in [The accounting cycle](/en/fundamentals/accounting/accounting-cycle). # Assets, liabilities & equity Source: https://docs.lerian.studio/en/fundamentals/accounting/assets-liabilities-equity The accounting equation explained with everyday analogies — what you own, what you owe, what's yours, and why Assets = Liabilities + Equity always balances. Every business — and honestly, every person — can describe its financial life with three words: **assets**, **liabilities**, and **equity**. Get these three, and the single most important idea in accounting falls into place. Let's use a house. ## The three words, in plain terms *** * **Assets** — everything you **own** that has value. Your house, your car, the cash in your account, the money customers still owe you. * **Liabilities** — everything you **owe** to someone else. Your mortgage, your car loan, an unpaid bill. * **Equity** — what's **actually yours** once the debts are paid off. It's the leftover. Say your house is worth \$300,000 (an asset). You still owe \$200,000 on the mortgage (a liability). The part that's truly yours — your **equity** — is \$100,000. That's the whole idea — **what you own, minus what you owe, is what's yours:** $$ Assets - Liabilities = Equity $$ | Term | Plain meaning | House example | | ------------- | ------------------------ | -------------------------- | | **Asset** | What you own | The \$300,000 house | | **Liability** | What you owe | The \$200,000 mortgage | | **Equity** | What's left over for you | The \$100,000 that's yours | ## The equation that ties them together *** You just saw the equation one way. Move `Liabilities` to the other side and you get the **same idea** flipped around — the form accountants actually use, called the **accounting equation**: $$ \underset{\$300{,}000\ \text{house}}{\text{Assets}} \;=\; \underset{\$200{,}000\ \text{mortgage}}{\text{Liabilities}} \;+\; \underset{\$100{,}000\ \text{yours}}{\text{Equity}} $$ It's the same sentence written two ways. The first asks *what's truly mine?* The second flips it to show *how everything I own was funded* — nothing changes but the order. Everything the business **owns** was paid for in one of two ways: with **money it borrowed** (liabilities) or with **money that's its own** (equity). So the total value of what you own always equals the sum of those two sources. ## Why it always balances *** Here's the part people miss: the equation can **never** be out of balance, by design. If something changes on one side, something else has to change to keep it even. * **You buy a \$20,000 car with a loan.** Assets go up by \$20,000 (the car). Liabilities go up by \$20,000 (the loan). Still balanced. * **You pay off \$5,000 of that loan with cash.** Assets drop by \$5,000 (cash leaves). Liabilities drop by \$5,000 (debt shrinks). Still balanced. * **The business earns \$10,000 in profit.** Assets go up by \$10,000 (cash). Equity goes up by \$10,000 (it's yours to keep). Still balanced. Every real-world event touches the equation in a way that keeps both sides equal. That's not a coincidence — it's the rule that makes accounting trustworthy. If your books *don't* balance, you know immediately that something was recorded wrong. ## In short *** * **Assets** are what you own, **liabilities** are what you owe, and **equity** is what's left over for you. * The **accounting equation** — *Assets = Liabilities + Equity* — just says everything you own was funded either by debt or by your own stake. * It **always balances**: every change on one side forces a matching change elsewhere. A broken balance is a sign of an error. **Next up** You've got what a business owns, owes, and keeps. The other half of the picture is what it earns and spends — meet [Revenue, expenses & costs](/en/fundamentals/accounting/revenue-and-expenses), and how profit feeds back into equity. # Chart of accounts Source: https://docs.lerian.studio/en/fundamentals/accounting/chart-of-accounts What a chart of accounts is, why it keeps financial data organized, how it ties accounting together, and how it maps to the account hierarchy in Midaz. If accounting is a giant filing cabinet of money movements, the **chart of accounts** is the labels on the drawers. It's the organized list of every "bucket" a business uses to sort its transactions — and without it, the records would be an unsearchable pile. ## What it actually is *** A **chart of accounts** is simply a structured list of all the accounts a business uses to record its financial activity. Each account is a category — a place to put a certain kind of transaction. Think of how you might organize your own money into labeled envelopes: *Rent*, *Groceries*, *Salary*, *Savings*. A chart of accounts is the same idea, just more complete and consistent. Every transaction gets filed into one of these named buckets, so you always know what it was for. ## How it's organized *** The accounts are grouped to match the concepts you've already met. Most charts follow the same five families: | Group | What goes here | Examples | | --------------- | -------------------------- | ---------------------------------- | | **Assets** | What the business owns | Cash, equipment, money owed to you | | **Liabilities** | What the business owes | Loans, unpaid bills | | **Equity** | What's left for the owners | Owner's stake, retained profit | | **Revenue** | Money earned | Sales, service income | | **Expenses** | Money spent | Rent, salaries, supplies | The first three feed the [Balance Sheet](/en/fundamentals/accounting/financial-statements); the last two feed the Income Statement. So the chart of accounts is the bridge between raw transactions and the reports everyone reads. In practice, a simple one looks like this — each family gets its own range of numbers, with individual accounts slotted inside: ``` 1000 Assets 1010 Cash 1020 Accounts receivable 1030 Equipment 2000 Liabilities 2010 Loans payable 2020 Taxes payable 3000 Equity 3010 Owner's equity 3020 Retained earnings 4000 Revenue 4010 Product sales 4020 Service income 5000 Expenses 5010 Rent 5020 Salaries 5030 Supplies ``` The numbers aren't decoration — they keep accounts in a fixed order and leave room to add new ones (a `1040` for inventory, say) without disturbing the rest. ## Why it matters *** A good chart of accounts is what makes financial data **usable**: * **Consistency** — the same kind of expense always lands in the same place, so reports are reliable. * **Clarity** — anyone can see, at a glance, where money is going. * **Scale** — whether you have ten transactions or ten million, they all have a clear home. Without it, [double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) entries would have nowhere consistent to land, and the [financial statements](/en/fundamentals/accounting/financial-statements) couldn't be assembled. It's the quiet structure that makes everything else work. ## How it connects in Lerian *** In accounting, the chart of accounts is the structure that organizes your records. In a **ledger system**, that same idea becomes an **account hierarchy** — a tree that groups balances, movements, and the business entities behind them: ```mermaid theme={null} flowchart TD A["Chart of accounts"]:::root --> B["Assets"]:::family A --> C["Liabilities"]:::family A --> D["Equity"]:::family A --> E["Revenue"]:::family A --> F["Expenses"]:::family B --> B1["Cash"]:::leaf B --> B2["Accounts receivable"]:::leaf B --> B3["Equipment"]:::leaf F --> F1["Rent"]:::leaf F --> F2["Salaries"]:::leaf F --> F3["Supplies"]:::leaf classDef root fill:#f3f4f6,stroke:#6b7280,color:#111827 classDef family fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef leaf fill:#eff6ff,stroke:#93c5fd,color:#1e3a8a ``` In **Midaz**, that hierarchy lives inside a Ledger, so you can group and nest accounts to mirror how a real business is structured. [Accounting in Lerian](/en/fundamentals/accounting/accounting-in-lerian) shows exactly how this — and the other accounting ideas — map onto the product. **See also in Core Banking** This same structure becomes an account hierarchy in a ledger — see [The building blocks](/en/fundamentals/core-banking/building-blocks) and [Designing your ledger plan](/en/fundamentals/core-banking/designing-your-ledger-plan). ## In short *** * A **chart of accounts** is the organized list of buckets a business sorts its transactions into. * It groups accounts into **assets, liabilities, equity, revenue, and expenses** — the bridge between raw entries and readable reports. * It's what keeps financial data **consistent, clear, and scalable**, and in **Midaz** it becomes a flexible account **hierarchy** that mirrors how a real business is organized. **Next up** Once every transaction is filed into the right account, those totals roll up into reports. Meet them in [Financial statements](/en/fundamentals/accounting/financial-statements). # Debits and credits Source: https://docs.lerian.studio/en/fundamentals/accounting/debits-and-credits What debit and credit really mean in a banking system — the two ends of a money movement, and why one balance is a wallet to you and a liability to the ledger. Two words trip up almost everyone the first time they read a banking system: **debit** and **credit**. They sound like accounting jargon, but they name something simple — the two ends of a money movement. A debit is where money comes **from**; a credit is where it **goes**. ## Debit is out, credit is in *** Every movement of money leaves one account and arrives in another. Those two ends have names: * A **debit** is the account money moves **out of** — the **source**. * A **credit** is the account money moves **into** — the **destination**. When you send R\$100 to a merchant, your account is **debited** (money out) and the merchant's account is **credited** (money in). One movement, two sides. ```mermaid theme={null} flowchart LR S["Your account
(source)"]:::debit M(("Movement
R$100")):::bal D["Merchant account
(destination)"]:::credit S -->|"debit · R$100 out"| M M -->|"credit · R$100 in"| D classDef debit fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef credit fill:#fde68a,stroke:#d97706,color:#78350f classDef bal fill:#f3f4f6,stroke:#6b7280,color:#111827 ``` This is exactly how your bank statement already reads: money leaving your account shows as a debit, money arriving shows as a credit. ## Your wallet is not the whole story *** Here's where the everyday intuition and the accounting meaning part ways — and it's the single most common source of confusion. In your **wallet**, the math is obvious: money in is good, money out is less. When your bank says it **credited** your account, your balance went **up**; a **debit** took money away. So it's tempting to read *credit = add, debit = subtract*. But that's your wallet's point of view. The **ledger** that holds your money sees the same balance another way. To the institution, your balance isn't something it owns — it's money it **owes you**. Whether a debit or credit grows or shrinks a balance depends on that balance's persisted direction, not account type alone. So keep the two ideas apart: * **Wallet thinking** asks: did *my* number go up or down? * **Accounting thinking** asks: which account did this movement leave (**debit**), and which did it arrive in (**credit**)? The accounting meaning is the reliable one, and it never changes: **a debit is the source, a credit is the destination.** Whether that raises or lowers a given balance is a *separate* question answered by that balance's persisted direction. ## Assets and liabilities: the two sides of the ledger *** Each balance in a Ledger has a **direction**, and that direction — not an account category alone — decides whether a debit or credit changes its available amount. In Midaz, a balance direction is `debit` or `credit`: * A balance with direction **`debit`** increases its available amount on a debit and decreases it on a credit. * A balance with direction **`credit`** increases its available amount on a credit and decreases it on a debit. An account type can supply a default direction, but Midaz applies the direction persisted on the balance. For every **[asset](/en/midaz/assets)** you define — BRL, USD, a loyalty point — the Ledger automatically keeps an external account named after it (`@external/BRL`, `@external/USD`), and you can define your own named external accounts too. Same R\$100, two truths: it's an **asset** in your wallet and a **liability** on the institution's Ledger. Both are right — they're just the two sides of the same movement. [The outside world](/en/fundamentals/core-banking/external-accounts-and-reconciliation) follows the asset side across the boundary. ## The iron rule: debits always equal credits *** Here is what makes the whole system trustworthy: **total debits always equal total credits.** Every movement is recorded on both sides at once — the same amount out of the source and into the destination — so the two sides match exactly. If they don't, something is wrong, and the books say so. Take that R\$100 payment. R\$100 leaves your account as a debit and R\$100 arrives in the merchant's account as a credit. One event, two entries, totals equal: ```mermaid theme={null} flowchart LR D["Your account
DEBIT R$100"]:::debit C["Merchant account
CREDIT R$100"]:::credit D --- BAL{{"Total debits R$100
= Total credits R$100"}}:::bal C --- BAL classDef debit fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef credit fill:#fde68a,stroke:#d97706,color:#78350f classDef bal fill:#dcfce7,stroke:#16a34a,color:#14532d ``` The money didn't appear or vanish — it moved from one account to another, and the matching debit and credit prove it. This is the engine behind **double-entry bookkeeping**: every movement written down twice, once as it leaves and once as it arrives. An **[operation](/en/midaz/operations)** is the smallest Midaz record of a balance effect. A non-pending transfer can produce a source debit and destination credit; pending or canceled flows can also include `ON_HOLD` or `RELEASE` operations. **See also in Core Banking** See what debit and credit mean from the ledger's side in [How money is recorded](/en/fundamentals/core-banking/double-entry-explained). ## In short *** * A **debit** is the account money moves **out of** (the source); a **credit** is the account money moves **into** (the destination). That meaning never changes. * Your **wallet** and the **ledger** read the same balance differently: to you it's your money; to the institution it's a **liability** — money it owes you. * Which side a debit or a credit grows depends on the balance's persisted **direction**: `debit` increases on a debit; `credit` increases on a credit. * **Total debits always equal total credits**, so money is never created or lost — it only moves. **Next up** Debits and credits only make sense as a pair. See how they work together in [Double-entry bookkeeping](/en/fundamentals/accounting/double-entry-bookkeeping). # Double-entry bookkeeping Source: https://docs.lerian.studio/en/fundamentals/accounting/double-entry-bookkeeping Why every transaction touches two accounts so nothing gets lost or invented — the idea behind double-entry bookkeeping, with simple everyday examples. **Double-entry bookkeeping** sounds technical, but the idea is something you already understand: **money never just vanishes or appears — it always moves from somewhere to somewhere else.** Double-entry is simply the habit of recording *both* ends of that movement, every single time. ## One event, two sides *** Think about handing a friend \$10 for lunch. Two things happened at once: **your** wallet went down by \$10, and **their** wallet went up by \$10. One event, two effects. Accounting insists you write down **both**. Every transaction touches **at least two accounts** — one giving, one receiving. That's why it's called *double*-entry. The **value** recorded on one side must be matched by equal value recorded on the other — it isn't always physical cash moving, but the two amounts always agree. So a transaction is never a single line floating on its own. It's **one event with two connected effects**, and recording both is what keeps everything in balance: ```mermaid theme={null} flowchart LR A["One business
event"]:::event --> B["Effect 1
one account changes"]:::effect A --> C["Effect 2
another account changes"]:::effect B --> D["Equal value
on both sides"]:::equal C --> D D --> E["Books stay
balanced"]:::balanced classDef event fill:#f3f4f6,stroke:#6b7280,color:#111827 classDef effect fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef equal fill:#fde68a,stroke:#d97706,color:#78350f classDef balanced fill:#dcfce7,stroke:#16a34a,color:#14532d ``` This is the same balance rule from [debits and credits](/en/fundamentals/accounting/debits-and-credits): one account gets a debit, another gets a matching credit, and the totals agree. ## Why bother recording it twice? *** Recording both sides feels like extra work. It's actually the whole point — it's a built-in lie detector. * **Nothing gets lost.** If money left an account, it *must* show up somewhere else. You can't misplace it without the books refusing to balance. * **Nothing gets invented.** Money can't appear out of thin air. For it to land somewhere, it had to come from somewhere. * **Errors reveal themselves.** If the two sides don't match, you know instantly that something's wrong — before it becomes a real problem. A single-entry list (just "money in, money out") can't do any of this. Double-entry turns the record into something you can **trust and prove** — which is exactly why banks and ledger systems are built on it. ## A simple example *** **Paying rent — \$1,000 in cash:** ```mermaid theme={null} flowchart LR Cash["Cash (an asset)
− $1,000
money left"]:::cash Rent["Rent Expense
+ $1,000
where it went"]:::rent Cash -->|"$1,000 moves"| Rent classDef cash fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef rent fill:#fee2e2,stroke:#dc2626,color:#7f1d1d ``` | Account | Effect | | ------------------- | --------------------------------------------------- | | **Cash** (an asset) | Goes **down** \$1,000 — money left | | **Rent expense** | Goes **up** \$1,000 — that's what the money was for | The \$1,000 didn't disappear. It moved from your cash into the cost of rent — two accounts, one matched movement, a clear source and a clear destination. Money coming *in* works the same way in reverse: take a \$2,000 customer payment and **Cash** goes up while **Revenue** goes up to record where it came from. You'll write both of these out properly — debit and credit, side by side — in [Journal entries](/en/fundamentals/accounting/journal-entries). This page is only about *why* both sides have to exist; the next one is about *how* to write them. ## How it keeps the equation balanced *** Remember **Assets = Liabilities + Equity**? Double-entry is what keeps that equation true after every transaction. Because each event is recorded with equal and opposite effects, the two sides of the equation can never drift apart. The bookkeeping habit and the balancing rule are really the same idea, viewed from two angles. **See also in Core Banking** Core banking is built on this same both-sides rule — see [How money is recorded](/en/fundamentals/core-banking/double-entry-explained). ## In short *** * **Double-entry** records both sides of every transaction — where money came from and where it went. * It exists so **nothing gets lost or invented**, and so errors show up immediately. * Every entry uses a matching **debit and credit**, which keeps *Assets = Liabilities + Equity* in balance no matter what happens. **Next up** That's the rule. Next, see how you actually write a transaction down — step by step — in [Journal entries](/en/fundamentals/accounting/journal-entries). # Financial statements Source: https://docs.lerian.studio/en/fundamentals/accounting/financial-statements The three reports that summarize a business's money — the Balance Sheet, Income Statement, and Cash Flow Statement — and what each answers, in plain language. All those carefully recorded transactions add up to something useful: **financial statements**. These are the summaries that turn thousands of tiny entries into a picture anyone can read. There are three main ones, and each answers a different question about the business. You don't need to know how to *build* them — just what each one **tells you**. ## The three statements at a glance *** | Statement | The question it answers | Think of it as | | ----------------------- | ------------------------------------- | -------------------- | | **Balance Sheet** | What do we own and owe *right now*? | A snapshot | | **Income Statement** | Did we make a profit *over a period*? | A highlight reel | | **Cash Flow Statement** | Where did the cash actually *move*? | A bank-account story | ## The Balance Sheet — a snapshot in time *** The **Balance Sheet** is a photograph of the business at a single moment. It lists everything the company **owns** (assets), everything it **owes** (liabilities), and what's **left over for the owners** (equity). It's the [accounting equation](/en/fundamentals/accounting/assets-liabilities-equity) — *Assets = Liabilities + Equity* — printed as a report. If you want to know how healthy a business is *today*, this is where you look. ## The Income Statement — profit over time *** The **Income Statement** (also called the *Profit & Loss* or *P\&L*) covers a stretch of time — a month, a quarter, a year. It starts with the **money earned** (revenue), subtracts the **money spent** (expenses), and shows whether the business ended up with a **profit or a loss**. Where the Balance Sheet is a snapshot, this is the story of what happened *between* two snapshots. ## The Cash Flow Statement — where the cash moved *** The **Cash Flow Statement** tracks **cash and cash equivalents** (shortened to *cash* on this page) going in and out over a period. This matters because a business can look profitable on paper yet still run out of cash — for example, if customers haven't paid their bills yet. Under accrual accounting, revenue is recognized when it is earned and expenses when they are incurred, not when cash moves; cash-flow reporting tracks whether the business has the liquidity to pay its bills. ## How they fit together *** The three aren't separate — they're three views of the same reality: ```mermaid theme={null} flowchart TD A["Income Statement
Did we make a profit?"]:::is --> B["Profit changes
equity"]:::link D["Cash Flow Statement
Where did cash move?"]:::cf --> E["Cash balance
changes"]:::link B --> C["Balance Sheet
What do we own & owe now?"]:::bs E --> C classDef is fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef cf fill:#fef3c7,stroke:#d97706,color:#78350f classDef bs fill:#dcfce7,stroke:#16a34a,color:#14532d classDef link fill:#f3f4f6,stroke:#6b7280,color:#111827 ``` * The **Income Statement** shows whether you earned a profit. * That profit flows into **equity** on the **Balance Sheet**. * The **Cash Flow Statement** explains why the cash on that Balance Sheet went up or down. Read together, they tell you whether a business is profitable, solid, and able to pay its bills — the three things anyone lending money or making decisions needs to know. **See also in Core Banking** See how a ledger's records are proved against the outside world in [The outside world](/en/fundamentals/core-banking/external-accounts-and-reconciliation). ## In short *** * The **Balance Sheet** is a snapshot of what you own and owe right now. * The **Income Statement** shows whether you made a profit over a period. * The **Cash Flow Statement** tracks where cash and cash equivalents moved — because profit and cash aren't the same thing. **Next up** You just saw that profit and cash aren't the same thing. See exactly why — and when each one gets recorded — in [Accrual vs. cash accounting](/en/fundamentals/accounting/accrual-vs-cash). # Journal entries Source: https://docs.lerian.studio/en/fundamentals/accounting/journal-entries How you actually record a transaction, step by step — from debits and credits to writing it down, with T-accounts and real examples like paying rent. You've met [debits and credits](/en/fundamentals/accounting/debits-and-credits) and [double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) as ideas. Now comes the practical bit: **how do you actually write a transaction down?** The answer is the **journal entry** — the basic unit of recording, the place where every concept finally touches paper. ## What a journal entry is *** A **journal entry** is the written record of a single transaction. It names the accounts involved, says how much value was recorded, and marks which side — debit or credit — each account is on. Think of it as the sentence accounting uses to describe an event. Every entry has the same three parts: * **Which accounts** are affected (at least two). * **How much** the amount is. * **Which side** each account sits on — debit (left) or credit (right). And it follows one unbreakable rule you already know: **total debits must equal total credits.** If they don't, the entry is wrong before you've even finished writing it. ## The steps, every single time *** Recording any transaction is the same short routine. Walk through it slowly the first few times and it becomes automatic. 1. **What happened?** Describe the event in plain words — "we paid rent," "a customer paid us." 2. **Which accounts does it touch?** Every event hits at least two. One gives, one receives. 3. **Does each account go up or down?** Decide the direction for each. 4. **Translate up/down into debit/credit.** Use the account type to know which side that means. 5. **Check the balance.** Debits on the left, credits on the right — and the two totals must match. ```mermaid theme={null} flowchart TD A["1 · What happened?"]:::step --> B["2 · Which accounts
are affected?"]:::step B --> C["3 · Did each account
go up or down?"]:::step C --> D["4 · Translate into
debit or credit"]:::step D --> E["5 · Check:
debits = credits"]:::check classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef check fill:#dcfce7,stroke:#16a34a,color:#14532d ``` Step 4 is the one that trips people up, so keep the cheat sheet from [debits and credits](/en/fundamentals/accounting/debits-and-credits) handy: | Account type | To increase it | To decrease it | | ------------------------------ | -------------- | -------------- | | **Assets** (what you own) | Debit | Credit | | **Liabilities** (what you owe) | Credit | Debit | | **Equity** (what's yours) | Credit | Debit | | **Revenue** (money earned) | Credit | Debit | | **Expenses** (money spent) | Debit | Credit | ## T-accounts in practice *** A **T-account** is just a way to picture one account as a big letter **T** — debits on the left, credits on the right. A journal entry is what you get when you write down the matching halves of two (or more) T-accounts at once. ``` Cash (an asset) Rent expense ----------------------- ----------------------- Debit (+) | Credit (-) Debit (+) | Credit (-) ----------------------- ----------------------- | $1,000 $1,000 | ----------------------- ----------------------- ``` The left side of one account lines up with the right side of another. That mirror is the entry. Now let's write a couple for real. ## Example 1 — paying rent in cash *** **What happened:** the business paid \$1,000 for rent. * It touches two accounts: **Cash** and **Rent expense**. * **Cash** goes *down* — money left the business. Cash is an asset, so a decrease is a **credit**. * **Rent expense** goes *up* — that's what the money was for. Expenses increase with a **debit**. Written as a journal entry: | Account | Debit | Credit | | ---------------- | ------- | ------- | | **Rent expense** | \$1,000 | | | **Cash** | | \$1,000 | Debits (\$1,000) equal credits (\$1,000). The entry balances. It reduces cash and recognizes rent expense for the period; both effects are on the record. ## Example 2 — receiving payment for a cash sale *** **What happened:** a customer paid the business \$2,000 for a cash sale. * It touches **Cash** and **Revenue**. * **Cash** goes *up* — money arrived. Cash is an asset, so an increase is a **debit**. * **Revenue** goes *up* — that's where the money came from. Revenue increases with a **credit**. | Account | Debit | Credit | | ----------- | ------- | ------- | | **Cash** | \$2,000 | | | **Revenue** | | \$2,000 | Again, debits equal credits, and the money has a clear source and destination. Notice that **Cash was debited this time and credited last time** — same account, opposite directions, depending on whether money came in or went out. If the payment settles an earlier invoice, the credit is to Accounts Receivable rather than Revenue. **See also in Core Banking** In a ledger system a journal entry becomes a transaction made of operations — see [How money moves](/en/fundamentals/core-banking/how-money-moves) and [The building blocks](/en/fundamentals/core-banking/building-blocks). ## In short *** * A **journal entry** is how you actually record a transaction: which accounts, how much, and which side each is on. * Follow the same steps every time — name the event, find the two accounts, decide up or down, translate to debit/credit, then check that the two sides balance. * The iron rule never changes: **debits must equal credits**, which is what keeps every entry — and the whole ledger — trustworthy. **Next up** Every entry posts to an account — but where does that list of accounts come from? Meet the [Chart of accounts](/en/fundamentals/accounting/chart-of-accounts). # Revenue, expenses & costs Source: https://docs.lerian.studio/en/fundamentals/accounting/revenue-and-expenses What revenue, expenses, and costs really are, told apart in plain language — the words that drive the Income Statement and why the difference matters. Two words show up everywhere in accounting: **revenue** and **expenses**. They're the money a business *earns* and the money it *spends*. Get these two straight and the [Income Statement](/en/fundamentals/accounting/financial-statements) — the report that says whether you made a profit — suddenly reads like a simple subtraction. ## Revenue — the money you earn *** **Revenue** is the amount a business earns from doing what it does: selling a product, providing a service, charging for a subscription. It is recognized when earned, which may differ from when cash is received. Picture a bakery. Every loaf it sells brings in revenue. Add up a day's sales and that total is the day's revenue. It's the inflow that everything else gets measured against. One thing to hold onto: revenue is about **what you earned**, not necessarily the cash sitting in your account today. That subtlety has its own page — [accrual vs. cash](/en/fundamentals/accounting/accrual-vs-cash) — but for now, just think of revenue as *money earned from the work*. ## Expenses — the money you spend to earn it *** An **expense** is a cost recognized in a period to keep the business running and earn revenue: rent, salaries, electricity, flour for the bakery. It may or may not involve a cash payment in that same period. Expenses aren't a bad thing — they're the fuel. You can't sell bread without buying flour and paying the baker. The point of accounting isn't to avoid expenses; it's to know exactly what they are so you can tell whether the revenue was worth it. ## Cost — close to expense, but not identical *** People use **cost** and **expense** as if they mean the same thing, and in casual talk they nearly do. The small distinction worth knowing: * A **cost** is what you *pay* to get something — the price of acquiring a resource. Buying an oven for the bakery has a cost. * An **expense** is a cost *counted against the revenue of a period* — the portion that's "used up" in earning this stretch's income. The flour baked into today's bread is an expense today. In short: a **cost** becomes an **expense** once it's used up in the work of earning revenue. The oven is a cost when you buy it; the slice of its wear-and-tear that helped make this month's bread becomes an expense this month. | Term | Plain meaning | Bakery example | | ----------- | -------------------------------------------- | ------------------------------------------ | | **Revenue** | Money earned from the work | Selling loaves | | **Cost** | What you pay to acquire something | Buying the oven | | **Expense** | A cost used up to earn this period's revenue | The flour baked today, the rent this month | Don't over-think the cost-versus-expense line — just know that "cost" leans toward *acquiring*, and "expense" leans toward *using up to earn revenue*. Follow one bag of flour through the bakery and the whole chain falls into place: ```mermaid theme={null} flowchart LR A["Buy flour
cost"]:::cost --> B["Bake the bread
expense"]:::expense B --> C["Sell the bread
revenue"]:::revenue C --> D["Revenue − expenses
profit or loss"]:::profit classDef cost fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef expense fill:#fed7aa,stroke:#ea580c,color:#7c2d12 classDef revenue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef profit fill:#dcfce7,stroke:#16a34a,color:#14532d ``` ## Why they matter to the Income Statement *** These two words are the entire engine of the [Income Statement](/en/fundamentals/accounting/financial-statements): * Start with **revenue** — the amount recognized as earned. * Subtract **expenses** — the costs recognized for that period. * What's left is **profit** (or, if expenses were bigger, a **loss**). A month at the bakery, in one line: $$ \underbrace{\$1{,}000}_{revenue} - \underbrace{\$700}_{expenses} = \underbrace{\$300}_{profit} $$ This is a simplified view. Revenue minus expenses tells you whether the business reported profit or loss over a period; a full statement can group and present those amounts in more detail. And that profit doesn't just disappear — it flows into **equity**. Earn a profit and, before any distributions to owners, the owners' stake in the business grows; run a loss and it shrinks on the same basis. That's the quiet link back to [Assets, liabilities & equity](/en/fundamentals/accounting/assets-liabilities-equity): the Income Statement is the story of *how* equity changed over a period. ## In short *** * **Revenue** is the amount a business earns from its work; **expenses** are costs recognized to earn that revenue, whether or not cash moves in the same period. * A **cost** is the amount incurred to acquire something; it becomes an **expense** once it's used up in earning a period's revenue. * In a simplified view, the **Income Statement** is **revenue minus expenses** — the difference is profit or loss. **Next up** Now you know the five account families. Next, see how every change to them gets written down — as [Debits and credits](/en/fundamentals/accounting/debits-and-credits). # What is accounting? Source: https://docs.lerian.studio/en/fundamentals/accounting/what-is-accounting The core idea behind accounting — a trustworthy record of money in, money out, and what a business owns versus what it owes. No formulas, just the concept. **Accounting** is the practice of keeping a clear, honest record of a business's money — and everything of value it owns or owes, not just the cash on hand. Think of it as a diary that answers three questions at any moment: **What came in? What went out? And where do we stand right now?** That last question is the important one. A business doesn't just want to know how much cash is in the drawer today — it wants to know what it **owns**, what it **owes**, and whether the two add up. Accounting is how it keeps that picture accurate, day after day. ## The three things every business tracks *** Strip away the jargon and accounting is really watching three flows: * **Money coming in** — sales, payments from customers, loans received, money the owners put in. * **Money going out** — rent, salaries, supplies, loan repayments. * **What's owned vs. what's owed** — the cash, equipment, and money others owe you, balanced against the debts you still have to pay. A good record aims to keep these connected. If money moved, it should be written down; if something is owned or owed, it should be accounted for. Controls and reconciliation help identify missing or inconsistent records. ## Why it has to be trustworthy *** Imagine running a shop where you *think* you have money but aren't sure. You can't pay suppliers with a guess. You can't prove to a bank that you're worth lending to. You can't tell if you're actually making a profit. Accounting uses records and controls to make financial information checkable. A balanced entry is an arithmetic check: it confirms the recorded sides match, but does not by itself prove authorization, classification, or completeness. | Without good accounting | With good accounting | | ---------------------------------- | ------------------------------------------------------ | | You guess how much you have | Your records show how much you have and why | | Money can quietly go missing | Controls and reconciliation help catch missing records | | You can't prove anything to others | Auditors and banks can verify it | | Profit is a mystery | Profit is a number you can see | **See also in Core Banking** The same trustworthy-record promise is what banking platforms are built to keep — see [What is core banking?](/en/fundamentals/core-banking/what-is-core-banking). ## In short *** * **Accounting** is the trustworthy record of money coming in, money going out, and what a business owns versus owes. * Balanced entries are an important arithmetic control, while reconciliation and other controls help test completeness and accuracy. * That focus on checkable records is why accounting underpins banking, ledgers, and every serious financial system. **Next up** Now that you know *what* accounting tracks, see *why* it matters the moment software starts holding money in [Why accounting matters](/en/fundamentals/accounting/why-accounting-matters). # Why accounting matters? Source: https://docs.lerian.studio/en/fundamentals/accounting/why-accounting-matters The "so what?" for builders — why double-entry is a system requirement, why balancing guarantees correctness, and why financial systems rely on it. You already know *what* accounting is: an honest record of money in, money out, and what's owned versus owed. This page is about why that matters for the thing you're actually building — a ledger, a wallet, a payments flow, a banking platform. The short version: **the moment software starts holding other people's money, it inherits accounting's rules whether it names them or not.** So this isn't a detour into finance theory. The same ideas that keep a shopkeeper's books honest are the ones that keep your system from quietly losing — or inventing — a customer's balance. ## Money is a promise, and promises need a ledger *** A balance in an app isn't cash sitting in a box. It's a **promise**: a claim that some amount belongs to someone and can be moved or withdrawn. The only thing making that promise real is the record behind it. Lose the record, or let it drift, and the money effectively doesn't exist — or worse, exists twice. That's why every financial product, underneath the UI, is a record-keeping problem first. The interesting parts — transfers, fees, settlement, statements — all sit on top of one question that has to be answered correctly every single time: **who owns what, right now?** Accounting is simply the discipline that has been answering that question reliably for centuries. ## Why double-entry is a system requirement, not a convention *** Imagine a customer moves \$50 from one wallet to another. If the first wallet goes down by \$50 *and* the second goes up by \$50, matching recorded totals let the system check its internal record of that transfer. If only one side gets recorded, the mismatch is a signal to investigate rather than proof that the business event was correct. That's why double-entry exists. It gives a transaction a checkable constraint: the recorded sides must match. Encode that as a rule and you get an important consistency control: * **A movement records its participating accounts.** The record identifies the accounts affected by the transaction. * **The recorded sides must equal.** Matching totals make the entry arithmetically checkable. * **A mismatch surfaces immediately.** If the recorded sides do not match, the transaction can be rejected before the inconsistency compounds. Midaz applies this directly: it rejects a transaction when its resolved source, destination, and send totals differ. That check is valuable, but it does not prove authorization, account selection, classification, valuation, or complete business correctness. ## Balance is a consistency control *** Here's the part worth internalizing as a builder. Balanced records make one part of a transaction continuously testable: the recorded amounts agree. Matching debits and credits detects a recorded amount mismatch. *Assets = Liabilities + Equity* is a statement-level accounting equation, not a runtime assertion Midaz applies after each operation. Records can balance while an account is wrong, a transaction is unauthorized, a value is misclassified, or another required record is missing. Financial systems still rely on this control. Recording both sides gives balance a useful tripwire: when it breaks, the recorded amounts are inconsistent and the system must investigate before proceeding. Put end to end, that's the chain in one line — balanced records keep the amounts behind a balance consistent; reconciliation and separate controls are still needed to trust it fully: ```mermaid theme={null} flowchart LR A["User sees
a balance"]:::step --> B["System keeps
ledger records"]:::step B --> C["Each movement
has two sides"]:::step C --> D["Books stay
balanced"]:::step D --> E["Recorded amounts
are consistent"]:::trust classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef trust fill:#dcfce7,stroke:#16a34a,color:#14532d ``` ## Financial systems are accounting systems wearing different clothes *** Strip a core banking platform down to its foundation and you find a ledger. Strip the ledger down and you find double-entry accounting, enforced by code instead of by a clerk with a pen. The product vocabulary changes — *accounts*, *transactions*, *operations*, *balances* — but the bones are the same accounting ideas, now running at scale and in real time. That's why this matters for what you build: * **The data model is accounting.** Accounts, entries, and balances aren't an implementation detail bolted onto a payments app — they *are* the app's source of truth. * **The safety properties use accounting controls.** A balanced posting detects mismatched recorded amounts; authorization, classification, and completeness require additional controls. * **The hard problems are accounting at scale.** Handling many transfers at once, keeping every copy of the data in agreement, and proving what happened long after the fact all come down to keeping that balanced record correct across millions of movements. Learn the accounting once and a lot of system design stops feeling arbitrary. The constraints you keep bumping into aren't the platform being fussy — they're centuries-old rules for not losing track of money, finally written down as code. **See it in the product** These ideas have direct counterparts in Lerian and Midaz — accounts, transactions, operations, and balanced movements. See how they map in [Accounting in Lerian](/en/fundamentals/accounting/accounting-in-lerian), or step into the [Core banking fundamentals](/en/fundamentals/core-banking/what-is-core-banking). ## In short *** * The moment software holds money, it inherits accounting's rules — a balance is a promise, and the record behind it is what makes that promise real. * **Double-entry is a consistency control**: every recorded transaction has participating accounts and matching recorded sides. * **Balance is not a complete correctness guarantee**. It detects amount mismatches; authorization, classification, and completeness need separate controls. * Strip any serious financial system down and you find an accounting system at its core, now enforced by code instead of by hand. **Next up** Time to meet the equation everything rests on. Start with [Assets, liabilities & equity](/en/fundamentals/accounting/assets-liabilities-equity). # The building blocks Source: https://docs.lerian.studio/en/fundamentals/core-banking/building-blocks The five core nouns of core banking — Organization, Ledger, Asset, Account, and Balance — each in one sentence, in the order they depend on each other. Walk into a library and the structure is obvious before anyone explains it. There's the library itself, the catalog that organizes everything, the shelves, what's actually sitting on each shelf right now, and the *kind* of thing a shelf holds. A core banking system is built from five pieces that line up the same way. You don't need to memorize them. You just need to see how each one rests on the one before it. ## They stack in order *** Each block depends on the one above it: a shelf sits in a library, a balance sits in an account. Here they are, top to bottom. * **Organization** — your company, the outer boundary of everything you run. *The library building itself.* All your records live inside it and belong to it. * **Ledger** — one self-contained book of records. *The catalog.* You can keep more than one — say, one per business line or per region — and each is its own balanced, independent set of books. * **Asset** — the *kind* of value being tracked: a currency like USD, loyalty points, anything countable. *The category of thing on a shelf.* Tracking the unit separately from the amount is what lets the system know that 100 of one thing isn't 100 of another. * **Account** — a container tied to one asset. A non-external account can have one or more keyed balance records; an external account has exactly one `default` balance. *A single shelf.* Every movement of money has accounts on both sides. * **Balance** — one keyed record of the amount an account holds, including available and held amounts. *What's actually on the shelf at this moment.* An **Organization** can hold many **Ledgers**; a **Ledger** holds many **Accounts**; an **Account** is always tied to one **Asset** and can carry one or more keyed **Balance** records. Read top to bottom, each block is the home of the next. ```mermaid mermaid theme={null} flowchart TB O["Organization
your company"] L["Ledger
one book of records"] Acc["Account
holds value, sends & receives"] Bal["Balance
the amount held right now"] As["Asset
the kind of value"] O --> L L --> Acc Acc --> Bal As -.-|"defines the unit"| Acc classDef box fill:#e8f0fe,stroke:#4285f4,color:#333; classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class O,L,Acc,As box; class Bal bal; ``` ## A balance is a result, not a setting *** Here's the one idea worth slowing down on. A **balance** is not a number you type in and adjust by hand. It's the **result** of every movement into and out of the account, added up. Money arrives, money leaves, and the balance is simply where that leaves you. That's why the books stay trustworthy: a balance can't be quietly edited, only *moved into the shape it has* by recorded movements with two sides each. A balance isn't always one single number you can spend. Part of it can be **available** — yours to use now — while part is **held**, set aside for a movement that hasn't finished. We'll unpack available versus held when we look at how money moves. ## In short *** * The five building blocks stack in order: **Organization** → **Ledger** → **Account** → **Balance**, with an **Asset** defining what an account holds. * An **Organization** is your company; a **Ledger** is one self-contained book; an **Account** holds and moves value through keyed **Balance** records. Non-external accounts can have multiple records; external accounts have one `default` record. * An **Asset** is the *kind* of value — tracked separately so amounts of different things never get confused. * A **balance** is a **result** of recorded movements, never a number you set by hand. Each balance record can track **available** and **held** amounts and its own sending and receiving settings. These five nouns are the vocabulary for everything else. Once they feel natural, movements, holders, and fees are just things you *do* with them. **See it in Lerian** See the building blocks in practice: [Core entities](/en/midaz/core-entities), [Ledgers](/en/midaz/ledgers), [Accounts](/en/midaz/accounts), [Balances](/en/midaz/balances), and [Assets](/en/midaz/assets). # Designing your ledger plan Source: https://docs.lerian.studio/en/fundamentals/core-banking/designing-your-ledger-plan How to model your own business as a set of accounts — a simple, worksheet-style method for designing a chart of accounts before you build anything. By now you know the pieces: accounts, balances, movements, the rules that govern them. This is the page where you put them to work on *your* business. The goal is to move from "I understand the terms" to "I can model what my company actually does". Here's the mindset shift: a **ledger plan** — sometimes called a **chart of accounts** — is not a list of accounts you rush to create. It's a *deliberate classification* of the accounts your business needs, and how they relate. Designing it well, up front, saves you from painful restructuring later. ## Think rooms before you build *** Imagine designing a house. Before you pour any concrete, you decide what each room is for — kitchen, bedroom, office — and who's allowed in. You don't start laying bricks and figure out the floor plan afterward. A ledger plan works the same way. You decide what each account is *for*, what rules it follows, and how money flows between rooms — **before** you create anything. The rest of this page is a simple, four-step method for doing exactly that. ## Step 1: List your real-world actors *** Start away from the software entirely. On paper, list every real-world party that touches money in your business. Don't worry about structure yet — just name them. A typical list looks like this: * **Customers** — the people or companies who hold value with you * **Treasury** — your own operating funds * **Revenue** — money you earn * **Fees** — charges you collect on movements * **Settlement** — money in transit to or from the outside world * **Expenses** — money you pay out This is your raw material. Every business has its own version of this list. ## Step 2: Decide what needs its own truth *** Not every actor needs its own account. The test is simple: **does this thing need its own separate, trustworthy balance — its own truth?** If you need to know, at any moment, exactly how much value sits with this actor, it needs its own account. If it's just a label or a detail of something else, it doesn't. A good rule of thumb: if you'd ever want to ask "how much is in *here* right now?" — that's an account. If two things should never share a balance, they should never share an account. ## Step 3: Classify and group with three questions *** This is the heart of the method. For each account you kept, ask **three universal questions**. Together they tell you how to structure and group your accounts. | Ask yourself | What it's about | | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | Do these accounts need a **rule that validates** what they are or how they behave? | Classification — enforcing that an account is a certain *kind*, with its own constraints. | | Do I need to **slice them for reporting**? | Labeling — tagging accounts so you can later filter and report by group (region, tier, department). | | Do I need to **group them operationally** around a customer or wallet? | Operational grouping — bundling several accounts that belong together for one owner. | These three questions are independent. An account might need all three, or just one. Keep them separate in your mind: ```mermaid mermaid theme={null} flowchart TD A["An account in your plan"] --> Q1{"Needs a rule that
validates its kind?"} Q1 -->|yes| C["Give it a classification"] A --> Q2{"Needs to be sliced
for reporting?"} Q2 -->|yes| L["Give it a reporting label"] A --> Q3{"Belongs to a customer
or wallet group?"} Q3 -->|yes| G["Put it in an operational group"] classDef q fill:#fff8e1,stroke:#f4b400,color:#333; class Q1,Q2,Q3 q; ``` The big mistake here is collapsing these three into one. *Classification* (what an account is allowed to be) is not the same as a *reporting slice* (how you'd group it in a report), which is not the same as an *operational group* (which accounts belong to one customer). Treating them as one tangled thing is how ledger plans become unmanageable. ## Step 4: Sketch the movements *** Finally, draw the arrows. For each kind of movement your business makes, sketch which account is the source and which is the destination. This is where your plan comes alive — and it's exactly the input you'll need when you later define reusable rules for those movements. ## A worked example: a simple wallet business *** Say you run a wallet app. Customers load money in, send it to each other, and you take a small fee on transfers. Walk the method: **Actors:** customers, your fee/revenue account, and a settlement account for money entering from the outside world. **Own truth?** Each customer wallet needs its own balance — yes. Fees collected need their own balance — yes. Settlement needs its own balance to mirror the outside world — yes. **Three questions:** * *Validation?* Customer wallets behave differently from your fee account, so they're a different **kind** — classify them. * *Reporting slice?* You want to report customers by country — add a **label** for region. * *Operational group?* A premium customer might hold several wallets — **group** them together. **Movements:** a transfer moves value from one customer wallet to another, plus a small part into the fee account. Money entering the app moves from settlement into a customer wallet. ```mermaid mermaid theme={null} flowchart LR S["Settlement
(outside world)"] -->|load| C1["Customer wallet A"] C1 -->|transfer| C2["Customer wallet B"] C1 -->|fee part| F["Fee / revenue"] classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class C1,C2,F,S bal; ``` That's a complete ledger plan for a small business — sketched before touching any tool. ## Common first-timer mistakes *** * **Over-classifying.** Creating a dozen account kinds when two would do. Start coarse; refine only when a real rule demands it. * **Mixing reporting into structure.** Don't bake a reporting slice (like "region") into an account's *kind*. Keep how you classify separate from how you report. * **Treating the plan as a list.** Rushing to create accounts before sketching movements leaves you reorganizing later. * **Skipping the "own truth" test.** Giving everything its own account — or cramming unrelated things into one — both cause pain. ## In short *** * A **ledger plan** is a deliberate classification of the accounts your business needs — not a list to rush. * Design it like rooms in a house: decide purpose and access **before** you build. * The method: (1) list real-world actors, (2) keep the ones that need their **own truth**, (3) classify and group them with the **three questions**, (4) sketch the movements. * Keep the three questions — *validation*, *reporting slice*, *operational grouping* — separate. Collapsing them is the classic mistake. **See it in Lerian** Those three questions map onto how Lerian models accounts: * *Validation rule?* → **Account Types** * *Reporting slice?* → **Segments** * *Operational grouping?* → **Portfolios** See it all together in [Accounting in Midaz](/en/midaz/accounting-in-midaz), then put your plan to work with the console [Setup path](/en/midaz/console/midaz-console-setup-path) and [Concepts map](/en/midaz/console/midaz-console-concepts-map). # How money is recorded Source: https://docs.lerian.studio/en/fundamentals/core-banking/double-entry-explained Why every movement of money has two sides, what debit and credit really mean in a ledger, and how double-entry keeps every transaction balanced and auditable. If you've ever looked at a banking system and felt lost the moment someone said "debit" and "credit", this page is for you. You don't need an accounting background. You just need one idea — and once it clicks, almost everything else in core banking falls into place. Here it is: **money never appears out of nowhere, and it never simply vanishes. It moves.** Every time money moves, it leaves one place and arrives at another. Recording *both* sides of that movement is the whole idea behind **double-entry accounting**. ## Money always comes from somewhere *** Think about pouring water between two buckets. The water that leaves the first bucket is exactly the water that fills the second. Nothing is created, nothing is lost — it just changes place. You can always check that the amounts match. A core banking system records money the same way. Every movement has two parts: * **Where it came from** — the source * **Where it went** — the destination If a customer sends R\$100 to a merchant, the system doesn't just say "the merchant has R\$100 more". It records the *full story*: R\$100 **left** the customer and R\$100 **arrived** at the merchant. Two sides of one movement, always equal. This is why it's called double-entry: every movement is written down **twice** — once as it leaves, once as it arrives. A single-sided record would let money quietly appear or disappear. Double-entry makes that impossible. ## The two sides have names: debit and credit *** The two sides of a movement have traditional names. Don't let them intimidate you — they're just labels for "out of" and "into": * A **debit** is the entry on the account that money moves **out of**. * A **credit** is the entry on the account that money moves **into**. That's it. When the customer pays the merchant, the system records a **debit** on the customer's side and a **credit** on the merchant's side. Same amount, opposite directions. You'll sometimes see debit and credit described as "decrease" and "increase", but that's not always true — it depends on the kind of account. The reliable way to think about it is always the same: **debit is the source, credit is the destination.** ## Why both sides are always required *** Because every movement is recorded on both sides, the totals always have to match. The sum of everything that left equals the sum of everything that arrived. If they don't balance, something is wrong — and the system won't let the movement through. ```mermaid theme={null} flowchart LR A["Customer account
(source)"] -->|"– R$100 · debit"| T(("Movement
R$100")) T -->|"+ R$100 · credit"| B["Merchant account
(destination)"] classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class A,B bal; ``` This single rule — *the two sides must balance* — is what makes a financial system trustworthy. It means the records can never silently lose money or invent it. Every value in the system can be traced back to a movement that has a clear source and a clear destination. Each individual part of a movement — a single debit or a single credit — is called an **operation**. A movement of money is built from operations that balance out. You'll see this word a lot, so it's worth remembering: **an operation is one part of a movement.** ## In short *** * Money doesn't appear or disappear — it **moves** from a source to a destination. * **Double-entry** means every movement is recorded on both sides: a **debit** (out of) and a **credit** (into). * The two sides always carry the same amount, so the books **always balance** — that's what keeps the system trustworthy. * A single debit or credit is called an **operation**. Keep this mental model close. Everything that follows — accounts, balances, transactions, fees — is just this idea, applied. **See it in Lerian** See these ideas in practice: [Transactions](/en/midaz/transactions) and the [Glossary](/en/glossary). # The outside world Source: https://docs.lerian.studio/en/fundamentals/core-banking/external-accounts-and-reconciliation The doorway between your Ledger and the outside world — how external accounts work and how reconciliation proves your records match reality across systems. Everything so far has lived neatly inside your Ledger. But money doesn't stay inside — it arrives from banks, card networks, and other systems, and it leaves to them too. So how does a self-contained Ledger connect to the world outside? And once it does, how do you *prove* your records still match what really happened out there? Two ideas answer this, and they're really one story. Picture a building with a single front door. Everyone and everything coming in or going out passes through it. The door doesn't store anything itself — it just marks the boundary between inside and outside. Your Ledger has a door like this, and it's worth understanding before you trust the numbers on either side of it. ## External accounts: the doorway to the outside *** An **external account** is the doorway between your Ledger and the world beyond it — banks, card networks, other systems. Money entering your Ledger comes *from* an external account; money leaving goes *to* one. Every crossing of your boundary passes through it. In Midaz, the default external account has `debit` direction. When R\$100 flows *in* to a customer, Midaz uses that external account as the source: its debit increases the available amount, balancing the credit on the receiving account. The direction does not mean the external account has a negative balance. Think of the building's front door again. The external account is that door. Its direction tells Midaz how to apply a debit or credit; it does not label a balance as missing money. ```mermaid mermaid theme={null} flowchart LR E["External account
(the doorway)"] -->|"+ R$100 in"| A["Customer account
(inside the Ledger)"] classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class A bal; classDef ext fill:#eef2ff,stroke:#6366f1,color:#333; class E ext; ``` ## Reconciliation: proving the books match reality *** Once you have a boundary, you need to check that what you recorded inside matches what the outside world recorded. That check is **reconciliation**: proving your internal records line up with external records — bank statements, processor reports, settlement files. You compare the two, line by line, and confirm they agree. When they don't, reconciliation is how you catch it: * **Missing** movements — something happened outside that your Ledger never recorded. * **Duplicated** movements — the same thing recorded twice. * **Mismatched** movements — amounts or details that don't line up. When do you do it? It depends on your business — some reconcile **continuously** as records arrive, others at the **end of each day**. Either way, it's the natural counterpart to having a boundary: the moment you connect to the outside world, you take on the job of proving the two sides still agree. Reconciliation is like **counting the till against the receipts** at the end of a shift. The cash drawer is your Ledger; the receipts are the outside record. Counting them against each other proves the books match what actually happened — and flags anything that doesn't. ## In short *** * An **external account** is the doorway between your Ledger and the outside world — money in comes from it, money out goes to it. * In Midaz, the default external account has `debit` direction, so a debit increases its available amount as the source of an inflow. * **Reconciliation** is proving your internal records match external ones (bank statements, processor reports). * It catches **missing**, **duplicated**, and **mismatched** movements, run **continuously** or at **end of day**. **See it in Lerian** See these ideas in practice: [Transactions](/en/midaz/transactions) and the [Glossary](/en/glossary). # Who owns what: holders, accounts & aliases Source: https://docs.lerian.studio/en/fundamentals/core-banking/holders-accounts-aliases Holders, accounts, and aliases untangled — what the ledger actually owns, who stands behind it in the real world, and the friendly names that point to it. You've seen accounts hold balances and movements flow between them. But who *owns* those accounts? And why do banking systems keep tossing around words like "alias" and "alias account" as if they were obviously different things? This page untangles all of it. Imagine a row of numbered safe-deposit boxes in a vault. The box itself just holds what's inside it — it doesn't know or care who you are. To make that vault useful, you also need a record of *who* the box belongs to, and a friendlier way to refer to it than a long number. Those are three separate jobs, and core banking keeps them separate on purpose. ## The account is the truth *** An **account** is the balance container — the truth that lives inside the Ledger. It records exactly how much of an asset it holds, built up from every movement in and out. That's all it does, and that's deliberate: the account stays clean and reliable because it carries no messy real-world identity data. In the vault, the account is the numbered box. It holds value; it doesn't hold your name, your documents, or your phone number. ## The holder is the person or company *** A **holder** is the real-world owner standing behind the account — **the person or company you register**. This is the customer record: name, document number, contact details. Registering these owners is its own job, often called *cadastro de pessoas* — keeping track of the people and companies your business deals with. Why keep the holder separate from the account? Two reasons: * One owner can hold **many accounts**. The owner is registered once; the accounts attach to them. * The Ledger stays trustworthy. Identity data changes often and is sometimes sensitive — you don't want it tangled into the records that prove your balances. In the vault, the holder is the person named on the contract. One person can rent several boxes. ## Aliases: friendly names for accounts *** Accounts are identified by long, unique IDs — accurate, but no fun to type or remember. So you give them friendlier names. Here's where two similar-sounding terms get confused — they do different things: * An **alias** is a human-friendly handle for an account — a nickname like `marketing-fund` so you don't juggle a long ID. * An **alias account** is a record that ties an owner's real-world banking details (bank, branch, account number) to an underlying Ledger account. So an *alias* is just a convenient label pointing at one of your own accounts. An *alias account* is a bridge — it connects outside banking details to the right account inside your Ledger. Back to the vault. The **alias** is the nickname sticker on the box ("Marketing fund") so staff don't need the box number. The **alias account** is the front-desk index card that links a customer's external bank details to which box is theirs. ```mermaid mermaid theme={null} flowchart LR H["Holder
(registered person/company)"] --> AA["Alias account
(real banking details)"] AA --> ACC["Account
(balance in the Ledger)"] AL["Alias
(friendly nickname)"] -.points to.-> ACC classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class ACC bal; ``` ## Why bother with all this separation *** Splitting these roles apart looks like extra work, but it buys you real flexibility: * One holder, **many accounts**, without duplicating the owner each time. * Friendly **aliases** so people and systems reference accounts without long IDs. * **Alias accounts** that connect to multiple banks and external systems — without ever polluting the Ledger's clean record of value. The Ledger keeps the truth. Everything else points at it. ## In short *** * An **account** is the balance container — the truth inside the Ledger, free of identity data. * A **holder** is the real-world owner you register — the person or company (*cadastro de pessoas*). One holder can own many accounts. * An **alias** is a friendly nickname pointing at one of your accounts. * An **alias account** is a record linking real-world banking details to a Ledger account. * Keeping these separate buys flexibility while keeping the Ledger clean. **See it in Lerian** See these ideas in practice: [Accounts](/en/midaz/accounts) and [Alias accounts](/en/midaz/crm/alias-accounts). # How money moves Source: https://docs.lerian.studio/en/fundamentals/core-banking/how-money-moves What counts as one movement of money, what its parts are called — transactions, operations, and holds — and where money sits before it finally settles. You already know that money never appears or vanishes — it moves from a source to a destination, and both sides get recorded. This page is about the *movement* itself: what one complete movement is, what its pieces are called, and the part most people find confusing — where the money sits while a movement is still in progress. Picture handing cash across a counter. The moment the cash leaves your hand and lands in the other person's, the whole exchange happened as one event. A banking system treats movement the same way: one complete handover, recorded in full, all at once. ## One movement, recorded in full *** A **transaction** is one complete movement of value — the whole event, start to finish. When a customer sends R\$100 to a merchant, that single transaction *is* the R\$100 changing hands. But you've already seen that every movement has two sides. So a transaction is built from smaller pieces: * An **operation** is one part of the movement — a single debit or a single credit. * A transaction is made of operations that **balance**: everything debited equals everything credited. The R\$100 transfer is one transaction made of two operations — a debit of R\$100 leaving the customer, and a credit of R\$100 arriving at the merchant. Same amount, opposite directions, recorded together. Keep this word handy: an **operation is one part of a movement**. A transaction is the whole movement; the operations are its parts. You'll meet this word everywhere in core banking. ## The shapes a movement can take *** The simplest movement has one source and one destination, but a single transaction can spread across more accounts — and however many parts it has, it always balances. The common shapes: * **One-to-one** — one source, one destination. A plain transfer: customer out, merchant in. * **One-to-many** — one source, several destinations. The customer pays R\$103: R\$100 goes to the merchant and R\$3 to a fee account. Three operations, still balanced (R\$103 out = R\$100 + R\$3 in). * **Many-to-one** — several sources, one destination. A few accounts pooling money into a single fund. * **Many-to-many** — several sources and several destinations at once, like a marketplace splitting incoming payments across sellers and fees. The rule never changes, no matter how many parts there are: **everything that leaves must equal everything that arrives.** Not every ledger supports all four shapes. Simpler systems may allow only one-to-one (or one-to-many) movements, while others handle the full many-to-many case in a single transaction — so what's possible depends on the ledger you use. A transaction is **all-or-nothing**. Either every operation succeeds together, or none of them happen. The books can never be left half-recorded — this property is called **atomicity**. ## Money in transit: holds and two-phase movements *** Here's the part that trips up almost everyone: where does the money sit *before* a transaction is final? Many movements don't settle the instant they start. The system first **reserves** the amount — it's set aside, still in the account, but no longer free to spend. Then the transaction either **commits** (settles for good) or **cancels** (releases the reservation). This is a **two-phase** movement: reserve first, then commit or cancel. To make sense of this, an account's money is described in two parts: * **Available balance** — what you can actually spend right now. * **Held balance** — what's reserved for a movement that hasn't finished. Still yours, just frozen in place until it settles or releases. Think of a hotel pre-authorization on your card. At check-in the hotel **reserves** an amount — it isn't gone, but you can't spend it. At check-out the charge **commits**, or the hold is **canceled** and the money frees up. Same money, two phases. ```mermaid mermaid theme={null} flowchart LR R["Reserve
amount held"] -->|"settles"| C["Commit
moves to destination"] R -->|"released"| X["Cancel
returns to available"] classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class R,C,X bal; ``` ## Spending control *** Once you can see money as *available* versus *held*, controlling spending becomes natural. At any moment, an account's money sits in one of three states: * **Available** — free to spend. * **Held** — reserved for a movement in progress (in transit). * **Blocked** — frozen on purpose, set aside so it can't move at all. On top of that, movement can be capped by **limits** — a ceiling on how much can flow. Together, these explain the question people always ask: *why can't this account spend?* Usually it's because the money is held, blocked, or over a limit — not missing. ## In short *** * A **transaction** is one complete movement of value — the whole event. * An **operation** is one part of it: a single debit or credit. A transaction is operations that balance. * A movement can range from **one-to-one** to **many-to-many**; whatever its shape, it's **all-or-nothing** and always balances. * Money in transit is **held**, not gone: a two-phase movement **reserves**, then **commits** or **cancels**. * An account's money is **available**, **held**, or **blocked**, and can be capped by **limits** — that's spending control. **See it in Lerian** See these ideas in practice: [Transactions](/en/midaz/transactions), [Operations](/en/midaz/operations), and [Balances](/en/midaz/balances). # The rules that govern movement Source: https://docs.lerian.studio/en/fundamentals/core-banking/routes-and-fees The reusable transaction routes that keep each kind of money movement recorded correctly, and why a fee is just a few more operations on the same journey. You know how money moves and how it's recorded. But who decides the *rules*? When a payment happens, something has to make sure the right kinds of accounts are involved and the right parts get recorded — every single time, the same way. And when a fee shows up, where does it actually come from? This page demystifies both. Think of a paper form built for one kind of request. Someone designed it once — the boxes it has, the line for who's allowed to sign, the rule for how it's filed. After that, any clerk just fills in the details, and every request of that kind comes out identical — no guessing. That's the idea behind a route: set the rule once, and it's applied the same way every time. ## A route is a reusable rule for one kind of movement *** A **route** is a reusable rule for one *kind* of movement. It answers three questions, once, so you don't answer them again on every transaction: * **Who can be the source?** Which accounts are allowed to send. * **Who can be the destination?** Which accounts are allowed to receive. * **Which parts get recorded?** Which debits and credits the movement should produce. Define the rule once, and every transaction of that kind follows it. A "customer payment" route spells out that a customer account sends, a merchant account receives, and exactly which operations to write. Why does this exist? So the same kind of movement is **always recorded correctly**, and **only valid accounts** take part. No clerk fills the form in wrong, because the form won't let them. A route is like a **form template** for one kind of transaction. It lays out who's allowed and how the movement is booked, so every movement of that kind comes out identical. A fee, as you'll see, is just an extra line on that form. ## Fees are just more operations *** Fees feel mysterious, but they're not magic — and they're *not* "subtract a number and move on". Remember that a transaction is made of **operations** (each one part — a single debit or credit). A fee is simply *more operations* in the same transaction, sending value into a fee or revenue account. Take the one-to-many example from before. A customer pays **R\$103**: * **R\$100** lands at the **merchant**. * **R\$3** lands in a **fee account**. That's one transaction with three operations. The customer's R\$103 leaving balances exactly against R\$100 + R\$3 arriving. Nothing is invented or lost — the fee is real money moving to a real account, recorded like any other part. ```mermaid mermaid theme={null} flowchart LR C["Customer
– R$103"] --> R{{"Route
(rule + parts)"}} R -->|"+ R$100"| M["Merchant"] R -->|"+ R$3"| F["Fee account"] classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class M,F bal; ``` Because the fee is part of the same transaction, it's all-or-nothing too: the merchant gets paid and the fee is collected together, or neither happens. ## When you'd reach for routes and fees *** You don't need a route for a one-off movement. You reach for one when a *kind* of movement repeats and must always be booked the same way — payments, transfers, settlements. And you add a **fee** whenever a movement should also send a slice of value into a revenue or cost account. Defining these deliberately up front is part of planning your Ledger, which comes next. ## In short *** * A **route** is a reusable rule for one kind of movement: who can send, who can receive, and which parts get recorded. * Routes keep every movement of a kind **recorded correctly**, with **only valid accounts** taking part. * A **fee** isn't magic — it's just **more operations** moving value into a fee or revenue account (customer R\$103 = merchant R\$100 + fee R\$3). * Because the fee is part of the same transaction, it's **all-or-nothing** with the rest. **See it in Lerian** See these ideas in practice: [Accounting Routes entities](/en/midaz/transaction-routing-entities) and the [Fees Engine](/en/midaz/fees/fees-engine-overview). # What is core banking? Source: https://docs.lerian.studio/en/fundamentals/core-banking/what-is-core-banking A plain definition of core banking, what it manages — accounts, balances, transactions — and the five responsibilities we use to explain a modern core system. A **core banking system** is the central platform a financial institution runs on. It manages **accounts, balances, transactions, the customers behind them, and money moving in and out** — all in one place, as a single source of truth. Underneath all of it sits one idea that ties everything together: a trustworthy record of **who owns what** and **what moved**. The other jobs all connect to it — feeding it, moving value through it, reading it, proving it's right. That's what makes banking *banking*: money can never be quietly lost or invented. You don't need a banking background to follow along — this page just gives you the lay of the land. ## Why it's harder than it looks *** Keeping that record right is deceptively hard, and it's the reason most of the system exists: * **It must always balance** — money can't appear or disappear, ever. * **It has to stay correct at scale** — millions of movements, all consistent with each other. * **It must be provable** — to auditors and regulators, long after the fact. ## How we'll explain it: five jobs *** There are many ways to describe a core banking platform. In this guide we use **five core responsibilities** — it's how we organize the topic for learning, not an official taxonomy. | Job | What it does | | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | | **Record value** | Keeps the trustworthy record of who owns what, balanced so nothing is lost or invented. Everything else builds on this. | | **Move value** | Brings money in, sends it out, and transfers it between accounts. | | **Identify** | Registers the people and companies behind the accounts. | | **Report** | Turns the raw record into statements and reports you — and regulators — can read. | | **Trace & prove** | Keeps an auditable trail of everything that happened. | The **Ledger** is the foundation — but it's only **one of the five**. A complete core banking system is all five working together, not a Ledger with extras bolted on. The other four build on the Ledger once value is being recorded, and capabilities like **fees** attach to it to extend it. You adopt these pieces one at a time, not as a single machine you switch on. The upside: the same handful of ideas — accounts, balances, movements — run through all of them. Learn those once, here, and the rest of the documentation gets much easier to follow. ## In short *** * **Core banking** is the central platform that manages accounts, balances, transactions, customers, and payments — all on one trustworthy record of who owns what and what moved. * Keeping that record correct at scale, and provable after the fact, is the hard part — and the reason for most of what the system does. * To learn it, we use **five responsibilities** — **record**, **move**, **identify**, **report**, and **trace & prove**. The **Ledger** is the foundation, but core banking is all five, not the Ledger alone. **See it in Lerian** See how these jobs map to real solutions in the [Core banking vision](/en/core-banking-vision) and [Products and plugins](/en/products-and-plugins). # Where to go next Source: https://docs.lerian.studio/en/fundamentals/core-banking/where-to-go-next How everything you learned about core banking shows up in Lerian and Midaz — and where to go next to start building, integrating, and running transactions. You've gone from "lost the moment someone said debit and credit" to modeling your own business as a ledger plan. That's the whole foundation — here's how it shows up in Lerian, and where to start. ## How the concepts show up in Lerian *** Everything you just learned has a home in Lerian: * **Recording value** — the accounts, balances, and assets you learned about, and the double-entry that keeps them in check, all live in [Midaz](/en/midaz/what-is-midaz), the Ledger. It's the foundation everything else builds on. * **Moving value** — transfers, operations, and holds are recorded as [transactions](/en/midaz/transactions) in Midaz. When value crosses the boundary with the outside world, payment integrations like [Pix](/en/rails/pix/pix-overview) and [TED](/en/rails/ted/ted-overview) carry it — but plenty of movement is simply recorded in the Ledger. The routes and fees you saw come from Midaz's [transaction routing](/en/midaz/transaction-routing-entities) and the [Fees Engine](/en/midaz/fees/fees-engine-overview). * **Identifying who owns what** — the holders and friendly aliases behind your accounts live in [CRM](/en/midaz/crm/crm-overview). * **Reading the record back** — Midaz produces [account statements](/en/midaz/building-account-statements) straight from the ledger, and [Reporter](/en/reporter/what-is-reporter) builds richer reports and templates for audits, analytics, and decisions. * **Tracing and proving** — [Tracer](/en/tracer/what-is-tracer) gives you the auditable trail across everything that happened. And when you design your own ledger plan, the three questions — validation, reporting slice, operational grouping — map to **Account Types, Segments, and Portfolios** in Midaz. ## Ready to start? *** There are two ways to put this into practice — pick what fits your team: Integrate programmatically — start here, then go deeper with the Guides and API Reference for the integration details. Prefer a UI? Do the whole setup by hand in the Lerian Console — no code required. Need a term? The [Glossary](/en/glossary) defines every word from this tab. Keep this tab bookmarked — and see how the pieces come together in [Building a complete core banking](/en/building-a-complete-core-banking). # Where to start Source: https://docs.lerian.studio/en/fundamentals/introduction A quick map of the Fundamentals tab — two plain-language series, Core Banking and Accounting, and which one to read first for what you want to learn. The **Fundamentals** tab is where you build intuition before touching anything Lerian-specific. It holds two short, plain-language series that build a foundation for core banking from two angles: accounting explains trustworthy financial records, while Core Banking explains how a complete banking platform applies them. Explains the language underneath it: assets, liabilities, debits, credits, and how the books stay balanced. Explains a complete core banking platform: its ledger foundation, accounts, balances, value movement, customer identity, reporting, and auditability. They sit side by side because they answer different questions. One shows you the *machine*; the other shows you the *grammar* that machine is built on. ## Which one first *** | If you want to… | Start with | | -------------------------------------------------------------------------------------- | ------------------------------------- | | Understand how a complete core banking platform works, including its ledger foundation | **Core Banking** | | Understand the accounting language behind it | **Accounting** | | Understand both | **Accounting**, then **Core Banking** | If you're unsure, start with **Accounting**. The vocabulary it teaches makes everything in Core Banking — and the rest of the docs — easier to follow. # Getting started Source: https://docs.lerian.studio/en/getting-started Begin with Lerian — evaluate the platform, choose a deployment model, and run your first double-entry transaction with Midaz to see the ledger in action. Whether you want to evaluate the platform or start building, this is the place to begin. **New to core banking?** If terms like *Ledger*, *debit and credit*, or *chart of accounts* aren't yet second nature, start with the fundamentals first — they'll make everything here click into place. **[Begin with Core Banking Fundamentals](/en/fundamentals/core-banking/what-is-core-banking)** ## Evaluating the platform *** Follow these steps to understand the platform's capabilities, architecture, and deployment options. Learn what problems the platform solves and who we built it for — in business terms. **[Why Lerian →](/en/why-lerian)** Lerian organizes around four pillars — **Ledger**, **Transactional Services**, **Governance**, and **Connectivity** — that form a composable financial platform. **[Read the Core Banking vision →](/en/core-banking-vision)** Decide between Lerian Cloud (SaaS), fully managed by Lerian, or self-hosted BYOC based on your compliance requirements and operational maturity. **[Compare deployment models →](/en/deployment-models)** Explore real-world use cases — digital banking, global portfolios, Pix, marketplace payouts, and more. **[Browse use cases →](/en/midaz/use-cases)** ## Building with Lerian *** Follow these steps to go from zero to your first double-entry transaction. Choose the path that matches your deployment model. Your Lerian onboarding team provides you with API credentials (client ID and secret) and the base URL for your environment. Use the Access Manager Auth API to exchange your credentials for a JWT access token. This token contains your tenant context — you don't need to manage a tenant ID explicitly. **[Learn how to authenticate →](/en/platform/access-manager/using-access-manager)** Include the token as a Bearer token in the `Authorization` header of every request. Your calls are automatically scoped to your tenant. **[Make your first API call →](/en/reference/quick-start-guide)** Follow the same API flow as any Midaz deployment — create an organization, a ledger, assets, and accounts. The platform handles tenant isolation transparently. **[Follow the recommended workflow →](/en/midaz/recommended-workflow)** The API surface is identical across deployment models. Lerian Cloud and BYOC Multi-Tenant require a Bearer token and scope requests to the authenticated tenant. A permitted non-production BYOC Single-Tenant deployment can have authentication disabled. Learn more about [multi-tenancy](/en/multi-tenancy). Decide how you want to run the platform. BYOC gives you full control over data, security, and networking — in your own cloud or on-premises. **[Compare deployment models →](/en/deployment-models)** Lerian organizes around four pillars — **Ledger**, **Transactional Services**, **Governance**, and **Connectivity** — that form a composable financial platform. Understanding this helps you model integrations correctly from day one. **[Read the Core Banking vision →](/en/core-banking-vision)** Midaz is Lerian's double-entry core ledger. Install it when you need to model accounts and record transactions; other products remain modular and can be adopted independently. **[Install Midaz →](/en/midaz/midaz-setup)** With Midaz running, create the essential building blocks: an organization, a Ledger, assets, and accounts. **[Make your first API call →](/en/reference/quick-start-guide)** Move beyond isolated API calls and follow the end-to-end operational workflow — from resource setup to transaction routing and balance management. **[Follow the recommended workflow →](/en/midaz/recommended-workflow)** ## What's next? *** With your Ledger running and your first transactions created, pick the capability you want to add next. Run your first automated reconciliation between a bank statement and your Ledger. Set up spending limits and validation rules to evaluate transactions before they proceed and return an ALLOW, DENY, or REVIEW decision for your authorization flow to act on. Create a report template and generate your first financial report from a configured data source. For Midaz data, connect Reporter to Midaz first. # Glossary Source: https://docs.lerian.studio/en/glossary Key financial, ledger, and platform terms explained for non-technical audiences — a quick reference for language used across Lerian and Midaz docs. This glossary defines the core financial and platform terms used throughout Lerian's documentation. It is designed to help product managers, sales teams, and anyone new to financial infrastructure quickly understand the language used in our guides. *** ## A *** ### Account The fundamental unit of value storage in a ledger. An account holds a balance denominated in a specific asset (e.g., BRL, USD). Accounts can represent customer accounts, internal settlement accounts, fee collection accounts, or any other container for tracking value. ### Account Type A classification that defines the nature and purpose of an account — such as checking, savings, escrow, or settlement. Account types enable proper transaction routing and ensure that business rules are applied consistently across account categories. ### Asset A unit of value tracked in the ledger. Assets can be traditional currencies (BRL, USD, EUR), digital currencies (BTC, ETH), or custom units like loyalty points or academic credits. Every account is linked to exactly one asset. ### Atomic (Atomicity) A property of transactions where all operations either succeed together or fail together — there is no partial execution. If a transaction involves three operations (a debit, a credit, and a fee), either all three happen or none do. This prevents incomplete financial events. ### Audit trail A chronological record of every action and transaction in the system. Audit trails are immutable — once recorded, they cannot be modified or deleted. This is essential for regulatory compliance (SOX, GLBA) and dispute resolution. *** ## B *** ### Balance The current amount of an asset held in an account. Midaz tracks two balance amounts: **available** (can be used immediately) and **on hold** (reserved for pending transactions). ### Blast radius The scope of impact if a failure, misconfiguration, or security incident occurs. In multi-tenancy contexts, blast radius refers to how many tenants are affected when something goes wrong in one tenant's scope. DATABASE mode minimises blast radius because each tenant has a fully isolated database; a failure or data corruption affects only that tenant. SCHEMA mode has a wider blast radius because tenants share the same database process. See also: **DATABASE mode**, **SCHEMA mode**, **Tenant isolation**. ### BYOC (Bring Your Own Cloud) A deployment model where you run Lerian software on your own infrastructure — whether public cloud (AWS, GCP, Azure), private cloud, or on-premises. This gives you full control over data residency, security, and scaling. See also: **SaaS**. *** ## C *** ### CEL (Common Expression Language) A lightweight, type-safe expression language that Tracer uses for writing business rules. CEL lets analysts and compliance teams define validation logic — such as "if transaction amount exceeds 10,000 and account type is personal, flag for review" — without writing application code or requiring deployments. ### Core banking A complete banking platform that combines a ledger foundation with the capabilities required to operate financial products: accounts and balances, value movement, customer identity, reporting, governance, connectivity, and auditability. A ledger is foundational, but it is not the whole core banking platform. ### Child account A sub-account created under a parent account, typically used for budgeting or organizational purposes. For example, a customer's savings account might have child accounts for "Vacation Fund" and "Emergency Fund," each tracking its own balance while rolling up to the parent. *** ## D *** ### Double-entry accounting An accounting principle where every financial movement is recorded as at least two operations: a debit from one account and a credit to another. This ensures the system always balances — the total of all debits equals the total of all credits. Midaz enforces double-entry automatically on every transaction. ### DSL (Domain-Specific Language) A simplified programming language designed for a specific domain. In Flowker, the DSL lets teams define workflow steps, conditions, and integrations using a structured format — without writing general-purpose code. *** ## E *** ### ERP (Enterprise Resource Planning) A business management software system — such as SAP, Oracle, or NetSuite — that integrates core processes like accounting, procurement, and inventory. ERPs are common data sources for reconciliation with Matcher. ### External account A special account in Midaz (prefixed with `@external/`) that represents value entering or leaving the system from outside. For example, when a student enrolls in a course, credits move from `@external/available-courses` into the student's account. External accounts act as boundaries between your ledger and the outside world. *** ## F *** ### Fail closed A safety posture where a system denies access by default when it cannot verify a required condition, rather than allowing access and risking a breach. In Lerian multi-tenancy, tenant resolution is fail-closed: if the tenant cannot be identified from the JWT, or if tenant configuration cannot be retrieved, the request is rejected — never served against a default, shared, or another tenant's data. See also: **Tenant isolation**, **Multi-tenancy**. *** ## H *** ### Helm Chart A pre-configured deployment package for Kubernetes. Helm Charts contain all the instructions needed to install, configure, and upgrade software in a Kubernetes cluster. Lerian provides Helm Charts for deploying products in BYOC (self-hosted) environments. *** ## I *** ### Idempotency The property that an operation produces the same result whether executed once or multiple times. In financial systems, this prevents duplicate transactions — if a payment request is accidentally sent twice, idempotency ensures the money only moves once. ### Immutable (Immutability) A property meaning "cannot be changed or deleted after creation." In Midaz, all transactions and audit records are immutable — once recorded, they exist permanently. This guarantees data integrity for compliance and auditing. *** ## K *** ### Kubernetes A platform for running and managing software applications at scale across multiple servers. Kubernetes automates deployment, scaling, and recovery — ensuring applications stay available even when individual servers fail. Required for BYOC deployments of Lerian products. *** ## L *** ### Ledger The core financial book that records all transactions, balances, and operations for an organization. Think of it as the single source of truth for a business unit's finances. An organization can have multiple ledgers for different purposes (e.g., retail operations, treasury). *** ## M *** ### Metadata Additional information attached to entities like accounts, transactions, or organizations. Metadata is stored as key-value pairs and can hold any custom data your business needs — such as a customer's external ID, a transaction's reference number, or a department code. It does not affect ledger logic but enriches records for reporting and integration. ### Multi-tenancy An operating model where one deployment serves independent customer contexts — called tenants — with product-specific request scoping and storage isolation. Lerian Cloud operates multi-tenant; BYOC multi-tenancy is available only for products and entitlements that support it. Tenant Manager uses `dedicated` and `shared` for PostgreSQL modes described as `DATABASE` and `SCHEMA`. See also: **Tenant**, **Tenant isolation**. *** ## O *** ### Onboarding domain The part of Midaz responsible for structuring your financial ecosystem — creating organizations, ledgers, assets, accounts, portfolios, and segments. This is the "setup" phase before any transactions occur. ### Operation The atomic unit of movement in the ledger — a single debit or credit. Every transaction is composed of one or more operations, providing precise traceability for compliance and reporting. ### Operation Route A reusable template that defines the rules for one "leg" of a financial transaction — specifying which accounts participate, in what direction (source/destination), and under what accounting annotation. ### Organization The top-level business entity in Midaz — typically a bank, fintech, or subsidiary. Each organization holds its own ledgers, configurations, and access controls. *** ## P *** ### Pix Brazil's instant payment system, operated by the Central Bank of Brazil (BACEN). Pix enables real-time transfers 24/7 between individuals, businesses, and government entities. In the Lerian ecosystem, the Pix plugin connects Midaz to the Pix network, handling payment initiation, settlement, and fee management. ### Portfolio A grouping of accounts belonging to the same customer, business unit, or purpose. For example, a single client may hold checking, savings, and investment accounts across different currencies — a portfolio ties them together for a unified view. ### Plugin An optional extension that adds functionality to a specific Lerian product. Examples include Pix (instant payments), TED (bank transfers), and Payments (boletos and bill payment). Plugins run inside a host product's architecture rather than standalone — today, all available plugins extend Midaz. *** ## R *** ### Reconciliation The process of comparing two sets of records — for example, your internal ledger against bank statements — to verify they match. Matcher automates this process by applying matching rules and flagging discrepancies. *** ## S *** ### Lerian Cloud (SaaS) Lerian's fully managed deployment model. Lerian operates the licensed platform modules and underlying infrastructure; customers use the supported product APIs and configuration surfaces. See also: **BYOC**. ### Segment A classification applied to accounts that share characteristics — like customer tiers (VIP, Student, Business) or regional divisions. Segments enable differentiated rules, pricing, and benefits across your customer base. ### Service-centric architecture An architectural model in which configuration and infrastructure are organised around services rather than tenants. In Lerian multi-tenancy, a Tenant entity holds identity metadata, while a Service entity holds infrastructure configuration (database credentials, connection details, isolation mode). This separation means the same tenant can have different isolation modes across different products, and a service can be reconfigured independently of the tenant's identity. See also: **Multi-tenancy**, **Tenant isolation**. ### Settlement The process of finalizing a financial transaction — transferring the actual funds between institutions. Settlement accounts in Midaz act as clearing points where money is held temporarily before being moved to its final destination (e.g., via Pix or TED to the central bank). ### SOX (Sarbanes-Oxley Act) A U.S. federal law that mandates strict financial record-keeping and auditing standards for publicly traded companies. SOX compliance requires immutable audit trails and accurate financial reporting — capabilities that Midaz and Matcher provide by design. ### Source-available A licensing model where source code is publicly accessible under terms that restrict some uses. Within the Lerian portfolio, **Midaz** and **Fetcher** are source-available under the Elastic License 2.0 (ELv2); they are not generic open-source products. Other Lerian products, including Reporter, Matcher, Flowker, Tracer, and plugins, are closed source and available to licensed customers. ### Streaming Hub Lerian's event delivery edge. Streaming Hub consumes platform events from the internal event stream and fans them out to external systems through signed webhooks, message queues, or a pull API — handling delivery retries and signature verification along the way. It is a closed-source Lerian product, available to licensed customers. *** ## T *** ### Tenant A logically isolated customer context in a multi-tenant deployment. Its data model, authentication claims, routing, and backing resources are defined by the supported product service; do not assume one product's tenant model applies to another. See also: **Multi-tenancy**, **Tenant isolation**. ### Tenant isolation The controls that prevent one tenant's supported product service, data, configuration, and operations from being used by another. For PostgreSQL services managed through Tenant Manager, `dedicated` corresponds to a database per tenant and `shared` to a schema per tenant in a shared database. Authentication and routing controls remain product-specific. See also: **Multi-tenancy**, **Tenant**. ### Terraform An infrastructure automation tool that provisions and manages cloud resources — servers, databases, networks, security groups — through declarative configuration files. Lerian provides Terraform templates for setting up BYOC infrastructure on AWS, GCP, or Azure. ### TED (Transferência Eletrônica Disponível) A Brazilian electronic bank transfer system for high-value or same-day transfers between financial institutions. Unlike Pix, TED operates only during business hours. In the Lerian ecosystem, the TED plugin connects Midaz to the TED network. ### Transaction A movement of value between accounts. Transactions can involve multiple operations (debits and credits) and are processed atomically — either all operations succeed, or none do. This ensures no partial financial events occur. ### Transaction domain The part of Midaz responsible for executing financial movements — processing transactions, updating balances, and maintaining audit trails. ### Accounting Route (Transaction Route) A complete transaction blueprint composed of multiple Operation Routes. For example, a "Pix Transfer" route defines all the debits and credits involved, ensuring double-entry compliance every time that transaction type is executed. Called **Accounting Route** in the Lerian Console and product docs; exposed as `transactionRoute` in the API. *** ## W *** ### Webhook An automated notification sent from one system to another when a specific event occurs. For example, Matcher can send a webhook to your JIRA instance when a reconciliation exception is found, automatically creating a ticket for your team to investigate. # Welcome Source: https://docs.lerian.studio/en/index Explore Lerian Docs — guides, platform capabilities, API references, and integration tutorials covering Midaz, plugins, and the wider Lerian ecosystem.

Build core banking on primitives you control.

Guides, platform capabilities, and API references for the entire Lerian ecosystem — from the double-entry ledger up.

Get started → Explore the API

New to core banking? Start with the fundamentals → · Why Lerian →

post-transaction.sh











      

Go from zero to your first transaction.

01

Model your ledger

Set up your Organization, Ledger, and Accounts.

02

Post a transaction

Move value with strict double-entry guarantees.

03

Deploy anywhere

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.

Midaz CORE

The double-entry ledger at the platform's core — organizations, ledgers, accounts, and transactions you fully control. Source-available under the Elastic License 2.0.

Get started with Midaz

Matcher

Reconcile transactions across banks, processors, and ERPs, and work through the exceptions that don't match.

Reconcile a batch

Reporter

Turn ledger and platform data into audit-ready reports from configurable templates.

Build a report

Flowker

Orchestrate financial workflows and connect third-party services through its API.

Design a workflow

Tracer

Apply real-time spending controls and analyze transaction risk as money moves.

Trace a transaction

Fetcher

Connect to external databases and extract their data through one secure API. Source-available under the Elastic License 2.0.

Extract external data

Lender

Run the full consumer credit (CDC) journey — from product definition and origination through servicing, accounting, and audit.

Explore Lender

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.

Explore Streaming Hub

Brazil Rails

Where the platform meets Brazil's payment system. Reach the rails through Lerian's own messaging with BACEN, or through an interface with a connectivity partner. Either way, you keep your ledger, your data, and your business logic. What are Brazil Rails? →

Lerian native messaging Connect straight to BACEN on Lerian-owned software — Pix over SPI, TED over SPB, and five more rails. Direct Pix via JD Participate in Pix as a direct participant, with JD providing the certified connection to BACEN. Indirect Pix via BTG Offer full Pix services as an indirect participant on BTG Pactual's infrastructure. Bank Transfer (TED) Send and receive interbank transfers across Brazilian banks the same business day.

Lerian Console

One web interface to operate every Lerian product — dashboards, settings, and per-product modules in a single place.

About the Console Console settings

Platform

Shared services that run under every product — access, lifecycle, observability, and Kubernetes delivery.

Access Manager

Control who can do what, across every product.

Lifecycle Management

Provision, update, and retire resources automatically.

Observability

Centralized logs, metrics, and traces across services.

Helm Charts

Deploy on Kubernetes with official Helm charts.

News

Features, improvements, and changes across the platform and its documentation.

View latest announcements →

© 2026 Lerian. All rights reserved.

# About Midaz Source: https://docs.lerian.studio/en/midaz/about-midaz Explore Midaz's Ledger service, its Onboarding and Transaction domains, and its embedded CRM and Fees capabilities. Midaz is a source-available core ledger. Financial institutions use it to build, operate, and scale their transactions on a modular foundation. You keep full control of your stack. Midaz's Ledger service has two core domains: **Onboarding** and **Transaction**. It also embeds CRM for holders and instruments, and Fees. Together, they provide the components to configure, execute, and govern financial flows. This domain-driven architecture ensures that configuration (Onboarding) and execution (Transaction) remain clearly separated yet fully integrated — enabling performance, traceability, and business agility. ## Domains and APIs *** ### Onboarding domain In the **Onboarding Domain**, institutions structure their financial operations. They configure the essential entities of their ledger. This domain sets the rules, accounts, and relationships that govern how the ledger records and interprets transactions. #### Components of the Onboarding Domain * **Organizations**: A business entity that runs on Midaz — such as a bank, fintech, or subsidiary. Each organization holds its own ledgers, configurations, and access controls. One platform then supports multi-entity operations. * **Ledgers**: The core financial book of an organization. Ledgers track every balance, transaction, and operation. They protect the integrity of all financial data. Think of a ledger as the single source of truth for a business unit's finances. * **Assets**: The units of value that the ledger tracks — such as BRL, USD, BTC, or loyalty points. With this flexibility, institutions manage traditional currencies, digital assets, and custom reward programs in the same system. * **Account Types**: Classifications that define the nature and purpose of accounts — for example, checking, savings, escrow, or settlement. With `validateAccountType` enabled, a new non-external Account's `type` must match a registered value. Operation Routes separately validate accounts during route processing. * **Accounts**: The fundamental units of value storage — where money, or any asset, actually lives. Each account always links to an Asset. You can group accounts into segments or portfolios to reflect your business structure. * **Portfolios**: A group of accounts that belong to the same customer, business unit, or purpose. For example, one client may hold checking, savings, and investment accounts in different currencies. A portfolio ties them together for a unified view. * **Segments**: Groups of accounts that share traits, such as customer tiers or regional divisions — for example, VIP or Student. Segments help you enforce different rules, prices, or benefits across your customer base. The Onboarding Domain configures how your financial world is structured, ensuring consistency before any transaction takes place. ### Transaction Domain After you configure the Onboarding Domain, the **Transaction Domain** takes over. It powers the real-time execution, movement, and reconciliation of funds. #### Components of the Transaction Domain * **Balances**: The current balance of any account, including its available and on-hold amounts. This real-time snapshot supports validations, pre-transaction checks, and customer-facing displays. * **Operation Routes**: Reusable templates that define the rules for each leg of a financial transaction. A route specifies which accounts participate, in what direction (source or destination), and under what accounting annotation. Every transaction type then follows consistent business rules. * **Operations**: The atomic unit of movement in the ledger — a single debit or credit. The ledger breaks every financial movement into operations. This gives a complete audit trail and precise traceability for compliance and reporting. * **Accounting Routes**: Complete transaction blueprints that combine multiple Operation Routes. For example, a Pix Transfer route defines all the debits and credits involved. When you enable Accounting Route validation, it enforces double-entry compliance for that transaction type. The API exposes this as the `transactionRoute` resource. * **Transactions**: The actual movement of value between accounts. A transaction can involve multiple **Operations**, such as fees, taxes, or splits. This supports complex financial flows, like marketplace payouts or multi-party settlements, in a single atomic event. The Transaction Domain ensures that every movement is processed accurately, transparently, and in compliance with business and accounting rules configured upstream. ## In short *** Midaz is a domain-driven ledger platform for reliability, flexibility, and governance. | Domain | Purpose | Key APIs | | :-------------- | :----------------------------------------------------------- | :------------------------------------------------------------------------------------------- | | **Onboarding** | Structure and configuration of the financial ecosystem. | Organizations, Ledgers, Assets, Account Types, Accounts, Portfolios, Segments | | **Transaction** | Execution, movement, and accounting of financial operations. | Balances, Accounting Routes (`transactionRoute`), Transactions, Operation Routes, Operations | With this architecture, financial institutions structure their data with precision and operate at scale. You keep full control and transparency over your financial ecosystem. # Account types Source: https://docs.lerian.studio/en/midaz/account-types Register Account Types to enforce classification per Ledger, validate the type field on account creation, and align account behavior with your accounting rules. Account Types classify the accounts in your ledger. Each [**Account Type**](/en/reference/midaz/create-an-account-type) has a name, a description, and a unique key value. You register the Account Types that match your accounting structure. When you enable Account Type validation, the ledger checks the `type` field of every new non-external Account against these registered types. ## How Account Type validation works ### Enabling Account Type validation You enable Account Type validation per ledger through the [Ledger Settings API](/en/midaz/ledgers#ledger-settings). Send a `PATCH` request to the Ledger Settings endpoint: ```json PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings theme={null} { "accounting": { "validateAccountType": true } } ``` Settings changes take effect immediately — no redeployment required. You can update them at any time through the API. ### Behavior of the `type` field in Accounts API When you create an Account, the `type` field behavior depends on the validation setting: * **Validation disabled (default):** You must provide a `type`. It accepts any string value up to 256 characters. The ledger does not compare it to registered Account Types. * **Validation enabled:** The `type` field must match the `keyValue` of a registered Account Type. Midaz stores new `keyValue` values in lowercase and compares the value case-insensitively. Use lowercase values in requests and configuration. If it does not match, the ledger rejects the account with a validation error. External accounts skip this check. Before you enable Account Type validation, review your existing accounts. The `type` field of an existing Account cannot be changed, so create a matching registered Account Type or recreate an Account whose `type` must change. ### The `keyValue` field The `keyValue` field identifies an Account Type. It has these constraints: * **Immutable:** You set `keyValue` when you create the Account Type. You cannot change it afterward. The Update Account Type endpoint does not accept this field. * **Unique per ledger:** Each `keyValue` must be unique within a ledger. * **Used for validation:** When you enable `validateAccountType` in Ledger Settings, the ledger compares the `type` field of each non-external Account case-insensitively with a registered `keyValue`. The ledger rejects any non-external Account with an unregistered `type`. ## Managing Account Types *** You manage Account Types through the API or the Lerian Console. * [Create an Account Type](/en/reference/midaz/create-an-account-type) — Register a new Account Type for your Ledger. * [List Account Types](/en/reference/midaz/list-account-types) — Retrieve all Account Types configured in your Ledger. * [Retrieve an Account Type](/en/reference/midaz/retrieve-an-account-type) — Get detailed information about a specific Account Type. * [Update an Account Type](/en/reference/midaz/update-an-account-type) — Modify the definition of an existing Account Type. * [Delete an Account Type](/en/reference/midaz/delete-an-account-type) — Remove an Account Type that is no longer in use. **See also** * [Transaction Routing](/en/midaz/transaction-routing-entities) — Operation Routes use `ruleType: account_type` to validate an account's `type` against the route's configured `account.validIf` values. * [Accounting Entries](/en/midaz/accounting-entries) — How Midaz resolves debit and credit rubrics for the accounts these types classify. # Accounting Entries Source: https://docs.lerian.studio/en/midaz/accounting-entries Map transaction actions and route directions to accounting classifications, and annotate every operation with its accounting code and description. Accounting Entries (also known as **Rubricas**) map a transaction action and route direction to an accounting `code` and `description`. They annotate an operation after route resolution; Operation Route rules and transaction legs determine the participating accounts. ## What are Accounting Entries *** A **rubric** maps a transaction action and route direction to an accounting classification. Instead of computing each classification by hand, you register rubrics once. Midaz then resolves them automatically as it processes transactions. Each rubric carries: * **`code`** — an accounting code (for example, `1.1.1.001`). * **`description`** — a human-readable label for the entry (e.g., `Customer checking — outbound`). * A set of **action mappings** — one entry per action type, each with its own debit and/or credit rubric. When the engine processes a transaction with route validation (`accounting.validateRoutes`) enabled and a matching rubric registered, it resolves the rubric for each operation. It records the resulting **`routeCode`** and **`routeDescription`** on the operation. This gives you a complete audit trail from transaction to operation to rubric. Your teams can trace exactly which accounting rule applied to each movement. Configure rubrics per action on each Operation Route. For `direct` and `commit` actions, **Source** routes require the **debit** rubric and **Destination** routes require the **credit** rubric. Dedicated `block` and `unblock` rubrics are optional; when absent, Midaz resolves the `direct` rubric for those actions. Source-side `hold` and `cancel` actions require **both** rubrics, and `overdraft` requires **both** on every supported route type. **Bidirectional** routes always require **both**. ## The 8 action types *** Each action represents a distinct transactional event. The first five actions cover the transaction lifecycle. The last three cover overdraft, block, and unblock movements. A single rubric can map different debit and credit classifications for each action. Every stage of an operation then receives the right accounting annotation. | Action | Identifier | Description | | :------------ | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Direct** | `direct` | Immediate, single-step debit/credit between two accounts, with no intermediate stages (e.g., a fee or adjustment). | | **Hold** | `hold` | Reserves funds by creating a pending movement (moves value from `available` to `on_hold` on the source account). | | **Commit** | `commit` | Confirms a previously held amount, releasing the `on_hold` value to the destination account. | | **Cancel** | `cancel` | Cancels/reverses a hold, returning the `on_hold` value to the `available` balance on the source account. | | **Revert** | `revert` | Reverses a completed `direct` transaction by creating a counter-transaction that undoes the original. | | **Overdraft** | `overdraft` | Classifies overdraft movements — the **debit** rubric marks overdraft usage (the deficit grows) and the **credit** rubric marks repayment (the deficit shrinks). Both rubrics are required when this entry is configured. | | **Block** | `block` | Optionally classifies a fund-block movement that freezes value on an account (for example, an `asset-freeze`). | | **Unblock** | `unblock` | Optionally classifies the release of previously blocked funds back to the `available` balance. | **Overdraft** classifies companion operations that the engine generates automatically during overdraft usage and repayment. For every supported route type and direction, configure both the debit and credit rubrics. **Block** and **unblock** can use dedicated rubrics for operations that the block and unblock transaction endpoints produce; without them, those actions use the `direct` rubric. When needed, register dedicated rubrics for these actions in the same way as for the other actions. Each action can point to different debit and credit accounting classifications within the same rubric. Map only the actions a route uses. If you enable strict validation (below), cover every action your transactions emit. ## Configuring Accounting Entries *** You register rubrics through the API as part of your Operation Routes. The `accountingEntries` block on a route defines one entry per action. Each entry carries its `debit` and/or `credit` rubric: ```json accountingEntries theme={null} { "accountingEntries": { "direct": { "debit": { "code": "1.1.1.001", "description": "Customer checking — outbound" }, "credit": { "code": "1.1.1.002", "description": "Customer checking — inbound" } }, "hold": { "debit": { "code": "1.1.1.001", "description": "Customer checking — reserve" }, "credit": { "code": "2.1.1.001", "description": "Pending settlement — hold" } } } } ``` You manage these entries through the Operation Route endpoints — see [Create an Operation Route](/en/reference/midaz/create-an-operation-route) and [Update an Operation Route](/en/reference/midaz/update-an-operation-route). For the full configuration flow, refer to [Transaction Routing](/en/midaz/transaction-routing-entities#4-configure-accounting-entries-actions). ## Validation modes *** Midaz reacts to a missing rubric based on the Ledger's accounting settings. Two distinct gates control this behavior: ### Default (graceful) By default (`accounting.validateRoutes` disabled), Midaz does not resolve rubrics at all: the transaction proceeds normally and the `routeCode` and `routeDescription` fields stay empty (nil) for every operation. It raises no error. ### Strict (opt-in) Set `accounting.validateRoutes` to `true` in the [Ledger Settings](/en/midaz/ledgers#ledger-settings) to enforce route validation. In strict mode, a requested action with no routes in the transaction-route cache returns `0157 ErrNoRoutesForAction`. `0117 ErrAccountingRouteNotFound` applies when an operation route ID is absent from that cache. ```json PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings theme={null} { "accounting": { "validateRoutes": true } } ``` In strict mode, do not treat `0117 ErrAccountingRouteNotFound` as the error for every unmapped action: it applies when an operation route ID is absent from the transaction-route cache. A requested action with no routes in that cache returns `0157 ErrNoRoutesForAction`. Use **strict mode** in production ledgers where every transaction type needs an accounting classification. The graceful default helps while you onboard routes. In production, it can silently leave movements without a classification. When Midaz finds a matching rubric, it annotates the operation with two fields: * **routeCode** — the `code` of the resolved `AccountingRubric` for that action and direction. * **routeDescription** — the description of the resolved rubric, populated alongside `routeCode`. # Accounting Source: https://docs.lerian.studio/en/midaz/accounting-in-midaz Get an overview of accounting in Midaz: Chart of Accounts, Account Types, Accounting Entries, and Operations that keep every posting balanced and traceable. Midaz records accounting for real-time financial systems. It uses strict double-entry bookkeeping. It supports digital products, embedded finance, and high-volume operations. Midaz is ledger-centric: it enforces every balance, movement, and posting at the ledger level. You get traceability, auditability, and consistency by default. This page is the **entry point for the Accounting section**. It introduces the core primitives and shows how they fit together. It also points you to the reference pages and to a hands-on walkthrough of accounting end to end. ## How the primitives relate *** Midaz turns business events into balanced ledger postings. A small set of primitives builds on each other: #### Chart of Accounts This is the structure of balances your product needs. Midaz has no dedicated Chart of Accounts API. Instead, you model it on a ledger from assets, accounts, segments, portfolios, and account types. The `code` field on an Accounting Entry (for example `1.1.1.001`) carries the traditional account number for each posting. #### Account Types These are reusable classifications that you assign to accounts. Each account type has a name, a key value, and a description. You use account types to group accounts in your financial structure. #### Accounting Entries (Rubricas) A route does not need an entry for every transaction action. When it emits an action, the entry must satisfy the route-specific debit and credit requirements. Each configured entry contains the debit and/or credit rubric required by its operation route; every present rubric has a `code` and description. With route validation (`accounting.validateRoutes`) enabled, Midaz stamps the matching rubric onto each operation, based on the operation's action and direction. #### Operations A transaction produces ledger movements. An **Operation** is a single leg of a transaction — one debit or one credit on a specific account. Midaz validates that the original transaction’s source, destination, and send totals agree. Its returned operation set is not always a debit/credit pair: pending creation and cancellation process only source-side legs. Each operation carries a `transactionId`, an `amount`, and an `assetCode`. When route validation (`accounting.validateRoutes`) is enabled on the ledger and the operation route defines accounting entries, the operation also carries a `routeCode` and a `routeDescription` from the resolved rubric. With `accounting.validateRoutes` enabled, when an operation route defines an accounting entry, Midaz resolves a rubric for the operation's action and direction. It writes the rubric `code` to the operation's `routeCode` and links the operation with a `transactionId`. Together, these fields give a complete audit trail: transaction → operation → rubric. Your teams trace which accounting rule applied to each movement, without extra lookups. At a glance, the flow is: Diagram showing the Accounting primitives workflow Midaz validates that the original transaction’s source, destination, and send totals agree. Lifecycle responses do not always contain both directions: pending creation and cancellation process only source-side legs. Each movement stays traceable for audit and compliance. New to accounting in Midaz? Start with the **[Accounting Walkthrough](/en/midaz/accounting-walkthrough)**. It gives a step-by-step path from the chart of accounts to a working Pix payment example. # Accounting walkthrough Source: https://docs.lerian.studio/en/midaz/accounting-walkthrough An end-to-end, hands-on guide to designing and implementing accounting in Midaz — from chart of accounts to a working Pix payment example. This guide shows you how to implement accounting in Midaz from start to finish. It assumes you are a developer. You want enough accounting context to model a real product, not a full accounting textbook. By the end, you understand how the primitives fit together. You can also wire up a complete Pix payment with correct double-entry postings. For the conceptual overview and links to each reference page, see **[Accounting](/en/midaz/accounting-in-midaz)**. ## 1. Double-entry accounting fundamentals *** Midaz uses strict double-entry bookkeeping. The rules are simple but non-negotiable: * Every transaction **must** have at least one debit and one credit. * Total debits **must equal** total credits. * Every movement impacts the ledger in a balanced way. This guarantees no drift in balances, precise audit trails, and regulation-ready financial statements. In practice, you rarely do double-entry by hand in Midaz. You model your accounts and routing once. The engine enforces balance on every transaction. Think in terms of *where value comes from* (the debit side / source) and *where it goes* (the credit side / destination). Every Midaz operation lands on one side of that equation. ## 2. Chart of Accounts in Midaz *** In traditional accounting, the Chart of Accounts (CoA) defines account categories (Assets, Liabilities, Equity, Income, Expenses), their hierarchy, and how to classify movements. Midaz does not have a dedicated Chart of Accounts API. The CoA is not a resource you create or retrieve. It is the result of how you combine assets, accounts, segments, portfolios, and account types. The `code` field on Accounting Entries (e.g. `1.1.1.001`) is where traditional account numbering appears in practice. Each `code` annotates a posting with its classification. Midaz lets you mirror or adapt a CoA digitally using a small set of primitives: * **Assets** define *what* moves — currencies (BRL, USD), points/miles, crypto tokens, or internal units of value — with decimal precision and regulatory metadata. * **Accounts** are balance containers. Each has an asset code and type; it can also belong to a portfolio and segment. An **alias** (e.g. `@external/BRL`) identifies each account and keeps routing intuitive. * **Segments** categorize and isolate accounts (customer vs. internal funds, business units, multi-tenant separation). * **Portfolios** group accounts that share a purpose or belong to the same entity. To map a CoA in Midaz, you decide which balances you need and classify them with Account Types. You then organize them with segments and portfolios on a ledger, and assign `code` values on your Accounting Entries to match your numbering scheme. ### A recommended process List the balances you need: customer balances, internal accounts, reserve/settlement accounts, fee and revenue accounts. This is your CoA blueprint. Create an Account Type per conceptual category — e.g. `CASH`, `SETTLEMENT`, `FEE_REVENUE`, `FEE_EXPENSE`, `TREASURY`. Segments separate business domains (`CUSTOMER_FUNDS`). Portfolios manage ownership and grouping (`customer_12345_wallet`). For each logical balance, create a ledger account (customer BRL account, treasury account, provider fee expense account, merchant settlement account). ## 3. Setting up Account Types *** Account Types classify accounts by nature and purpose. With `validateAccountType` enabled, a new non-external Account's `type` must match a registered `keyValue`; an Operation Route can separately use `ruleType: account_type` to validate an account during route processing. Account Types do not themselves define permitted operations, internal or external status, or reconciliation rules. A typical setup for a payments product: * **CASH** → liquid customer funds * **SETTLEMENT** → funds awaiting clearing * **FEE\_REVENUE** → fees collected * **FEE\_EXPENSE** → provider fees * **TREASURY** → internal operations To enable Account Type validation, understand the `type` field, and manage Account Types via the API, see **[Account Types](/en/midaz/account-types)**. ### Understanding balance buckets Before you wire up two-phase flows, understand that each balance tracks funds in distinct fields: * **`available`** — funds you can spend or send right now. Debits and credits to a balance move this number. * **`onHold`** — funds that a pending `hold` reserves and does not yet commit. Midaz moves them out of `available`, but they still belong to the account until you commit or cancel the hold. * **`overdraftUsed`** — overdraft consumed by the balance, when overdraft is enabled. The three fields carry decimal strings with exact precision (for example, `"12.50"`). There is no separate `scale` field to interpret. See [Transaction amount](/en/midaz/amount) for the decimal value model. The two-phase actions move value between `available` and `onHold` on the source balance: | Action | `available` | `onHold` | | ---------- | ------------------------------------------------ | ------------------------------ | | **Direct** | Debited (source) / credited (destination) | unchanged | | **Hold** | ↓ decreased on source | ↑ increased on source | | **Commit** | credited on destination | ↓ released from source | | **Cancel** | ↑ returned to source | ↓ released back to `available` | | **Revert** | restored on both sides via a counter-transaction | unchanged | For the full balance model — multiple balances per account, permission flags, overdraft, and history — see **[Balances](/en/midaz/balances)**. ## 4. Defining Accounting Entries (Rubricas) *** **Accounting Entries** (Rubricas) map a transaction action and route direction to an accounting `code` and `description`. You register rubrics once instead of computing accounting classifications by hand for each movement. When `accounting.validateRoutes` is enabled on the ledger and a matching rubric is configured, Midaz annotates each operation with the resulting `routeCode` and `routeDescription`; Operation Route rules and transaction legs determine the participating accounts. You configure rubrics **per action** on each Operation Route, inside the `accountingEntries` block. For `direct` and `commit` actions, source routes require the **debit** rubric and destination routes require the **credit** rubric. Dedicated `block` and `unblock` rubrics are optional; when they are not configured, Midaz resolves the `direct` rubric for those actions. Source-side `hold` and `cancel` actions require **both** rubrics, `overdraft` requires **both** on every supported route type, and bidirectional routes always require **both**. ### The five transaction-lifecycle actions | Action | Code | What it does | | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Direct** | `direct` | Immediate, single-step posting with one debit and one or more credits, or one credit and one or more debits; no intermediate stages (e.g. a fee or adjustment). | | **Hold** | `hold` | Reserves funds by creating a pending movement (`available` → `onHold` on the source). | | **Commit** | `commit` | Confirms a previously held amount, releasing `onHold` to the destination. | | **Cancel** | `cancel` | Cancels a hold, returning `onHold` value to `available` on the source. | | **Revert** | `revert` | Reverses an `APPROVED` transaction via a counter-transaction when its Operation Routes are bidirectional. | Each action can point to different debit/credit accounting classifications within the same rubric. Every stage of an operation then receives the right accounting annotation. You register these mappings through the Operation Route endpoints — see [Create an Operation Route](/en/reference/midaz/create-an-operation-route). ```json theme={null} { "accountingEntries": { "direct": { "debit": { "code": "1.1.1.001", "description": "Customer cash-out" }, "credit": { "code": "2.1.1.001", "description": "External settlement" } } } } ``` For the full model, see **[Accounting Entries (Rubricas)](/en/midaz/accounting-entries)**. ## 5. Transaction routing *** Routing is a two-layer system that resolves at runtime: * **Operation Routes** define the accounting logic for each leg of a transaction: which accounts to debit or credit, balance keys, and validation rules. They carry the **Accounting Entries (rubricas)** above. * **Transaction Routes** define the business event that triggers accounting (`PIX_CASH_OUT`, `WALLET_TRANSFER`, `BANK_SLIP_SETTLEMENT`, …) and combine Operation Routes into a balanced financial event. When you submit a transaction, Midaz resolves the matching Transaction Route. It then resolves each Operation Route and its rubric for the current action. Before it records anything, Midaz runs four checks. It confirms that balances exist, that debits do not exceed the available balance, that assets match, and that the ledger stays balanced. For route structure, fields, the operation-type validation matrix, and API behavior, see **[Transaction Routing](/en/midaz/transaction-routing-entities)**. ## 6. End-to-end example — a Pix payment *** Let's tie it together with a simple Pix cash-out: a customer sends BRL out of their wallet to an external account. Create the customer account on your ledger. Midaz creates `@external/BRL` automatically together with the `BRL` Asset; do not create it yourself. * `customer_12345_brl` — Account Type `CASH`, asset `BRL` * `@external/BRL` — the automatically created external settlement account for funds leaving the ledger Enable `accounting.validateRoutes` on the ledger. Then, on the Operation Route for the customer leg (source), register the `direct` debit rubric. On the external leg (destination), register the `direct` credit rubric: ```json theme={null} { "accountingEntries": { "direct": { "debit": { "code": "1.1.1.001", "description": "Pix cash-out — customer" }, "credit": { "code": "2.1.1.001", "description": "Pix cash-out — external settlement" } } } } ``` This block is a combined illustration of both rubrics. Register only the `debit` field on the Source route and only the `credit` field on the Destination route. Submit a transaction against the `PIX_CASH_OUT` Transaction Route. Move, say, `100.00 BRL` from `customer_12345_brl` to `@external/BRL`. Midaz records two balanced operations: * **Debit** `customer_12345_brl` `100.00 BRL`, `routeCode: 1.1.1.001` * **Credit** `@external/BRL` `100.00 BRL`, `routeCode: 2.1.1.001` Both share the same `transactionId`, giving you a full trail from transaction → operation → rubric. For a two-phase flow (hold → commit/cancel), register the `hold`, `commit`, and `cancel` rubrics on the route. Submit the corresponding actions. Each stage resolves its own rubric. ## 7. Validation modes *** Midaz controls route validation with the `accounting.validateRoutes` setting on each ledger. The default is `false`. In this graceful mode, Midaz skips route validation. It leaves the `routeCode` and `routeDescription` fields empty on every operation and raises no error. Graceful mode is convenient while you onboard routes. In production, set `accounting.validateRoutes` to `true` in the [Ledger Settings](/en/midaz/ledgers#ledger-settings). Strict mode then validates the routes on every transaction: ```json theme={null} { "accounting": { "validateRoutes": true } } ``` In strict mode, a requested action with no routes in the transaction-route cache returns `0157 ErrNoRoutesForAction`. `0117 ErrAccountingRouteNotFound` applies when an operation route ID is absent from that cache. When a route resolves, Midaz stamps `routeCode` and `routeDescription` from its rubric. Use **strict mode** (`validateRoutes: true`) in production ledgers where every transaction type needs an accounting classification. Keep the graceful default only while you onboard routes. ## Next steps *** * Review the **[Accounting](/en/midaz/accounting-in-midaz)** overview and reference pages for full field-level detail. * Once your ledger produces structured postings, see **[Lerian Reporter](/en/reporter/what-is-reporter)** to transform ledger events into reconciliation files, financial statements, and COSIF-aligned regulatory outputs. # Accounts Source: https://docs.lerian.studio/en/midaz/accounts Use Accounts as the core financial unit of a Midaz Ledger, tied to a single Asset and recording every debit, credit, and balance for a customer or product. An Account is the core financial unit of a Midaz Ledger. Each Account links to one Asset and records every debit, credit, and balance for that Asset. In banking terms, an Account is a financial product, such as a checking account, a savings account, or a loan account. Midaz does not limit how many accounts you create. Create as many as your structure needs. ## Account structure *** * **Account > Ledger**: You create an Account within a Ledger. The Ledger tracks and consolidates all balances and operations. * **Account > Portfolio**: You can group Accounts into [**Portfolios**](/en/midaz/portfolios) to represent customer clusters, product lines, or business units. * **Account > Asset**: Each Account links to a **single Asset**. The Asset defines the type of value the Account holds, such as BRL, USD, BTC, or loyalty points. * **Account > Account Type**: When you enable Account Type validation, each non-external Account must use a registered Account Type. You register Account Types for your business classification. ## Key characteristics *** * Each Account links to exactly one Asset type. * Each Account has a unique identifier within a Ledger. * Every transaction records debits and credits between Accounts. ## Multiple accounts per customer *** A single customer often holds more than one balance. Midaz models each balance as its own Account, not as labels on a shared one. The guiding rule: **create a separate account whenever a balance needs its own truth.** The same customer might hold balances that behave differently: * **Different nature** — a main balance, a benefit balance, or a promotional balance. * **Different operational rules** — a court-ordered or blocked account that accepts inflows but restricts outflows. * **Separate statement and reconciliation** — a product sub-account or pocket that you track on its own. When the balance, ledger, statement, or rule differs, each one becomes its own Account. You classify it by an [Account Type](/en/midaz/account-types), group it under the customer with a [Portfolio](/en/midaz/portfolios), and tie it to identity through [CRM](/en/midaz/crm/crm-overview). One account with labels works until the balances diverge. Separate accounts keep each balance accurate from the start. For the full reference architecture — one customer, many accounts, with step-by-step examples — see [Midaz for multi-account customers](/en/midaz/midaz-for-multi-account-customers). ## External Account *** External Accounts in Midaz represent accounts outside your organization's structure. They track money that enters or leaves your ledger, usually to and from users, partners, or financial providers. External accounts have these characteristics: * **Hold the counterparty balance** for money that enters or leaves your ledger. * **Can represent an external negative position.** When an external account uses overdraft, its derived position can be negative; the persisted `Available` balance stays at zero and Midaz tracks usage in `OverdraftUsed`. * **The Ledger creates a canonical external Account automatically** when you create an Asset. * **The canonical external Account follows a clear naming pattern**: `@external/`, such as `@external/BRL`. In practice, these accounts act as the bridge between your system and the outside world. They record inflows and outflows at the ledger boundary. **Do not try to delete or change an external account.** Midaz blocks these operations to keep the Ledger accurate and traceable. ### External account codes The canonical external Account created with an Asset follows the naming pattern `@external/`. The asset code in that alias acts as the lookup key. You can retrieve this canonical Account and its balances with convenience endpoints that accept only the asset code: * `GET .../accounts/external/{code}` — Retrieve the external account for an asset code (for example, `BRL` resolves to `@external/BRL`). * `GET .../accounts/external/{code}/balances` — Retrieve the balances for that external account. These endpoints are shortcuts for the canonical external Account. They prepend `@external/` to the code you provide, then perform an alias-based lookup. The result is identical to a query by that full alias. ### Entity ID (external system reference) The `entityId` field exists on any account, not only external accounts. It links the account to a record in an external system, such as a core banking platform, a CRM, or a partner system. * **Not the same as alias**: You use the alias in transactions, and it must be unique within a ledger. The `entityId` is only a reference for your integration, and Midaz does not use it to move value. * **Optional**: Set it when you create the account or when you update it. The maximum length is 256 characters. * **Use case**: When your system already has an account identifier, such as `EXT-ACC-12345`, store it in `entityId`. You can then map between Midaz and your source of truth. ```json JSON theme={null} { "name": "User Checking Account", "assetCode": "BRL", "alias": "@user/checking_123", "entityId": "EXT-ACC-12345", "type": "checking" } ``` ## Parent Account ID *** The **Parent Account ID** links two accounts within Midaz. You define the relationship based on your business logic. You can use it for a traditional parent-child structure or for another relationship that your business needs. ## Account aliases *** An alias replaces a complex account ID with a readable label. This makes accounts easier to identify. * **For example**: Instead of the ID `3172933b-50d2-4b17-96aa-9b378d6a6eac`, you can use `@username_1`. ### Use the Account Alias in Transactions When you create a transaction, always use the **account alias** in the `account` field. Do not use the account ID. An alias is **optional** when you create a non-external Account. If you skip it, Midaz uses the account ID as the alias. A user-created external Account requires an alias. Every account then has a unique alias. ## Managing Accounts *** You can manage your Accounts through the API or the Lerian Console. ### Via API * [Create an Account](/en/reference/midaz/create-an-account) — Open a new Account linked to an Asset. * [List Accounts](/en/reference/midaz/list-accounts) — View all Accounts in your workspace. * [Retrieve an Account](/en/reference/midaz/retrieve-an-account) — Get details of a specific Account. * [Retrieve an Account by Alias](/en/reference/midaz/retrieve-an-account-by-alias) — Get details of a specific Account by its alias. * [Retrieve an External Account](/en/reference/midaz/retrieve-an-external-account) — Get details of a specific External Account by its asset code. * [Update an Account](/en/reference/midaz/update-an-account) — Edit the metadata or settings of an existing Account. * [Delete an Account](/en/reference/midaz/delete-an-account) — Delete a specific Account. **Transfer any remaining balance to another account before you delete an account.** Midaz does not delete an account or sub-account that still holds a balance. ### Via Lerian Console You can view, create, edit, and delete Accounts on the Accounts page. This page is in the Midaz module of the Lerian Console. [**Learn more in the Managing Accounts guide.**](/en/midaz/console/managing-accounts) # Accounts Source: https://docs.lerian.studio/en/midaz/accounts-overview Learn how to create, group, and classify Midaz Accounts using Portfolios and Segments to match your customers, product lines, and business unit structure. Accounts are the core financial units in Midaz. This section covers how to create, organize, and classify Accounts. You group them to match your business structure, from individual balances to customer-level groupings. ## Content *** Here, you'll find: * [**Accounts**](/en/midaz/accounts): The core financial unit in a Ledger. Each Account uses one Asset and records its debits, credits, and balances. Midaz also supports External Accounts to track value that moves in or out of your organization. * [**Portfolios**](/en/midaz/portfolios): Groups of Accounts that belong to one entity, such as a customer or business unit. A Portfolio works like a wallet that holds that entity's Accounts across different Assets. * [**Segments**](/en/midaz/segments): A classification for Accounts that share characteristics. You group Accounts into Segments to organize them by tier or type, such as premium card tiers or student accounts. # Transaction amount Source: https://docs.lerian.studio/en/midaz/amount Use the transaction field that belongs to the endpoint version. Midaz does not use one global amount field across every transaction API. Amount fields apply to transaction-creation bodies, not every transaction endpoint. v1 creation payloads use decimal `value`; the API accepts a JSON number or string, although a string avoids client-side IEEE-754 rounding. v2 `direct`, `hold`, `block`, and `unblock` creation endpoints require string `amount`. Commit, cancel, and revert are ID-only lifecycle endpoints and have no amount request body. Do not combine field conventions from different transaction versions. Use the API reference for the specific operation you call. # Architecture Source: https://docs.lerian.studio/en/midaz/architecture Explore Midaz's modular architecture, centered on the Ledger service and its supporting infrastructure. **In a nutshell**: Midaz runs the modular Ledger service and a separate Tracer service. Deploy and scale each service and its supporting infrastructure for your environment. For a business-oriented overview, see [About Midaz](/en/midaz/about-midaz). Midaz can run in public or private cloud environments. Select and operate the infrastructure that fits your deployment requirements. Under the hood, Midaz uses a **modular architecture**. Ledger is organized around domain boundaries, with supporting infrastructure for storage, caching, messaging, and streaming; Tracer runs as a separate service. ## Modular architecture *** The diagram below (*Figure 1*) shows how the pieces fit together. ### 1. Cluster layer (customer management) The customer manages this layer. It provides the Kubernetes foundation that runs Midaz. Enable these services for a smooth and secure deployment: * **Service**: Networking abstraction to expose and route services. * **AutoScaler**: Automatically adjusts the number of pods. * **HPA (Horizontal Pod Autoscaler)**: Scales applications based on metrics. * **Secrets**: Secure management of sensitive data. * **ConfigMap**: Externalized configuration management. This layer governs and coordinates all components within the cluster. ### 2. Front-end layer The front-end layer provides the user interface for system interaction (admin panel, dashboards, and more). **Important transition**: newer releases of Midaz no longer include a bundled console in the repository. **[Lerian Console](/en/platform/console/about-lerian-console)** now provides the visual interface. It is a unified platform that includes Midaz as one product module. If you use an older version of Midaz, you may still have the standalone **Midaz Console**. This component is now in maintenance mode. It receives critical security patches only. Migrate to Lerian Console for the latest features and improvements. ### 3. Back-end layer The Ledger back-end is a **modular monolith** that holds the core responsibilities of the Midaz ledger. Midaz also runs Tracer as a separate service. Ledger brings together four core components: * **Onboarding domain**: manages organizations, ledgers, assets, portfolios, segments, and accounts. * **Transactions domain**: handles financial transactions, operations, and asset movements. * **CRM**: is embedded in the Ledger process. * **Fees**: is embedded in the Ledger process. This approach keeps clear domain boundaries. It also simplifies deployment and lowers operational complexity. ### 4. Data layer This layer holds the databases and infrastructure components that support application data and event flows: * **PostgreSQL**: separate `onboarding` and `transaction` databases, each configured with primary and replica connections. * **Valkey**: A Redis-compatible key-value store for caching or fast data retrieval. * **MongoDB**: Stores unstructured and semi-structured data, such as entity metadata. * **RabbitMQ**: Message broker for asynchronous communication between services. * **Redpanda**: Kafka-compatible broker for Ledger event streams. Ledger publishes configured events to it only when `STREAMING_ENABLED=true` and streaming brokers are configured. Persistence is deployment-specific. The bundled Compose configuration does not mount a data volume for Redpanda; configure production storage and retention for each service. ### 5. Helm Chart layer Midaz uses **Helm Charts** (specifically `lerianstudio/helm`) to deploy and configure the infrastructure. The chart connects the code to Kubernetes. It deploys each component the same way across environments. For more details, see the [Deploying using Helm](/en/platform/helm/midaz/midaz-installation) page. ## How the layers work together *** Each layer has a focused role, with no unnecessary complexity. The system scales and adapts as your needs change. * Kubernetes coordinates how the layers communicate, for secure and reliable interactions across the system. * Helm keeps deployments and configurations in sync, so scaling and updates stay straightforward. Because the platform is modular, extending it is simple. You can add new components as needed without disruption to what already runs. # Assets Source: https://docs.lerian.studio/en/midaz/assets Define the currencies, cryptocurrencies, and tokenized instruments your Accounts hold — multi-asset support across a single Ledger. Assets represent the financial instruments or currencies that Accounts hold. Each Account holds exactly one Asset. Assets can include: * Currencies (e.g., BRL, USD, EUR) * Cryptocurrencies (e.g., BTC, ETH) * Non-currency assets (e.g., gold, loyalty points, tokenized securities) Midaz keeps each Asset independent within the Ledger. Balances stay clearly defined. ### Multi-Asset Finance today spans more than one currency or asset class. Midaz supports traditional currencies, cryptocurrencies, commodities, and tokenized assets in one system. Use it for multi-currency wallets, cross-border payments, or digital asset portfolios. ## Asset structure *** * **Asset > Account**: Each Account holds a single Asset. * **Asset > Ledger > Account**: You define Assets at the Ledger level. You create Accounts from the available Assets. ## Key characteristics *** * You must define each Asset before you use it. * Each Asset has a unique identifier, such as a currency code. * Midaz stores asset rates, but the Ledger transaction flow does not use them to convert values. The source and destination assets of a transaction must match. When you create an Asset, Midaz automatically creates an [External Account](/en/midaz/accounts) if one does not exist. The External Account backs every transaction that enters or leaves the Ledger. The External Account uses the naming convention `@external/`, for example, `@external/BRL`. ## Managing Assets *** You can manage Assets through the API or the Lerian Console. ### Via API * [List Assets](/en/reference/midaz/list-assets) — View all Assets in your workspace. * [Retrieve an Asset](/en/reference/midaz/retrieve-an-asset) — Get details of a specific Asset. * [Create an Asset](/en/reference/midaz/create-an-asset) — Register a new Asset in the Ledger. * [Update an Asset](/en/reference/midaz/update-an-asset) — Edit the properties of an existing Asset. * [Delete an Asset](/en/reference/midaz/delete-an-asset) — Mark an Asset as deleted so it can no longer be used. **Important** Do not use an External Account as an asset-retirement destination: deleting an Asset soft-deletes its External Account(s) and does not migrate or clear balances. Midaz currently has no balance-migration workflow in asset deletion; define and execute an approved retirement or settlement process before deletion. ### Via Lerian Console You can view, create, edit, and delete Assets on the Assets page in the Midaz module of the Lerian Console. [**Learn more in the Managing Assets guide.**](/en/midaz/console/managing-assets) # Async transaction processing Source: https://docs.lerian.studio/en/midaz/async-transaction-processing Async transaction processing validates the call fast, then persists the write through RabbitMQ, for lower API latency and higher throughput. ## Why this matters *** A client sends a transaction. Midaz must do two things: validate it and persist the result. In synchronous mode, both steps run in the same request. The client waits for every write to reach the database before it gets a response. This model is simple and predictable, but it has a ceiling. At high volume, database writes become the bottleneck. Each transaction holds a connection, waits for locks, and competes for I/O. Async mode breaks that dependency. Midaz validates the transaction, returns the response at once, and persists the data in the background through RabbitMQ. The client gets faster responses. With async processing enabled, the Bulk Recorder batches inserts by default; set `BULK_RECORDER_ENABLED=false` to persist queued messages individually. For broader scaling guidance, see [Scalability strategies](/en/midaz/scalability-strategies). ## How it works *** ### Synchronous mode (default) Midaz validates the transaction and writes it directly to PostgreSQL in the same request cycle. The API sends its response only after every database operation completes. Sequence diagram showing the client sending a POST /transaction to the Midaz API, which validates it, writes the transaction and operations to PostgreSQL, waits for confirmation, and only then returns 201 Created to the client. The 201 Created response carries the transient CREATED status, not final approval. Here's the full flow, step by step: 1. **The client sends** a `POST /transaction` to the Midaz API. 2. **The API validates the request** — it runs request-shape validation, balance checks, and limit enforcement here. 3. **The API writes to PostgreSQL** — it persists the transaction and its operations in the same request cycle. 4. **PostgreSQL confirms the write** — it commits all records. 5. **The API returns `201 Created`** to the client with the created transaction. The response leaves the server only after the database confirms everything. The response carries the transient `CREATED` status; Midaz promotes the transaction to `APPROVED` asynchronously after balance processing. Do not treat the `201` as final approval; wait for the status to reach `APPROVED` before you treat the transaction as settled. **Characteristics:** * Response time includes database write latency. * Each transaction is an independent database operation. * Simpler to reason about. The response shows exactly what Midaz persists. Even in synchronous mode, Midaz updates balances atomically in Redis during the request. Redis is authoritative for balances. The write above persists the transaction and its operations, not the Postgres balance rows. The always-on balance-sync worker reconciles those rows (see [Balance synchronization](#balance-synchronization)). ### Asynchronous mode Midaz validates the transaction the same way. Instead of a direct database write, Midaz publishes a message to RabbitMQ. A background consumer picks up the message and handles persistence separately. 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. The 201 Created response carries the transient CREATED status, not final approval. 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. Here's the full flow, step by step: 1. **The client sends** a `POST /transaction` to the Midaz API. 2. **The API validates the request** — it runs request-shape validation, balance checks, and limit enforcement exactly as in synchronous mode. 3. **The API publishes the transaction payload to RabbitMQ** instead of a direct database write. 4. **The API returns `201 Created`** to the client as soon as the queue accepts the message — the client does not wait for database persistence. The response carries the transient `CREATED` status; Midaz promotes the transaction to `APPROVED` asynchronously after balance processing. Do not treat the `201` as final approval; wait for the status to reach `APPROVED` before you treat the transaction as settled. 5. **RabbitMQ delivers the message** to a background consumer, decoupled from the API request. 6. **The consumer writes to PostgreSQL** — it persists the transaction and its operations from the queued message. The balance-sync worker coordinates balance updates and keeps balances consistent in both modes (see the **Balance synchronization** section). **Characteristics:** * Response time excludes database write latency — the client waits only for validation and queue publish. * Midaz serializes messages with MessagePack for compact, efficient transport. * Background consumers write to the database at their own pace, with retries; batch inserts require an enabled Bulk Recorder. The validation step is identical in both modes. Balance checks, request-shape validation, and limit enforcement — all of that happens before the API responds, regardless of processing mode. The difference is only in *when* the data hits the database. ## Built-in resilience *** If RabbitMQ is unavailable when async mode tries to publish a message, Midaz attempts a direct database write. If that write fails, Midaz returns the database error. This means: * During a queue outage, Midaz attempts to write directly to the database. * The client can receive an error if the fallback database write fails. * Midaz logs the queue failure and, if the direct write also fails, the fallback-write failure so your operations team can investigate them. During a queue outage, latency can increase because writes go straight to the database. Monitor your RabbitMQ health to keep async mode active. ## Enabling async mode *** Set one environment variable in the ledger application: ```bash Environment variable theme={null} RABBITMQ_TRANSACTION_ASYNC=true ``` With `false` (the default), all transactions use synchronous processing and persist directly to PostgreSQL. The current Ledger bootstrap still initializes RabbitMQ and wires its consumer. With `true`, the ledger publishes transaction payloads to the configured RabbitMQ exchange. A background consumer then handles persistence. ## RabbitMQ configuration *** Async mode uses the following RabbitMQ settings (all in the ledger `.env`): | Variable | Description | Default | | :------------------------------------------------ | :-------------------------------------- | :--------------------------------------------------- | | `RABBITMQ_TRANSACTION_ASYNC` | Enable async processing. | `false` | | `RABBITMQ_HOST` | RabbitMQ server hostname. | `midaz-rabbitmq` | | `RABBITMQ_PORT_HOST` | AMQP protocol port. | `3003` | | `RABBITMQ_PORT_AMQP` | Management API port. | `3004` | | `RABBITMQ_DEFAULT_USER` | Producer credentials (user). | `transaction` | | `RABBITMQ_DEFAULT_PASS` | Producer credentials (password). | — | | `RABBITMQ_CONSUMER_USER` | Consumer credentials (user). | `consumer` | | `RABBITMQ_CONSUMER_PASS` | Consumer credentials (password). | — | | `RABBITMQ_NUMBERS_OF_WORKERS` | Number of consumer worker goroutines. | `5` | | `RABBITMQ_NUMBERS_OF_PREFETCH` | Messages prefetched per worker. | `10` | | `RABBITMQ_TRANSACTION_BALANCE_OPERATION_EXCHANGE` | Exchange name for transaction messages. | `transaction.transaction_balance_operation.exchange` | | `RABBITMQ_TRANSACTION_BALANCE_OPERATION_KEY` | Routing key. | `transaction.transaction_balance_operation.key` | | `RABBITMQ_TRANSACTION_BALANCE_OPERATION_QUEUE` | Queue name. | `transaction.transaction_balance_operation.queue` | The consumer uses separate credentials (`RABBITMQ_CONSUMER_USER` / `RABBITMQ_CONSUMER_PASS`) from the producer. This follows the principle of least privilege — the consumer only needs read access to the queue. ## Balance synchronization *** A dedicated balance-sync worker coordinates balance updates. It uses Redis as a coordination layer. This worker runs in both synchronous and asynchronous modes. It keeps balances consistent even when multiple consumers process messages at the same time. | Variable | Description | Default | | :------------------------------ | :---------------------------------------------------------- | :------ | | `BALANCE_SYNC_BATCH_SIZE` | Number of balance updates to batch before flushing. | `50` | | `BALANCE_SYNC_FLUSH_TIMEOUT_MS` | Maximum wait time (ms) before flushing an incomplete batch. | `500` | | `BALANCE_SYNC_POLL_INTERVAL_MS` | How often (ms) the worker checks for pending updates. | `50` | The balance-sync worker runs automatically in both modes. You need no extra setup beyond an available Redis instance. ## RabbitMQ circuit breaker *** When you enable async mode, Midaz depends on RabbitMQ for transaction persistence. A built-in circuit breaker protects against broker outages. It monitors the health of the RabbitMQ connection and fails fast when the broker goes down. This prevents request pileups and cascading failures. The circuit breaker is active on the single-tenant RabbitMQ path. Multi-tenant RabbitMQ uses tenant connection management instead. The circuit breaker follows the standard three-state model: * **Closed** (normal): requests flow through to RabbitMQ. The breaker counts failures. * **Open** (tripped): the breaker does not contact RabbitMQ. Midaz bypasses the broker and attempts a direct database write for each async transaction; if that write fails, Midaz returns the database error. A background health checker monitors the broker and attempts recovery. * **Half-open** (probing): the breaker lets a limited number of requests through to test RabbitMQ recovery. If they succeed, the circuit closes. If they fail, it reopens. The circuit opens when either condition is true: * The number of consecutive failures reaches the threshold, OR * The failure ratio exceeds the configured percentage within the counting window ### Circuit breaker configuration | Variable | Description | Default | | :----------------------------------------------- | :------------------------------------------------------------------------------- | :------ | | `RABBITMQ_CIRCUIT_BREAKER_CONSECUTIVE_FAILURES` | Consecutive failures before the circuit opens. | `15` | | `RABBITMQ_CIRCUIT_BREAKER_FAILURE_RATIO` | Failure percentage (0–100) that triggers open state. | `50` | | `RABBITMQ_CIRCUIT_BREAKER_MIN_REQUESTS` | Minimum requests before evaluating the failure ratio. | `10` | | `RABBITMQ_CIRCUIT_BREAKER_INTERVAL` | Time window (seconds) for counting failures. Counters reset after each interval. | `120` | | `RABBITMQ_CIRCUIT_BREAKER_TIMEOUT` | How long (seconds) the circuit stays open before transitioning to half-open. | `30` | | `RABBITMQ_CIRCUIT_BREAKER_MAX_REQUESTS` | Requests allowed through in half-open state to probe recovery. | `3` | | `RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_INTERVAL` | How often (seconds) the background health checker pings RabbitMQ. | `30` | | `RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_TIMEOUT` | Timeout (seconds) for each health check ping. | `10` | When the circuit is open, Midaz attempts direct database writes for async transactions. If a direct write fails, Midaz returns the database error; the fallback does not guarantee transaction delivery during broker outages. For most production deployments, the defaults work well. Tune `CONSECUTIVE_FAILURES` and `TIMEOUT` if your RabbitMQ cluster has known recovery patterns. For example, lower the timeout if your broker recovers within seconds. Increase consecutive failures if you see transient network blips. ## How async mode connects to Bulk Recorder *** Async mode and the [Bulk Recorder](/en/midaz/bulk-recorder) are complementary features that work together: 1. **Async mode** decouples the API response from persistence — transactions go to RabbitMQ instead of directly to PostgreSQL. 2. **Bulk Recorder** optimizes how the consumer writes those messages to the database — it batches multiple messages into single bulk inserts. Bulk Recorder is active with async mode unless you explicitly set `BULK_RECORDER_ENABLED=false`. Without async mode, transaction persistence uses direct database writes and there is no transaction queue to batch. | Configuration | Processing behavior | | :-------------------------------------------- | :-------------------------------------------------- | | Async `false` | Direct database write per transaction (synchronous) | | Async `true`, Bulk Recorder `false` | Queue-based, one message processed at a time | | Async `true`, Bulk Recorder enabled (default) | Queue-based, messages batched for bulk inserts | ## When to use async mode *** **Use async mode when:** * You need lower API response times for transaction creation. * Your workload involves high transaction volumes (hundreds+ per second). * You run batch operations like mass payouts or settlements. * You want to decouple your API tier from database performance. **Keep synchronous mode when:** * You need direct, request-bound transaction persistence. * Transaction volume is low to moderate. * You want a successful API response to mean that Midaz already persisted the data. * You work in a development or testing environment where simplicity matters more than throughput. You can switch between modes at any time. Change `RABBITMQ_TRANSACTION_ASYNC` and restart the ledger application. You need no data migration, because the transaction format is the same in both paths. # Balance Overdraft Source: https://docs.lerian.studio/en/midaz/balance-overdraft Enable controlled Balance Overdraft in Midaz with automatic split operations, credit repayment priority, and limits for BNPL or settlement accounts. Balance Overdraft lets you debit a balance beyond its available funds. The primary balance never goes negative. Midaz tracks the deficit as **OverdraftUsed** and splits the operation between the primary balance and an internal companion balance. When credits arrive, Midaz repays the overdraft first. Any remainder flows to Available. This mechanism supports credit lines, BNPL, settlement accounts, earned wage access, and any product that needs controlled negative positions. ## Balance direction *** Balances carry a `direction` field that defines how debits and credits affect the balance: | Direction | Behavior | Typical use | | --------- | --------------------------------- | ------------------------------------ | | `credit` | Debit decreases, credit increases | Checking accounts, wallets, reserves | | `debit` | Debit increases, credit decreases | Loans, overdraft tracking, payables | At creation, Midaz uses an explicit `direction` when provided, then the Account Type's `defaultDirection`. If neither is set, external Accounts use `debit`; all other Accounts use `credit`. You set direction at creation time. It is **immutable**. The overdraft companion balance (described below) always uses `direction=debit`. ## Balance settings *** The `settings` object on a balance controls overdraft behavior: | Field | Type | Description | | ----------------------- | ---------------- | ---------------------------------------------------------------------------------------------------- | | `allowOverdraft` | boolean | Enables overdraft on this balance | | `overdraftLimitEnabled` | boolean | Gates whether a limit is enforced | | `overdraftLimit` | string (decimal) | Maximum overdraft amount. Required when `overdraftLimitEnabled` is `true`. Must be greater than `0`. | The `settings` object also carries `balanceScope`. It identifies a transactional balance (the default) or a system-managed internal balance, such as the overdraft companion. You can set `balanceScope: "transactional"` when you create or update a public balance. You cannot set `balanceScope: "internal"` through the public API. ## Configuration modes *** ### No overdraft (default) The standard behavior. Midaz rejects any debit that exceeds the available balance. ```json JSON theme={null} { "key": "checking", "assetCode": "BRL", "settings": { "allowOverdraft": false } } ``` ### Unlimited overdraft The derived position can go negative without a cap. The persisted `Available` balance remains at `0`, while Midaz tracks the deficit as `OverdraftUsed`. Use this for settlement or pool accounts, where negative positions are normal and you reconcile them externally. ```json JSON theme={null} { "key": "settlement", "assetCode": "USD", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": false } } ``` ### Limited overdraft The derived position can go negative up to a defined limit. The persisted `Available` balance remains at `0`, while Midaz tracks the deficit as `OverdraftUsed`. This is the most common mode for consumer credit products. ```json JSON theme={null} { "key": "checking", "assetCode": "BRL", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "5000.00" } } ``` When `overdraftLimitEnabled` is `true`, you must set `overdraftLimit` to a positive decimal string. If you omit it or set it to `"0"`, Midaz returns error `0172 - ErrInvalidBalanceSettings`. ## How overdraft works *** ### Operation split When a debit transaction exceeds the available funds, Midaz automatically splits the operation: 1. The debit consumes all remaining Available and floors it at **0**. 2. Midaz accrues the excess as **OverdraftUsed** on the primary balance. 3. If Midaz finds the internal `"overdraft"` balance (described below), it creates a companion operation. This operation records the liability as a double-entry debit. If it cannot find that balance, Midaz skips the companion operation; the primary balance still accrues **OverdraftUsed**. **Example:** Balance has Available = 300. A debit of 500 arrives. | Step | Available | OverdraftUsed | Description | | ------ | --------- | ------------- | ----------------------------------------------------- | | Before | 300 | 0 | Normal state | | After | 0 | 200 | 300 consumed from Available, 200 accrued as overdraft | The transaction succeeds as a single atomic operation. The caller does not need to handle the split — Midaz does it automatically. If you configure a limit, Midaz checks the resulting OverdraftUsed against `overdraftLimit` **before** it processes the transaction. If the result exceeds the limit, Midaz rejects the transaction with error `0167 - ErrOverdraftLimitExceeded`. ### Automatic repayment (refund split) When a credit arrives and `OverdraftUsed > 0`, Midaz prioritizes repayment: 1. Midaz applies the credit to **OverdraftUsed** first and reduces the debt. 2. Any remaining amount after OverdraftUsed reaches 0 flows to **Available**. 3. If Midaz finds the internal `"overdraft"` balance, a companion operation on it records the repayment. If it cannot find that balance, Midaz skips the companion operation; the credit still repays **OverdraftUsed** on the primary balance. **Example:** OverdraftUsed = 200, Available = 0. A credit of 350 arrives. | Step | Available | OverdraftUsed | Description | | ------ | --------- | ------------- | --------------------------------- | | Before | 0 | 200 | Overdraft active | | After | 150 | 0 | 200 repaid, 150 goes to Available | Repayment is automatic. You cannot bypass it. Midaz reduces overdraft positions as early as possible, which keeps the balance healthy. ### Cancelling a pending overdraft transaction When you cancel a `PENDING` transaction that drew overdraft: 1. The cancel reverses the original hold and any overdraft drawn during the pending window. `OverdraftUsed` returns to its value before the hold. 2. If Midaz finds the internal `"overdraft"` balance, a companion `CREDIT` operation on it shrinks the liability by the exact amount drawn. If it cannot find that balance, Midaz skips the companion operation; the primary cancel still restores `OverdraftUsed`. 3. When Midaz creates the companion credit, it applies the primary cancel and the companion credit in the **same atomic batch**, so the two balances do not drift. While the internal `"overdraft"` balance exists, Midaz keeps it in step with the primary balance across the hold, commit, and cancel phases of any pending transaction that touches overdraft. ## Position *** Every balance response includes a computed `position` block. It gives a real-time view of the balance state: | Field | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `available` | Derived `position.available`. It can be negative when the persisted balance has `Available = 0` and `OverdraftUsed > 0`. | | `onHold` | Mirrors `Balance.OnHold` — funds reserved by pending operations. | | `overdraftLimitAvailable` | Remaining non-negative overdraft headroom. It is `"0"` when a configured limit is fully used and is omitted when overdraft is unlimited. | Do not cache the `position` block for accounting purposes. Midaz never persists it — it computes the block at query time from the current balance state. ## Companion balance *** When you update a balance to set `allowOverdraft` to `true` for the first time, Midaz auto-provisions a **companion balance** under the same account. The companion balance records the liability side of the double-entry. Midaz creates it once per account and reuses it across every overdraft draw and repayment. | Property | Value | Why | | ---------------- | ------------- | ---------------------------------------------------------------------- | | `key` | `"overdraft"` | Reserved system key | | `direction` | `debit` | The companion tracks a liability — debits grow it, credits shrink it | | `scope` | `internal` | Blocks direct user operations | | `allowSending` | `true` | Required for DEBIT operations on the companion (overdraft draws) | | `allowReceiving` | `true` | Required for CREDIT operations on the companion (overdraft repayments) | This balance is **fully system-managed**: * You cannot create, modify, or delete it through the public API. * Midaz **reserves** the key `"overdraft"`. A request that creates a balance with this key returns error `0170 - ErrReservedBalanceKey`. * It mirrors the liability as a proper double-entry record, so the ledger stays balanced. The `scope: "internal"` value blocks direct user operations, regardless of the permission flags above. Midaz rejects any direct operation on this balance with error `0168 - ErrDirectOperationOnInternalBalance`. The companion moves only through system-driven overdraft enrichment. ## Overdraft state on operations *** Every operation exposes the overdraft state on the `balance` and `balanceAfter` blocks. The `overdraftUsed` field records the overdraft consumed before and after the operation. This gives a complete audit trail without a separate balance query. For operations that do not touch overdraft, both values are `"0"`. System-managed companion operations on the `"overdraft"` balance use `type: "OVERDRAFT"` (uppercase). The `direction` field carries the lifecycle: `"debit"` for a draw, `"credit"` for a repayment. ```json Primary debit drawing overdraft theme={null} { "type": "DEBIT", "direction": "debit", "amount": { "value": "500" }, "accountAlias": "@user123", "balanceKey": "checking", "balance": { "available": "300", "onHold": "0", "version": 1, "overdraftUsed": "0" }, "balanceAfter": { "available": "0", "onHold": "0", "version": 2, "overdraftUsed": "200" } } ``` ```json Companion overdraft draw theme={null} { "type": "OVERDRAFT", "direction": "debit", "amount": { "value": "200" }, "balanceKey": "overdraft", "balance": { "available": "0", "onHold": "0", "version": 1, "overdraftUsed": "0" }, "balanceAfter": { "available": "200", "onHold": "0", "version": 2, "overdraftUsed": "200" } } ``` Both the primary and the companion operation share the same `overdraftUsed` before/after pair. They mirror the primary balance's overdraft transition, so the lifecycle is visible from either row. The internal `snapshot` JSONB column on the `operations` table stores the same values for indexing and historical reconstruction. This column is not part of the public JSON wire. The values surface on `balance.overdraftUsed` and `balanceAfter.overdraftUsed` instead. Midaz can add future system-generated context to the snapshot without breaking the public contract. Companion operations inherit the primary operation's `routeId`. For each overdraft-capable route, configure both the `debit` and `credit` rubrics of the `overdraft` entry; Midaz requires both. Midaz resolves `routeCode` and `routeDescription` from the rubric that matches the companion's direction. ## Overdraft events *** At runtime, Midaz enables overdraft-event publication unless `RABBITMQ_OVERDRAFT_EVENTS_ENABLED` is explicitly `false`. The bundled example environment sets the flag to `false`; a deployment that starts from that example publishes no overdraft events until you set it to `true`. ```bash Environment theme={null} # The bundled example disables overdraft-event publication. Runtime enables it unless the flag is explicitly false. RABBITMQ_OVERDRAFT_EVENTS_ENABLED=false # Optional: route overdraft events to a dedicated exchange. # When unset, the broker's default exchange is used. RABBITMQ_OVERDRAFT_EVENTS_EXCHANGE=transaction.overdraft_events.exchange ``` ### Event types | Event | Description | | ------------------- | ------------------------------------------------------------------------ | | `overdraft.drawn` | Overdraft was consumed — OverdraftUsed increased | | `overdraft.repaid` | Overdraft was partially repaid — OverdraftUsed decreased but remains > 0 | | `overdraft.cleared` | Overdraft was fully repaid — OverdraftUsed reached 0 | ### Example event payload ```json JSON expandable theme={null} { "source": "midaz", "eventType": "balance", "action": "overdraft.drawn", "timestamp": "2026-04-28T14:30:00.000000Z", "version": "v3.0.0", "organizationId": "0198575d-f9fd-702b-bb15-fa4c980b32c7", "ledgerId": "0198575d-fa0b-7ac7-8b7d-9d3ab7dccafc", "payload": { "accountId": "0198575f-a8f9-7924-a6d7-8122f2c77ddd", "transactionId": "019b2c3d-4e5f-6789-0123-456789abcdef", "amount": "200", "overdraftBalance": "200", "timestamp": "2026-04-28T14:30:00.000000Z" } } ``` Use overdraft events to trigger downstream workflows — interest accrual, customer notifications, risk alerts, or automatic collection processes. ## Use cases *** ### Checking account overdraft (cheque especial) Classic consumer credit. The checking account's derived position can go negative up to a pre-approved limit; the persisted `Available` balance remains at `0` and the outstanding amount is tracked as `OverdraftUsed`. ```json JSON theme={null} { "key": "checking", "assetCode": "BRL", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "2000.00" } } ``` ### Buy Now, Pay Later (BNPL) A BNPL provider issues a purchase credit against the customer's balance. This creates an immediate overdraft position that the customer repays in installments. ```json JSON theme={null} { "key": "bnpl", "assetCode": "BRL", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "10000.00" } } ``` ### Earned Wage Access / Salary advance Employees draw against future earnings. Payroll credits clear the overdraft position when they arrive. ```json JSON theme={null} { "key": "salary-advance", "assetCode": "BRL", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "3000.00" } } ``` ### Marketplace receivables advance Sellers receive an advance on future receivables. Midaz repays the overdraft automatically as sales settlements arrive. ```json JSON theme={null} { "key": "receivables", "assetCode": "BRL", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "50000.00" } } ``` ### Settlement / Pool accounts (unlimited mode) Settlement and pool accounts routinely go negative during intraday processing. Unlimited overdraft avoids artificial rejections while you reconcile the position by end-of-day. ```json JSON theme={null} { "key": "settlement-pool", "assetCode": "USD", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": false } } ``` ### Revolving credit lines (B2B) Businesses draw and repay from a revolving credit facility. The overdraft limit represents the total credit line. ```json JSON theme={null} { "key": "credit-line", "assetCode": "BRL", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "500000.00" } } ``` ### Insurance pre-financing Insurers pre-finance claims before premium collection cycles close. The overdraft covers the gap between payout and collection. ```json JSON theme={null} { "key": "claims-prefin", "assetCode": "BRL", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "100000.00" } } ``` ### Loyalty programs (advanced points) Customers redeem points before they earn them. The overdraft tracks the point deficit and clears as customers accrue new points. ```json JSON theme={null} { "key": "loyalty-points", "assetCode": "POINTS", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "10000" } } ``` ## Protection rules *** Overdraft introduces several immutability and access constraints to maintain ledger integrity: * **Direction is immutable.** Once you set a balance's `direction` at creation, you cannot change it. * **Internal balances block writes.** You cannot create, delete, or update the `"overdraft"` companion balance through the public API — a PATCH returns error `0175`. * **Reserved keys.** Midaz reserves the key `"overdraft"` for the system-managed companion balance. * **Disabling overdraft preserves outstanding debt.** You can set `allowOverdraft: false` while `OverdraftUsed > 0` to block future draws, while incoming credits still repay the existing debt. * **Limit cannot drop below usage.** If `OverdraftUsed = 200`, Midaz rejects `overdraftLimit: "100"` with error `0173`, so repay below the new ceiling first or set a higher limit. * **Optimistic concurrency.** Balance updates use version-based concurrency control, and Midaz rejects a stale write with error `0174` — retry with the latest version. For the complete catalog of overdraft-related error codes (0167–0175), see the [Midaz error list](/en/reference/midaz/error-list). ## Next steps *** * Learn about [Balances](/en/midaz/balances) — the foundation that overdraft builds on. * Understand [Operations](/en/midaz/operations) to trace how overdraft splits appear in the ledger. * Set up the [Event Publisher](/en/midaz/event-publisher) to consume overdraft lifecycle events. * Explore [Transactions](/en/midaz/transactions) for the full picture of double-entry accounting in Midaz. # Balances Source: https://docs.lerian.studio/en/midaz/balances Track multiple Balances per Account to segment funds — investment reserves, credit limits, and operational funds without extra Accounts. A **Balance** represents the value that a specific account holds in Midaz. It reflects the result of all operations — debits and credits — over time. Each balance belongs to one asset, such as BRL, USD, or BTC. ## Multiple balances *** A single account can hold several balances. A unique key identifies each one. This lets institutions segment funds without creating multiple accounts for the same customer. External accounts cannot have multiple balances. **Each external account holds exactly one balance.** Typical use cases include: * Investment reserves * Credit limits * Collateral (blocked) funds * Day-to-day operational funds This approach (*Figure 1*) increases flexibility. It keeps the double-entry model — debit and credit — intact for accounting consistency, traceability, and transparency. Account with multiple balances If a transaction does not provide a `balanceKey`, Midaz uses the account's default balance. ### Balance key A `key` field identifies each balance uniquely within the account. * **Maximum length**: 100 characters, with no whitespace. * **Default key**: `"default"`. Midaz creates the default balance automatically when the account is created. * **Uniqueness**: Each key must be unique per account. A request to create a balance with a key that already exists on the account returns an error. * **In transactions**: If a transaction does not specify a `balanceKey`, Midaz uses the balance with key `"default"`. You set the `key` at creation time and cannot change it later. Choose descriptive keys like `"credit"`, `"collateral"`, or `"savings"` to make your balance model self-documenting. ### Permission flags Each balance has two independent permission flags that control whether it can participate in transactions: | Flag | Type | Description | | ---------------- | ------- | --------------------------------------------------- | | `allowSending` | boolean | Whether funds can be sent **from** this balance | | `allowReceiving` | boolean | Whether funds can be received **into** this balance | These flags are **per-balance** — they apply to one balance, not to the whole account. Both default to `true` when you do not set them. **Common use cases:** * **Freeze a balance**: Set both `allowSending` and `allowReceiving` to `false` to prevent any movement. * **Receive-only balance**: Set `allowSending` to `false` to block outbound transfers and still accept inflows. * **Send-only balance**: Set `allowReceiving` to `false` to prevent new funds from entering this balance. You can set both flags when you create a balance. You can also update them independently through the [Update a Balance](/en/reference/midaz/update-a-balance) endpoint. If an update request omits a flag, its current value stays unchanged. Midaz reads permission flags during transaction validation. A PATCH that changes only `allowSending` or `allowReceiving` does not rewrite an existing Valkey entry, so do not assume the immediately next cache-hit transaction will observe the change. Changes never alter operations already processed. ## Usage examples *** * **User Wallet (BRL)**: A digital wallet that shows an available balance of R\$500. * *Use case*: Show the balance in a mobile banking app and validate funds before a payment. * **Settlement Account (USD)**: A liquidity provider account with a USD balance of \$120,000. * *Use case*: Make sure daily treasury operations keep enough buffer for FX settlements. * **Blocked Balance (BRL)**: An account balance reserved as collateral. * *Use case*: Prevent the use of funds until a loan closes or the borrower meets the conditions. A blocked (collateral) balance restricts funds at the **balance level**. Midaz holds the value in a separate balance and combines it with permission flags to keep the funds unavailable. This differs from a [Block transaction](/en/midaz/transactions#blocking-and-unblocking-funds), which records a ledger movement with `BLOCK`-typed operations. A Block transaction flags funds for reasons like a compliance hold. Use a collateral balance for a standing operational restriction. Use a Block transaction when you need an auditable ledger entry. ## Balance structure *** * **Balance > Account**: Each Balance belongs to an Account, which holds and moves value. * **Balance > Asset**: Each Balance uses a specific Asset, such as BRL or BTC. * **Balance > Ledger**: Balances exist within a Ledger, which enables multi-book environments. * **Balance > Key**: Each Balance has a unique key within the account (e.g., `default`, `credit`, `collateral`). *Figure 2* shows an example of the structure. Balance structure relationships A balance is more than just a number. It includes metadata about the state of funds, such as pending operations and effective availability. ## Key characteristics *** * **Real-time tracking**: Midaz updates balances with every confirmed operation. * **Multiple balances per account**: Accounts can hold several balances, each with its own rules. * **Single source of truth**: Balances reflect the net sum of all operations on the account. * **Query by context**: You can list balances within an organization and ledger, retrieve them by account ID or alias, and retrieve an External Account’s balance by asset code. The balance-list endpoints do not filter by a generic asset code or `balanceKey`. * **Supports external accounts**: You can retrieve balances for internal or external accounts, such as liquidity pools or partners. ## Using balances in transactions *** The following transaction endpoints accept a `balanceKey` field to specify which balance to use: * [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) * [Create an Inflow Transaction](/en/reference/midaz/create-an-inflow-transaction) * [Create an Outflow Transaction](/en/reference/midaz/create-an-outflow-transaction) * [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) If a request does not provide a `balanceKey`, Midaz uses the account's default balance. ### New fields in responses * `balanceKey` - Appears in transactions and operations to show which balance the transaction used. * `key` - Appears in balances to identify each balance uniquely. Always use the `balanceKey` consistently across requests and responses. This avoids mismatches when accounts hold multiple balances. ## Cache key changes (Valkey) *** Balances in the cache (Valkey) include the `balanceKey`. ### Previous format ```json theme={null} :: ``` ### New format ```json theme={null} balance:{transactions}:::# ``` The key carries the `balance:{transactions}:` prefix, and the `balance_key` is appended to the account alias with a `#` separator. A tenant namespace can prefix the key further in multi-tenant deployments. Update direct Valkey readers to construct `balance:{transactions}:::#`, including `#default` for the default balance. Reads using the previous key format miss the current cache entry. ## Overdraft *** Balances support **overdraft** — the ability to debit a balance beyond its available funds. When you enable overdraft, Midaz tracks the deficit as `overdraftUsed`. Midaz also handles the operation split and repayment automatically. Two fields support this feature: * **`direction`** — For an automatically created default balance, direction is `credit` for non-external accounts and `debit` for External Accounts. Additional balances may set direction at creation; it cannot change afterward. * **`settings`** — Controls overdraft behavior: `allowOverdraft`, `overdraftLimitEnabled`, and `overdraftLimit`. Midaz **reserves** the key `"overdraft"` for the system-managed companion balance that records the liability side. A request to create a balance with this key returns an error. `settings.balanceScope` also distinguishes balances by scope. **Transactional** balances (the default) are user-managed and take part in regular transactions. The system operates **internal** balances exclusively — like the overdraft companion. User transactions cannot target, modify, or delete them through the public API. For full details on configuration modes, operation splits, automatic repayment, events, and use cases, see [Balance Overdraft](/en/midaz/balance-overdraft). ## Balance history *** Midaz provides **point-in-time queries** for balances. You can retrieve a balance state at a past timestamp on or after the balance’s creation. If no operation exists before that timestamp, Midaz returns the initial zero state; it returns `404` when the requested timestamp precedes balance creation. This supports auditing, reconciliation, and historical reporting. ### How it works When you query balance history, Midaz returns historical identity and amount fields. It omits `allowSending`, `allowReceiving`, `deletedAt`, and `metadata`; the current implementation also does not reconstruct historical `direction` or `settings`, and returns `overdraftUsed` as zero. Do not treat it as a full regular-balance response minus permission flags. **Why does history exclude permission flags?** `allowSending` and `allowReceiving` are mutable operational settings. You can toggle them at any time without a ledger entry. Balance amounts (`available`, `onHold`) change only from recorded transactions. Permission flags represent the *current* operational state of a balance, not a fact about its past. Historical audits and reconciliation care about **amounts** at a point in time. Whether sending or receiving worked at a given moment does not matter for audit or reconciliation. Mutable permission state in immutable snapshots would add ambiguity without value. ### Use cases * **Regulatory auditing**: Prove the exact balance of an account at a specific compliance checkpoint. * **Reconciliation**: Compare balance snapshots across systems at matching timestamps. * **Dispute resolution**: Retrieve the precise account state at the time of a contested transaction. * **End-of-day reporting**: Capture balance positions at market close for treasury operations. The `date` parameter is required. It must follow the format `yyyy-mm-dd hh:mm:ss` (e.g., `2026-01-15 10:30:00`). Midaz returns `404` when the requested timestamp precedes balance creation. ### Querying balance history You can query history for a single balance or for all balances of an account: * [Retrieve Balance History](/en/reference/midaz/retrieve-balance-history) - Get the state of a specific balance at a given point in time. * [Retrieve Balance History by Account](/en/reference/midaz/retrieve-balance-history-by-account) - Get the state of all balances for an account at a given point in time. ## Managing Balances *** You can retrieve your balances through the API. The Midaz ledger engine computes balance amounts from transactions — you cannot set `available` or `onHold` directly. You manage the balance records — key, permission flags, and settings — through the endpoints below. * [Create a Balance](/en/reference/midaz/create-a-balance) - Create a new balance for an account by defining a unique key. * [List Balances](/en/reference/midaz/list-balances) - Retrieve all balances by organization and ledger. * [Retrieve a Balance](/en/reference/midaz/retrieve-a-balance) - Get the balance of a specific account by its unique ID. * [Retrieve Balances by Account](/en/reference/midaz/retrieve-balances-by-account) - Get the balance for a specific account. * [Retrieve a Balance by Account Alias](/en/reference/midaz/retrieve-a-balance-by-account-alias) - Get the balance with a human-readable account alias (e.g., @user123). * [Retrieve a Balance of an External Account](/en/reference/midaz/retrieve-a-balance-of-an-external-account) - Retrieve the balance of an external account (e.g., `@external/BRL`). * [Update a Balance](/en/reference/midaz/update-a-balance) - Update the permission flags and settings of a balance. * [Delete a Balance](/en/reference/midaz/delete-a-balance) - Delete a balance entry from the system. Do you want to trace **how** a balance formed? Use the Operations API to inspect the ledger history that affected that account. ## Next steps *** * Use the [Operations API](/en/midaz/operations) to trace transactions that involve multiple balances. * Combine multiple balances with [Accounting Routes](/en/midaz/transaction-routing-entities) to create flexible and scalable financial flows. # Production ready Source: https://docs.lerian.studio/en/midaz/best-practices Hub for Midaz production best practices covering common mistakes, scalability strategies, banking entity hierarchy, and hardening for high-volume workloads. Midaz uses double-entry accounting as a single source of truth for every transaction. These best practices help you keep financial operations transparent, auditable, and compliant. ## Content *** This section contains the following pages: * [**Common mistakes to avoid**](/en/midaz/common-mistakes-to-avoid): The most common mistakes with Midaz and how to avoid them. * [**Scalability strategies**](/en/midaz/scalability-strategies): How to scale your Midaz deployment for high transaction volumes and multi-currency, multi-entity operations. * [**Structuring bank operations with Midaz**](/en/midaz/structuring-bank-operation): A strategic approach to entity hierarchy and account structure in a banking environment. * [**Midaz production best practices**](/en/midaz/midaz-production-best-practices): Recommendations to set up your Midaz production environment from the start. You minimize downtime, protect your data, and handle high-volume workloads. # Build on Midaz Source: https://docs.lerian.studio/en/midaz/build-midaz-sdk Build integrations on Midaz with the official Go and TypeScript SDKs — typed clients, sensible defaults, and shared patterns for API calls and errors. Midaz SDKs are the public integration layer for applications that call Midaz APIs. Use an SDK to build an application, automation, backend service, or integration on Midaz. Each SDK works with Midaz resources: organizations, ledgers, accounts, portfolios, balances, transactions, assets, and metadata. The SDKs give you typed, idiomatic interfaces for the Midaz API. You focus on product behavior, not raw HTTP requests, payloads, pagination, or response parsing. ## Available SDKs *** We offer two official SDKs: Use the official Go SDK to build Midaz integrations in Go. Use the official TypeScript SDK to build Midaz integrations in Node.js or TypeScript applications. ## Why use Midaz SDKs? *** * Developer-friendly: clean interfaces, strong typing, and sensible defaults. * Production-ready: shared patterns for API calls, errors, and integration workflows. * Consistent across stacks: the same integration model runs in Go and TypeScript. Use the language that fits your workflow. * API-focused: SDKs connect to the public Midaz APIs. Your application keeps control of its data and business logic. Each SDK is modular, so you bring in only what you need. ## SDKs and internal libraries *** SDKs are the public path for external applications to integrate with Midaz. Lerian also maintains internal shared libraries for its own services, plugins, and platform components. Lerian engineering teams use these libraries to standardize how services behave. These libraries are not public integration contracts. Do not treat them as supported APIs for external applications. To integrate with Midaz, start with the official SDK for your language. If an SDK does not cover a capability, use the Midaz API directly or contact Lerian first. # Build an account statement Source: https://docs.lerian.studio/en/midaz/building-account-statements Turn Midaz account operations into a clear customer statement by filtering by date, direction, and route with the List Operations by Account endpoint. This guide explains how to transform Midaz account operations into end-user account statements. You build an account statement from Midaz account operations. Each operation is a ledger movement that links to an account, such as a credit, debit, hold, release, or overdraft event. To build a statement, retrieve the account operations for the period. Keep the operations that affect the statement view. Then transform each operation into a row that users understand. Account balance flow Use the [List Operations by Account](/en/reference/midaz/list-operations-by-account) endpoint to list operations for a specific account: ```json theme={null} GET /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id}/operations ``` Use query parameters to define the statement period, control pagination, and filter the operations that the endpoint returns. ### Required for statement queries | Parameter | Description | | ----------------- | ---------------------------------------- | | `start_date` | Start date of the statement period | | `end_date` | End date of the statement period | | `limit` | Number of items per page | | `sort_order=desc` | Returns the most recent operations first | You need these fields for this statement use case. They define the statement window and make the result predictable for users. ### Required for pagination | Parameter | Description | | --------- | ------------------------------------------ | | `cursor` | Cursor returned from the previous response | ### Optional filters | Parameter | Description | | ------------------ | ---------------------------------------- | | `direction=credit` | Returns only incoming operations | | `direction=debit` | Returns only outgoing operations | | `type` | Filters by operation type | | `route_id` | Filters by the operation route ID (UUID) | | `route_code` | Filters by the operation route code | The endpoint can return the following operation types: | Type | What it means | | ----------- | ---------------------------------------------- | | `CREDIT` | Value entering the account | | `DEBIT` | Value leaving the account | | `ON_HOLD` | Amount temporarily locked | | `RELEASE` | Previously locked amount released | | `OVERDRAFT` | Movement related to overdraft usage | | `BLOCK` | System-generated account-block companion row | | `UNBLOCK` | System-generated account-unblock companion row | Use `type` to classify the accounting movement. Use `direction` to decide whether the statement shows the amount as positive or negative. Example request: ```json theme={null} GET /v1/organizations/org_123/ledgers/ledger_001/accounts/account_456/operations?start_date=2026-05-01&end_date=2026-05-31&limit=50&sort_order=desc ``` Each object in the `items` array can become one statement row. ### Required for statement rendering | Statement field | Operation field | | -------------------------------- | ------------------------ | | Date | `createdAt` | | Description | `description` | | Movement type | `type` | | Direction | `direction` | | Amount | `amount.value` | | Currency or asset | `assetCode` | | Balance after operation | `balanceAfter.available` | | Receipt or transaction reference | `transactionId` | You need these fields to render a useful statement row. The API does not require all of them. A statement without them loses meaning, traceability, or balance context. ### Recommended for user-friendly statements | Statement context | Operation field | | ----------------- | ----------------------- | | Counterparty | `metadata.counterparty` | | Document | `metadata.document` | | Pix key | `metadata.pixKey` | | End-to-end ID | `metadata.endToEndId` | | Channel | `metadata.channel` | | Category | `metadata.category` | Midaz returns the ledger movement. The integrating system should add business context in the `metadata` of each relevant `source.from[]` and `distribute.to[]` entry when it creates the transaction; metadata does not propagate from source entries to destination entries. Example transformation: ```json theme={null} { "date": "2026-05-18T14:23:11Z", "description": "Pix transfer received", "type": "CREDIT", "direction": "credit", "amount": "150.00", "asset": "BRL", "balanceAfter": "1240.55", "transactionId": "txn_987654", "metadata": { "counterparty": "John Doe", "pixKey": "john@example.com", "category": "Transfer" } } ``` ### Use `direction` to determine the sign | Direction | Display behavior | | --------- | ---------------------------- | | `credit` | Display as a positive amount | | `debit` | Display as a negative amount | Do not use `type` to determine whether the value is positive or negative. The `type` field classifies the accounting movement, while `direction` defines whether the value enters or leaves the account. ### Handle hold and release operations separately Do not display operations with the following types as regular settled movements: * `ON_HOLD` * `RELEASE` Instead: * `ON_HOLD` should appear as a balance hold or temporary lock * `RELEASE` should appear as a balance release or unlock ### Define a settled-operation policy Do not use `balanceAffected` as a settled-operation predicate. A normal `ON_HOLD` operation can set `balanceAffected` to `true` without changing the available balance. Define your statement policy explicitly from operation type and status, and show `ON_HOLD` and `RELEASE` according to the hold/release rules above. The endpoint splits responses into pages according to the `limit` value. To retrieve all operations: 1. Read the `next_cursor` field from the response 2. Send it as the `cursor` parameter in the next request 3. Repeat until the response no longer returns `next_cursor` Example flow: ```text theme={null} Request 1 -> returns items + next_cursor Request 2 -> cursor=next_cursor -> returns more items + next_cursor Repeat until next_cursor is no longer returned ``` ## Example statement output After you apply filters, transform operations, and apply display rules, the final statement can look like this: | Date | Description | Amount | Balance after | | ---------- | -------------------------- | ----------- | ------------- | | 2026-05-18 | Pix received from John Doe | +150.00 BRL | 1,240.55 BRL | | 2026-05-18 | Card purchase | -45.90 BRL | 1,194.65 BRL | The API does not return a ready-made statement page. It returns ledger events that the integrating system turns into a statement experience. ## Add business context The operations endpoint returns accounting events. A user-facing statement needs more context than the ledger movement alone. Send business metadata in each operation's metadata when you create the transaction. Midaz stores those fields with that operation. The statement can use them later to show who, what, and why behind the movement. * `counterparty` * `document` * `pixKey` * `endToEndId` * `channel` * `category` Example metadata: ```json theme={null} "send": { "source": { "from": [{ "accountAlias": "customer_123", "metadata": { "counterparty": "John Doe", "document": "12345678900", "pixKey": "john@example.com", "endToEndId": "E1234567890123456789012345678901", "channel": "PIX", "category": "Transfer" } }] } } ``` This lets you display entries such as: * "Pix received from John Doe" * "Card purchase at Coffee Shop" * "Transfer to Savings Account" instead of generic accounting descriptions. If the integrating system does not send these fields, the statement still works. It can then only display the accounting data that the operation returns. Midaz keeps the ledger consistent and auditable. The integrating system adds business context to each operation's metadata. # Bulk Recorder Source: https://docs.lerian.studio/en/midaz/bulk-recorder Use the Bulk Recorder to batch RabbitMQ messages into bulk PostgreSQL inserts, cutting database round trips and raising throughput on high-volume workloads. ## Why this matters *** Every transaction in Midaz creates balance operations that Midaz must persist. In the default synchronous mode, Midaz writes them directly to the database in the request cycle. This is fine for moderate volumes. At scale — thousands of transactions per second — it becomes the bottleneck. The Bulk Recorder accumulates messages and writes them in batches. It makes fewer round trips to PostgreSQL, lowers lock contention, and raises throughput. High-volume workloads gain the most: mass payouts, batch settlements, and real-time payment processing. For broader strategies to scale Midaz, see [Scalability strategies](/en/midaz/scalability-strategies). ## How it works *** The Bulk Recorder sits between the RabbitMQ consumer and the database layer. It does not insert each message at once. Instead, it collects messages in a buffer and flushes them under two conditions: 1. **Batch size reached** — the buffer fills to the configured size. 2. **Timeout elapsed** — the configured flush timeout expires, even if the buffer is not full. Whichever condition happens first triggers the flush. This gives large batches under load and low latency during quiet periods. 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. Here is the full flow, step by step: 1. **RabbitMQ delivers messages** to the BulkCollector one at a time — Message 1, Message 2, up to Message N. 2. **The BulkCollector holds them in memory** instead of one write per message. It collects until the batch fills or the flush timeout expires. 3. **The BulkCollector sends a chunked bulk INSERT to PostgreSQL.** Midaz splits large batches into chunks that respect PostgreSQL's parameter limits. Each chunk uses `ON CONFLICT (id) DO NOTHING`, so retries and duplicate deliveries stay safe. 4. **PostgreSQL confirms the write** and stores the data. 5. **The BulkCollector acknowledges each message** back to RabbitMQ after the write. It acknowledges them one at a time, not in a single bulk ack. This stops a shared channel from acking messages that other workers still process. ## Enabling Bulk Recorder *** Bulk mode requires async mode. The explicit Bulk Recorder setting is optional because it defaults to enabled: ```bash Environment variables theme={null} RABBITMQ_TRANSACTION_ASYNC=true BULK_RECORDER_ENABLED=true ``` Async mode is required. With `RABBITMQ_TRANSACTION_ASYNC=true`, the Bulk Recorder is enabled by default; set `BULK_RECORDER_ENABLED=false` to process queued messages individually. Without async mode, Midaz persists transactions directly instead of consuming queued messages. `BULK_RECORDER_ENABLED` defaults to `true` when you do not set the environment variable. If you already run with `RABBITMQ_TRANSACTION_ASYNC=true`, bulk mode is probably active. To confirm, check your application logs for `Bulk mode is ACTIVE` at startup. ## Configuration *** | Variable | Description | Default | | :------------------------------- | :----------------------------------------------------------------------- | :--------- | | `BULK_RECORDER_ENABLED` | Enable or disable bulk mode. | `true` | | `BULK_RECORDER_SIZE` | Number of messages to accumulate before flushing. `0` = auto-calculated. | `0` (auto) | | `BULK_RECORDER_FLUSH_TIMEOUT_MS` | Maximum time (ms) to wait before flushing an incomplete batch. | `100` | ### Auto-calculated batch size When `BULK_RECORDER_SIZE` is set to `0` (the default), Midaz derives the batch size: ``` batch size = RABBITMQ_NUMBERS_OF_WORKERS × RABBITMQ_NUMBERS_OF_PREFETCH ``` This aligns the collector's capacity with the real message flow from RabbitMQ. It prevents partial flushes and memory pressure. If you set `BULK_RECORDER_SIZE` by hand, keep it aligned with your prefetch settings. A size much larger than `workers × prefetch` rarely fills the collector. The collector then flushes mostly on the timeout. ## Tuning for your workload *** The two main levers are **batch size** and **flush timeout**. The right balance depends on your priority: latency or throughput. ### Low latency (real-time processing) Keep batches small and timeouts short. Midaz persists messages quickly, even when batches are not full. ```bash Low-latency configuration theme={null} RABBITMQ_NUMBERS_OF_WORKERS=5 RABBITMQ_NUMBERS_OF_PREFETCH=10 BULK_RECORDER_SIZE=0 # auto: 5 × 10 = 50 BULK_RECORDER_FLUSH_TIMEOUT_MS=50 ``` ### High throughput (batch operations) Larger batches and longer timeouts maximize database efficiency. Use this for mass payouts, end-of-day settlements, or migration workloads. ```bash High-throughput configuration theme={null} RABBITMQ_NUMBERS_OF_WORKERS=10 RABBITMQ_NUMBERS_OF_PREFETCH=20 BULK_RECORDER_SIZE=0 # auto: 10 × 20 = 200 BULK_RECORDER_FLUSH_TIMEOUT_MS=300 ``` Start with the defaults and adjust from what you observe. To confirm your settings, watch for the `Bulk mode configured for consumer` log at startup. ## Safety guarantees *** The Bulk Recorder stays safe under every condition: ### Idempotency Every bulk insert uses `ON CONFLICT (id) DO NOTHING`. If a message arrives twice — from a retry, a redelivery, or a network fault — PostgreSQL drops the duplicate. You get no data corruption and no constraint violations. ### Deadlock prevention Before each bulk insert, Midaz sorts the record IDs. All concurrent writers then acquire locks in the same order. This removes the most common source of PostgreSQL deadlocks under high concurrency. ### Internal chunking Midaz splits large batches into chunks that fit within PostgreSQL's 65,535-parameter limit per query: | Record type | Columns per row | Rows per chunk | Parameters per chunk | | :---------- | :-------------- | :------------- | :------------------- | | Transaction | 18 | 1,000 | 18,000 | | Operation | 31 | 1,000 | 31,000 | Midaz handles this chunking for you. Each `INSERT` carries at most 1,000 rows. `BULK_RECORDER_MAX_ROWS_PER_INSERT` is not currently applied to the PostgreSQL chunk size. ## When to use *** **Use Bulk Recorder when:** * You process high volumes of transactions (hundreds or thousands per second). * Your workload includes batch operations like mass payouts, settlements, or data migrations. * You already use async transaction processing (`RABBITMQ_TRANSACTION_ASYNC=true`). * You want to reduce database load and connection pressure. **Keep it disabled when:** * Your volume is low enough that individual inserts are not a bottleneck. * You need strict per-message ordering that batch processing would break. * You debug transaction processing and want a simpler, message-by-message flow. # Closing a customer account Source: https://docs.lerian.studio/en/midaz/closing-a-customer-account Follow the eight-step flow to freeze, settle, and archive a customer account across Ledger and CRM in line with BACEN account closure and audit requirements. To close a customer account in Midaz, you work across two areas: the **Ledger** (accounts and balances) and **CRM** (holders and alias accounts). BACEN rules make account closure a regulated event, so the order matters. Stop new credits first, then settle pending activity and return any remaining funds. Deactivate the underlying records last. This guide covers the full closure flow, from the [Holder](/en/midaz/crm/holders) to its [Alias Accounts](/en/midaz/crm/alias-accounts). You can run the flow two ways. **[Via API](#via-api)** gives you the endpoint, an example payload, and the compliance rationale for each step. **[Via Console](#via-console)** gives you the same steps as point-and-click actions in the Midaz Console. Follow the steps in order. Do not close accounts or archive CRM records before you zero the balances. Early closure can leave orphaned funds or break the audit trail that regulatory reporting needs. ## Overview *** The closure flow has eight steps, grouped into three phases: | Phase | Steps | Goal | | :----------------------- | :---- | :------------------------------------------------------------------------- | | **1. Freeze** | 1–2 | Mark the Holder inactive and stop new credits at the balance level. | | **2. Settle and zero** | 3–4 | Clear pending activity and return remaining funds to the customer. | | **3. Close and archive** | 5–8 | Deactivate Ledger Accounts and archive CRM records under retention policy. | Throughout this guide, `{organization_id}` and `{ledger_id}` identify the Midaz Organization and Ledger that own the accounts. This guide abbreviates the paths to `/v1/.../accounts/{accountId}` for readability. ## Prerequisites *** Before you start, make sure you have: * The `holderId` of the customer to offboard. * The list of `accountId` values linked to that Holder across the Ledger. Retrieve them with `GET /v1/organizations/{organization_id}/holders/{holder_id}/accounts`; the response is paginated, so retrieve every page. * The `instrument_id` of each Alias Account, mapped to its corresponding `accountId`. List the Holder's instruments with `GET /v1/organizations/{organization_id}/instruments?holder_id={holder_id}` (also paginated) and use each instrument's `accountId` field to map it to the Ledger Account. Steps 6 and 7 target these `instrument_id` values. * Confirmation from your compliance team that you can end the customer relationship (no legal holds, open disputes, or pending regulatory requirements). * Appropriate API credentials with permission to modify holders, instruments, balances, and accounts. Steps 6 and 7 update and delete instruments, so the credentials must carry `patch` and `delete` access on the `instruments` resource. Account closure is irreversible from the customer's perspective. Before you proceed, confirm there are no active products or open obligations. ## Via API *** Run the full closure flow programmatically. Each step lists the endpoint, an example payload, and the compliance rationale. ### Step 1 — Freeze the Holder Mark the Holder as inactive to record the closure across your systems. Update the Holder and set the `status` field on its person profile to an inactive value. ```http theme={null} PATCH /v1/organizations/{organization_id}/holders/{holder_id} ``` ```json theme={null} { "naturalPerson": { "status": "INACTIVE" } } ``` Marking the Holder inactive is a record change, not a deletion. The Holder record stays fully readable for audit. This status does not block new credits on its own — Step 2 blocks inflows at the balance level. For a legal person, set `legalPerson.status` instead. ### Step 2 — Block credits on the accounts For each account linked to the Holder, prevent new funds from entering. First, list the [Balances](/en/midaz/balances) of the account. Then update each balance to disable receiving. **Retrieve the account balances:** ```http theme={null} GET /v1/.../accounts/{accountId}/balances ``` **For each `balanceId` returned, block incoming funds:** ```http theme={null} PATCH /v1/.../balances/{balanceId} ``` ```json theme={null} { "allowReceiving": false } ``` Repeat this step for **every** `balanceId` on **every** account belonging to the Holder. A single balance left open can still receive credits and block closure later. When you set `allowReceiving` to `false`, the balance blocks new inflows but still allows outflows. This is exactly what you need in Step 4 to return the remaining balance to the customer. For details on the permission flags, see [Balances](/en/midaz/balances). ### Step 3 — Settle pending activity Before you can zero a balance, the account must have no in-flight movements. * **Check for transactions in processing.** Confirm the account has no pending or uncommitted transactions. Commit or cancel them as appropriate using [Commit a pending transaction](/en/reference/midaz/commit-a-pending-transaction) or [Cancel a pending transaction](/en/reference/midaz/cancel-a-pending-transaction). If you skip this step, a closed account can receive late entries. Late entries break reconciliation and the BACEN audit trail. ### Step 4 — Zero the balance Return any remaining funds to the customer (the account holder) and confirm `available = 0` and `onHold = 0` on every balance. * Record a **return transaction** that moves the remaining `available` amount from each customer account to the holder's designated destination (for example, an external settlement account). Use [Create a transaction](/en/reference/midaz/create-a-transaction-using-json). * **Confirm `available = 0` and `onHold = 0`** on every balance of every account in the Ledger before you proceed. You can check this with [Retrieve balances by account](/en/reference/midaz/retrieve-balances-by-account). Midaz **does not allow deleting an account that still holds a balance**. All balances must be zero before Step 5. ### Step 5 — Close the Ledger Accounts in Midaz After you zero the balances and clear pending activity, delete each Ledger Account. ```http theme={null} DELETE /v1/.../accounts/{accountId} ``` A successful request returns `204 No Content`. Repeat for every account linked to the Holder. See [Delete an account](/en/reference/midaz/delete-an-account) for the full contract. Deleting a Ledger Account is a logical removal. The account and its historical operations remain available for audit and reporting, subject to your retention policy. ### Step 6 — Register the closing date on the alias Record the official closure date on the Holder's Alias Account so the CRM and any regulatory exports reflect when the relationship ended. ```http theme={null} PATCH /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} ``` ```json theme={null} { "bankingDetails": { "closingDate": "2026-06-09" } } ``` The `closingDate` field lives in the `bankingDetails` object of the Alias Account and uses `YYYY-MM-DD` format. See [Alias Accounts](/en/midaz/crm/alias-accounts) for the full field reference. BACEN account-lifecycle reporting needs an accurate closing date. ### Step 7 — Archive the alias accounts in CRM Archive each Alias Account in CRM. Use a **soft delete**. It removes the record from active use but keeps it for the regulatory retention period. ```http theme={null} DELETE /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} ``` Do **not** pass `hard_delete=true`. A regulated closure must archive (soft-delete) the record and keep it. It must not erase the record permanently. See [Delete an instrument](/en/reference/midaz/delete-an-instrument). ### Step 8 — Archive the Holder in CRM After you archive all of its alias accounts, archive the Holder itself with a soft delete. ```http theme={null} DELETE /v1/organizations/{organization_id}/holders/{holder_id} ``` As in Step 7, omit `hard_delete=true`. Keep the Holder record under the applicable retention policy for audit and regulatory inspection. See [Delete a holder](/en/reference/midaz/crm/delete-holder). ## Via Console *** Run the same eight-step closure flow from the [Midaz Console](/en/midaz/console/midaz-module). The Console covers most of the flow point-and-click, but blocking credits (Step 2) still requires the API. Each step below notes the equivalent API step on this page. The order is the same as the API flow. Do not delete accounts or archive CRM records before you zero the balances. ### Step 1 — Freeze the Holder Mark the Holder as inactive to record the closure. This is a record change and does not block new credits on its own. From the **Holders** page, find the Holder to close. Click the three dots () in the **Actions** column, and select **Edit**. In the Holder form, set the **Status** to **Inactive**. Confirm the update. Marking the Holder inactive is a record change, not a deletion. The record stays fully readable for audit. It does not block new credits on its own — Step 2 blocks inflows at the balance level. See [Editing a Holder](/en/midaz/console/crm-editing-a-holder). ### Step 2 — Block credits on the accounts **This step requires the API. The Console does not support editing balance flags after account creation.** The Console lets you set `allowReceiving` only when you first create an Account, not when you edit an existing balance. Use the API to disable receiving on every balance. Follow [Step 2 — Block credits on the accounts](#step-2--block-credits-on-the-accounts) in the Via API section. For each account linked to the Holder, use the API to set `allowReceiving` to `false` on every `balanceId`. This blocks new inflows while outflows remain available for the return transaction in Step 4. ### Step 3 — Settle pending activity Confirm there are no in-flight movements before you zero any balance. From the **Transactions** page, filter by the accounts linked to the Holder. Confirm the account has no pending or uncommitted transactions, and commit or cancel any that are in flight. Review any pending transactions and commit or cancel them as appropriate. ### Step 4 — Zero the balance Return any remaining funds to the customer and confirm `available = 0` and `onHold = 0` on every balance. From the **Transactions** page, click **New Transaction**. Create a **return transaction** that moves the remaining `available` amount from each customer account to the holder's designated destination, for example an external settlement account. See [Creating a Transaction](/en/midaz/console/creating-a-transaction). Open each account and confirm **available = 0** and **onHold = 0** before you continue. Midaz **does not allow deleting an account that still holds a balance**. All balances must be zero before Step 5. ### Step 5 — Close the Ledger Accounts in Midaz After you zero the balances and clear pending activity, delete each Ledger Account. From the **Accounts** page, find the Account linked to the Holder, click the three dots () in the **Actions** column, and select **Delete**. A confirmation dialog will appear. Click **Confirm** to finalize the deletion. Repeat for every account linked to the Holder. Deleting a Ledger Account is a logical removal. The account and its historical operations remain available for audit and reporting, subject to your retention policy. See [Deleting an Account](/en/midaz/console/deleting-an-account). ### Step 6 — Register the closing date on the alias Record the official closure date on the Holder's Alias Account so the CRM and regulatory exports reflect when the relationship ended. From the **Alias Accounts** page, find the alias account to update, click the three dots () in the **Actions** column, and select **Edit**. In the Alias Account form, set the **Closing Date** (in `bankingDetails`) to the official closure date using `YYYY-MM-DD` format. Confirm the update. BACEN account-lifecycle reporting needs an accurate closing date. See [Editing an Alias Account](/en/midaz/console/crm-editing-alias-account). ### Step 7 — Archive the alias accounts in CRM Archive each Alias Account with a **soft delete**. It removes the record from active use but keeps it for the regulatory retention period. From the **Alias Accounts** page, find the alias account to archive, click the three dots () in the **Actions** column, and select **Delete**. A confirmation dialog will appear. Click **Confirm** to finalize. Use the standard (soft) delete. It archives and keeps the record instead of erasing it permanently. In regulated deployments, Midaz keeps the underlying record for the retention period. See [Deleting an Alias Account](/en/midaz/console/crm-deleting-alias-account). ### Step 8 — Archive the Holder in CRM After you archive all of its alias accounts, archive the Holder itself with a **soft delete**. From the **Holders** page, find the Holder to archive, click the three dots () in the **Actions** column, and select **Delete**. A confirmation dialog will appear. Click **Confirm** to finalize. Use **Soft Delete** (the default), not Hard Delete. Keep the Holder record under the applicable retention policy for audit and regulatory inspection. See [Deleting a Holder](/en/midaz/console/crm-deleting-a-holder). ## BACEN compliance notes *** * **Order is mandatory.** Freeze first (Steps 1–2), then settle and zero (Steps 3–4). This order stops funds from entering an account that is mid-closure. * **Return funds before closing.** Return any residual balance to the customer and confirm `available = 0` and `onHold = 0` before you delete an account. Midaz blocks closing an account that holds funds, and that closure would also break compliance. * **Archive, don't erase.** A soft delete (no `hard_delete`) keeps Holders and alias accounts available for the regulatory retention period. Permanent deletion would remove evidence that BACEN audits need. * **Record the closing date.** The `closingDate` on the alias gives regulators an authoritative timestamp for when the relationship ended. * **Preserve the audit trail.** Midaz removes Ledger Accounts and operations logically. They remain queryable for reconciliation and reporting. Treat the eight steps as a single transaction from a compliance standpoint. If any step fails, pause and resolve it before you continue. Do not leave the customer in a partially closed state. # Common mistakes to avoid Source: https://docs.lerian.studio/en/midaz/common-mistakes-to-avoid Avoid the typical pitfalls of Midaz implementations — Ledger structuring, Asset segregation, Accounting Routes, and external Accounts. A reliable Midaz implementation depends on a few structural choices you make early. Avoid the common mistakes below to keep balances accurate and access under control. ## Poor Ledger structuring *** * **Mistake:** You design ledgers that are too complex or too simplistic. For example, one ledger per customer creates fragmentation. Grouping unrelated processes creates confusion. * **Best practice:** Structure ledgers around key business areas or legal entities. Use organized groupings instead of a one-to-one approach. This improves manageability and internal transfers. ## Not using Portfolios for clients *** * **Mistake:** You treat each account in isolation instead of grouping it under a portfolio. This makes it hard to track all accounts linked to a single customer. * **Best practice:** Create a portfolio for each customer. Group their accounts for simple queries, relationship management, and reporting. ## Mixing Assets in one Account *** * **Mistake:** You manage multiple currencies or asset types in a single account, often through metadata overrides. This leads to unclear balances and reporting inconsistencies. * **Best practice:** Assign one asset per account. If a customer needs multi-currency support, create separate accounts. Use naming conventions or portfolios to structure them. ## Inadequate Accounting Routes *** * **Mistake:** You submit transactions without properly configured Operation Routes or Accounting Routes (Transaction Routes in the API). This causes inconsistent validations, misrouted funds, and unreliable ledger data. * **Best practice:** Define Accounting Routes that reflect your business logic and compliance requirements, then set the Ledger setting `accounting.validateRoutes` to `true` to enforce them. Its default is `false`. Use Operation Routes to validate the source and destination of each operation when route validation is enabled. Structured routes keep every transaction auditable. ## Overlooking External Account handling *** * **Mistake:** You mishandle external transactions. For example, you delete an account when funds leave the bank, which causes imbalances. * **Best practice:** Use the canonical `@external/` Account (for example, `@external/BRL`) for money that flows into and out of Midaz. This keeps the ledger balanced and matches real bank statements. ## Security lapses in access control *** * **Mistake:** You grant excessive privileges to administrators or use shared accounts. This compromises security and accountability. * **Best practice:** Implement role-based access control (RBAC) from the start. Do not use root or admin accounts for daily operations. Enforce strict credential management. Audit permissions regularly to prevent unauthorized privilege escalation. ## Improper error handling in transactions *** * **Mistake:** You fail to handle failed transactions in the integration layer. This leads to false assumptions about successful operations. * **Best practice:** Always validate transaction API responses. Implement retry mechanisms with backoff for transient errors. Log permanent failures. Make sure upstream systems, such as core banking applications, handle rejected transactions and notify the user. ## Ensuring a robust Midaz implementation *** Address these pitfalls early and Midaz stays a reliable financial backbone. Your team spends time on new features, not on avoidable problems. # Midaz entities Source: https://docs.lerian.studio/en/midaz/core-entities See how Midaz core entities — Organizations, Ledgers, Assets, Portfolios, Accounts, Segments, and Balances — connect to power financial operations at scale. This diagram shows how the main building blocks of Midaz connect to support financial operations at scale. Figure 1 gives an overview of how the system works. Midaz core building blocks and how its interconnected domains fit together to support financial operations at scale ## Core components *** ### Midaz Midaz is at the center. It coordinates everything, from data persistence to business logic. ### Data Layer All data flows into the persistent **data layer**. This layer gives the system durability, consistency, and performance. ## Domains and relationships *** ### Organizations Each organization is a distinct environment in Midaz. It can hold one or many ledgers, based on how you structure your data. ### Ledgers A ledger holds all financial activity. It tracks balances, stores assets, and powers transactions. Every ledger belongs to an organization. From here, the system branches into: * **Assets**: define the kind of value you track. * **Portfolios**: group related accounts under a shared purpose. ### Assets Assets define the units of value, like BRL, USD, points, or tokens. Each account uses one specific asset. ### Portfolios Portfolios group accounts by business logic. You can create as many portfolios as you need to organize your financial structure. Each portfolio contains one or more accounts. ### Accounts & Segments An account belongs to a portfolio and represents an individual balance. **Segments** group accounts, so you can isolate data by customer, product, region, or any other dimension. Segments help you track and report with precision. **Account Types** classify accounts by nature and purpose. They enable transaction routing and operational validation across account categories. ### Balance A **Balance** represents the net amount an **Account** holds of a given **Asset** in a specific **Ledger**. * An account can carry **multiple balances**. Each has a `balanceKey` (e.g. `default`, `collateral`, `operational`) to partition funds without splitting the account. * An **external account** has only one balance (`default`). * If you omit `balanceKey` in a transaction, Midaz uses the default balance. * Midaz derives balances automatically from ledger operations. You can list, fetch, or (in admin or test scenarios) adjust them manually. * The Valkey cache key includes `balanceKey` in its format: `balance:{transactions}:::#`. ### Transactions A transaction moves value between accounts. Each transaction contains **operations** (debits and credits). This keeps your logic consistent, traceable, and aligned with your business model and accounting policies. ### Accounting Routes entities The [Accounting Routes entities](/en/midaz/transaction-routing-entities) extend Midaz. They create a structured system that guarantees correct annotations and correct financial-flow directions. It has two parts: * **Operation Routes:** define atomic transaction behaviors. Each route sets how Midaz annotates and directs one financial movement, based on your business rules and accounting standards. * **Accounting Routes:** orchestrate multiple Operation Routes into balanced financial events. They enforce double-entry validation and keep transaction integrity across all accounts. (The API calls this `transactionRoute`.) This pair forms a self-governing system. Midaz automatically validates each transaction for correct annotation and flow direction. ## In short *** Midaz gives you a modular structure: * Organizations manage ledgers. * Ledgers hold assets, portfolios, and accounting structures. * Portfolios contain accounts, grouped by segment. * Transactions move value and trigger accounting treatments. * Your data stays synced, traceable, and ready for financial reporting. **Need help designing your Midaz setup?** Our consulting service can help you tailor the structure to your organization's needs. This applies whether you start from scratch or optimize an existing setup. [Contact us](https://lerian.studio/contact) to learn more. # Key features Source: https://docs.lerian.studio/en/midaz/core-features Explore the core capabilities that make Midaz adaptable and scalable — native events, multi-asset support, segments, and double-entry accounting. Midaz is a source-available core ledger. Financial institutions use it to build, operate, and scale their transactions on a modular foundation. You keep full control of your stack. Use Midaz to launch new financial products or to modernize existing processes. The features below show how the ledger records value, scales across organizations, and keeps balances consistent. ## Native event publisher *** Midaz emits supported domain events through its configured streaming publisher. Configure and operate the delivery path required by your environment before relying on an event stream for downstream processing. ## Client segments *** Group Accounts into ledger-scoped Segments. These persisted associations support scoped rules and operational grouping. ## Double-entry accounting *** Midaz records Transactions as Operations, including debit, credit, hold, and release movements. When Accounting Route validation is enabled, configured routes validate applicable participation and posting rules. ## Multi-currency/asset support *** Midaz supports transactions in multiple currencies and assets. You operate across different financial ecosystems from one ledger. ## Multi-organization/ledger support *** One Midaz instance can run multiple Organizations and Ledgers. The domain model scopes financial resources by Organization and Ledger. ## N:N transactions *** A single transaction can move value between many source and destination accounts. This N:N model supports complex business flows. ## Native race condition handling *** Race conditions happen when multiple operations change the same balance at the same time. Midaz handles this per account with atomic balance updates. Transactions stay reliable and accurate under high concurrency. ## Wallets portfolio management *** Group accounts into portfolios to manage them together. You get full visibility and control over assets and balances. ## Visual interface *** A web console gives you a single place to manage transactions, configuration, and system insights. In newer releases, **[Lerian Console](/en/platform/console/about-lerian-console)** provides the visual interface for Midaz. Midaz runs there as the **[Midaz Module](/en/midaz/console/midaz-module)**. If you run an older version, you may still have the standalone Midaz Console, now in maintenance mode. # Data model Source: https://docs.lerian.studio/en/midaz/data-model Reference the core tables and schemas that power Midaz, designed for developers and database administrators working close to the data. **This page is for developers and database administrators.** It contains detailed database schemas and table structures. For a business-level overview of Midaz entities and their relationships, see [About Midaz](/en/midaz/about-midaz). Explore the core tables and schemas that power Midaz. This reference helps developers and database administrators understand how Midaz structures and connects data across the system. As Midaz evolves, it may add new fields and tables. ## Tables *** Each table supports efficient queries and data integrity. ### Table: `organization` | Column | Type | Description | | :----------------------- | :------------ | :--------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Organization. | | **legalName** | `text` | The legal name of the Organization. | | **parentOrganizationId** | `uuid` | The unique identifier of the parent Organization. | | **doingBusinessAs** | `text` | The trade name of the Organization. | | **legalDocument** | `text` | The document of the Organization. | | **status** | `jsonb` | Information about the status. | | **address** | `jsonb` | Information about the address of the Organization. | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | The timestamp of soft deletion, if applicable (UTC). | ### Table: `ledger` | Column | Type | Description | | :----------------- | :------------ | :----------------------------------------------- | | **id** | `uuid` | The unique identifier of the Ledger. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **name** | `text` | The name of the Ledger. | | **status** | `jsonb` | Information about the status. | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `asset` | Column | Type | Description | | :----------------- | :------------ | :----------------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Asset. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **name** | `text` | The name of the Asset. | | **type** | `enum` | The type of Asset (`currency`, `fiat`, `crypto`, `commodity`, `others`). | | **code** | `text` | The code used to refer to the Asset. | | **status** | `text` | Information about the status. | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `account-type` | Column | Type | Description | | :-------------- | :------------ | :------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Account Type. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **name** | `text` | Name of the Account Type. | | **description** | `text` | Description of the Account Type. | | **keyValue** | `text` | Custom value defined by the user to identify the Account Type. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `account` | Column | Type | Description | | :------------------ | :------------ | :--------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Account. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **assetCode** | `text` | The code used to refer to the Asset. | | **name** | `text` | The name of the Account. | | **alias** | `text` | A unique, user-friendly identifier for the account. | | **type** | `text` | The type of account. | | **parentAccountId** | `uuid` | The unique identifier of the Parent Account. | | **entityId** | `text` | The unique identifier of the Entity responsible for the Account. | | **portfolioId** | `uuid` | The unique identifier of the Portfolio. | | **segmentId** | `uuid` | The unique identifier of the Segment. | | **status** | `jsonb` | Information about the status. | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `portfolio` | Column | Type | Description | | :----------------- | :------------ | :--------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Portfolio. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **entityId** | `text` | The unique identifier of the user responsible for the Portfolio. | | **name** | `text` | The name of the Portfolio. | | **status** | `jsonb` | Information about the status. | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `segment` | Column | Type | Description | | :----------------- | :------------ | :----------------------------------------------- | | **id** | `uuid` | The unique identifier of the Segment. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **name** | `text` | The name of the Segment. | | **status** | `jsonb` | Information about the status. | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **created\_at** | `timestamptz` | Timestamp of creation (UTC). | | **updated\_at** | `timestamptz` | Timestamp of last update (UTC). | | **deleted\_at** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `balance` | Column | Type | Description | | :----------------- | :------------ | :----------------------------------------------------------- | | **id** | `text` | The unique identifier of the Balance. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **accountId** | `uuid` | The unique identifier of the Account. | | **alias** | `text` | The alias for the account used in the operation. | | **assetCode** | `text` | The code used to refer to the Asset. | | **available** | `text` | Previous available balance. | | **onHold** | `text` | Amount on hold/reserved. | | **version** | `integer` | Balance version, which is updated with each transaction. | | **accountType** | `text` | The type of account. | | **allowSending** | `boolean` | If true, indicates that sending transactions is permitted. | | **allowReceiving** | `boolean` | If true, indicates that receiving transactions is permitted. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `operation-route` | Column | Type | Description | | :----------------- | :------------ | :--------------------------------------------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Operation Route. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **title** | `text` | Short text summarizing the purpose of the operation. Used as an entry note for identification. | | **description** | `text` | Detailed description of the Operation Route purpose and usage. | | **operationType** | `text` | `operationType` — the entry's applicability: `source`, `destination`, or `bidirectional`. | | **account** | `jsonb` | Defines the rule for selecting the account that will participate in the operation (debit or credit). | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `operation` | Column | Type | Description | | :-------------------------------------------- | :------------ | :------------------------------------------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Operation. | | **transactionId** | `uuid` | The unique identifier of the Transaction. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **accountId** | `uuid` | The unique identifier of the account of which you want to retrieve the balances. | | **balanceId** | `uuid` | The unique identifier of the Balance. | | **accountAlias** | `text` | The alias for the account used in the operation. | | **description** | `text` | Description of the transaction. | | **type** | `text` | The type of the operation (debit or credit). | | **assetCode** | `text` | The name of the asset used in the operation. | | **chartOfAccounts** | `text` | The name of the chart of accounts that the operation belongs to. | | **route** | `text` | **\[Deprecated]** Legacy route identifier, kept for backward compatibility. Use `routeId` instead. | | **routeId** | `uuid` | Identifier of the route used by the operation (the current route field). | | **amount** | `jsonb` | An object containing information about the amount used in the operation. | | **balance** | `jsonb` | An object containing information about the balance before the operation. | | **balanceAfter** | `jsonb` | An object containing information about the balance after the operation. | | **status** | `jsonb` | The transaction status (pending, completed, reversed). | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | **Deprecated field** — This field remains for backward compatibility. Do not use it. ### Table: `transaction-route` | Column | Type | Description | | :------------------ | :------------ | :----------------------------------------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the Transaction Route. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **title** | `text` | Short text summarizing the purpose of the transaction. Used as an entry note for identification. | | **description** | `text` | A description for the Transaction Route. | | **operationRoutes** | `array` | A list of Operation Route IDs that define the debit and credit logic for the transaction. | | **metadata** | `object` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | ### Table: `transaction` | Column | Type | Description | | :-------------------------------------------- | :------------ | :------------------------------------------------------------------------------------------------- | | **id** | `uuid` | The unique identifier of the transaction. | | **organizationId** | `uuid` | The unique identifier of the Organization. | | **ledgerId** | `uuid` | The unique identifier of the Ledger. | | **description** | `text` | Description of the transaction. | | **route** | `text` | **\[Deprecated]** Legacy route identifier, kept for backward compatibility. Use `routeId` instead. | | **routeId** | `uuid` | Identifier of the route used by the transaction (the current route field). | | **status** | `jsonb` | Information about the status. | | **amount** | `text` | The sent amount. | | **assetCode** | `text` | The code used to refer to the Asset. | | **chartOfAccountsGroupName** | `text` | The name of the chart of accounts group used to categorize the operations of a transaction. | | **source** | `array` | The list of accounts used as source. | | **destination** | `array` | The list of accounts used as destination. | | **operations** | `array` | The list of operations in the transaction. | | **metadata** | `jsonb` | Key-value pairs to add as metadata. | | **createdAt** | `timestamptz` | Timestamp of creation (UTC). | | **updatedAt** | `timestamptz` | Timestamp of last update (UTC). | | **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). | **Deprecated field** — This field remains for backward compatibility. Do not use it. ## Data model *** *Figure 1* shows the core Midaz tables and their relationships. The diagram helps developers and database administrators see how entities connect within the system. Use it to guide data handling and integration. Core Midaz database tables and the relationships between them, as a reference for data handling and integration # Event publisher Source: https://docs.lerian.studio/en/midaz/event-publisher Enable the Midaz event publisher to stream approved, pending, canceled, and reversal transaction events to a RabbitMQ exchange for downstream integrations. ## Why this matters *** Published events let your systems react to a transaction the moment it happens. You can trigger customer notifications, sync your ERP, feed analytics dashboards, or start compliance workflows. Your systems stay loosely coupled. The sections below cover the technical setup. For a business overview, see [About Midaz](/en/midaz/about-midaz). ## Enabling transaction events *** Midaz publishes transaction events **by default**. It treats the flag as enabled unless you set it to `false`. The bundled example configuration ships the flag set to `false`. On a stack that starts from that example, set the flag to `true` in the transaction application: ```bash JSON theme={null} RABBITMQ_TRANSACTION_EVENTS_ENABLED=true ``` With the flag enabled, Midaz publishes events to this RabbitMQ exchange: ```bash JSON theme={null} transaction.transaction_events.exchange ``` ## Event types *** Midaz emits one of these event types, based on the transaction lifecycle stage: | Action | Description | | :--------- | :-------------------------------------------------------------------------------------------------------------------------------------- | | `APPROVED` | The transaction was successfully completed. This includes single-step transactions and two-phase transactions that have been committed. | | `PENDING` | A two-phase transaction was created and is waiting for either a commit or cancellation. | | `CANCELED` | A two-phase transaction was canceled before confirmation. | | `CREATED` | A reversal transaction was initiated. This is a transient status that progresses to `APPROVED` once processing completes. | | `NOTED` | An annotation transaction was recorded. The transaction is logged in the ledger without affecting account balances. | ## Example event payload *** ```json JSON expandable theme={null} { "source": "midaz", "eventType": "transaction", "action": "APPROVED", "timestamp": "0000-00-00T18:09:03.757330233Z", "version": "v3.0.0", "organizationId": "0198575d-f9fd-702b-bb15-fa4c980b32c7", "ledgerId": "0198575d-fa0b-7ac7-8b7d-9d3ab7dccafc", "payload": { "id": "0198575f-a8f9-7924-a6d7-8122f2c77ddd", "status": { "code": "APPROVED", "description": "APPROVED" }, "amount": "1", "assetCode": "BRL", "source": ["account:1"], "destination": ["account:2"], "metadata": { "key": "value" }, "operations": [ { "type": "DEBIT", "amount": { "value": "1" }, "accountAlias": "account:1" }, { "type": "CREDIT", "amount": { "value": "1" }, "accountAlias": "account:2" } ] } } ``` The full payload includes timestamps, balance snapshots, and other identifiers. You use these for audit and traceability. ## Event routing model *** Midaz uses a topic exchange to publish messages. It does not send messages directly to specific queues. You control which events you receive with your own bindings. ### How routing works Midaz tags each event with a routingKey. The routingKey uses this format: ``` midaz.transaction. ``` The `` value is the current transaction status: `APPROVED`, `PENDING`, `CANCELED`, `CREATED`, or `NOTED`. To consume events, your application must: **Create a queue** in RabbitMQ. **Bind your queue** to the Midaz exchange with a routingKey pattern for the events you want. ### Visual overview Midaz event routing model, showing how published events reach subscriber queues bound to the exchange by routing-key patterns You can configure multiple queues with different bindings to serve specific teams or services independently. ## Queue and binding example *** ### Creating a new queue ```json JSON theme={null} { "queues": [ { "name": "new_queue_name.queue", "vhost": "/", "durable": true } ] } ``` ### Binding the queue to receive all events ```json JSON theme={null} { "bindings": [ { "source": "transaction.transaction_events.exchange", "vhost": "/", "destination": "new_queue_name.queue", "destination_type": "queue", "routing_key": "midaz.transaction.*" } ] } ``` The wildcard `*` matches all five statuses: `APPROVED`, `PENDING`, `CANCELED`, `CREATED`, and `NOTED`. To subscribe to one event type, replace the wildcard with the exact status. For example, `midaz.transaction.NOTED` receives only annotation events. Midaz does not manage or create RabbitMQ queues for you. You must provision the queues and set up the correct bindings. # Foundation Source: https://docs.lerian.studio/en/midaz/foundation-overview Meet the foundation layer of Midaz — Organizations, Ledgers, and Assets — the structural building blocks that all Accounts, Balances, and Transactions rely on. The foundation layer defines the structural building blocks of your financial data in Midaz. Organizations, Ledgers, and Assets form the base on which all accounts, balances, and transactions operate. ## Content *** Here, you'll find: * [**Organizations**](/en/midaz/organizations): The top-level entity that represents a financial institution. Organizations govern all financial data. They can also establish parent-child hierarchies for structures like subsidiaries, regional branches, or business units. * [**Ledgers**](/en/midaz/ledgers): The financial backbone of an Organization. Ledgers track all transactions and operations. This gives you complete traceability and control. Organizations can use multiple Ledgers to segregate financial operations by business line, geography, or regulatory requirement. * [**Assets**](/en/midaz/assets): Define the units of value your system works with — currencies like BRL and USD, cryptocurrencies, loyalty points, or tokenized securities. Each Account uses a single Asset. Midaz supports multi-asset operations within one system. # Initiating with Enterprise Model Source: https://docs.lerian.studio/en/midaz/initiating-with-enterprise-model Set up Lerian Enterprise in the right order to onboard product, engineering, and ops teams across Midaz, Access Manager, and plugins. This guide is for **Enterprise Model** users with access to **Midaz** and **Access Manager**. It also shows how to configure the **Fees Engine** and **CRM** capabilities that ship inside Midaz, and how to add **Reporter** — in the most effective order. ## Getting started: Setup order *** For clean, modular integrations, follow this setup order: Recommended setup order for the Enterprise Model, sequencing Midaz, Access Manager, and the optional Fees, CRM, and Reporter capabilities ### Step 1: Configure Midaz Core Start with the [Midaz recommended workflow](/en/midaz/recommended-workflow). It covers these tasks: * Create your Organization and Ledger(s). * Define assets and account types. * Set up portfolios, accounts, and segments. * Configure Accounting Routes for validation. * Build double-entry transaction workflows. ### Step 2: Activate Access Manager **Access Manager** centralizes authentication and role-based access control (RBAC) for Midaz and its plugins. * Define user roles and scopes. * Assign permissions to API keys or users. * Control access to operations by role. Start with the [Access Manager Setup](/en/platform/access-manager/access-manager) page. ### Step 3: Configure CRM **CRM** ships inside the Midaz ledger process and manages `Holder` and `Instrument` records. Instruments link Holders to ledger accounts, and accounts can optionally belong to portfolios. * Register customers as `Holder` objects. * Create Instruments to link Holders to ledger accounts, or use `POST /v1/organizations/{organization_id}/holders/{id}/accounts` to open a holder-owned account in one call. * Add metadata for compliance, segmentation, and insights. This links each customer to the right ledger accounts. Start with the [CRM Setup](/en/midaz/crm/crm-overview) page. ### Step 4: Configure Fees Engine Use the **Fees Engine** to automate pricing and monetization rules across your operations. * Define fee packages with flat or percentage calculations. * Attach fees to operations like transfers or withdrawals. * Control when and how the Fees Engine calculates and charges each fee. Start with the [Fees Engine Setup](/en/midaz/fees/fees-engine-overview) page. ### Step 5: Design with Reporter templates Use **Reporter** and its template engine to generate dynamic, personalized content such as: * Transaction summaries * Regulatory documents * Customer notifications Use it to standardize documents and messages with context data from Midaz and CRM. Start with the [Reporter quick start](/en/reporter/reporter-quick-start) page. ## Final recommendations *** After you set up all systems: * Review the integration points between Midaz and each capability. * Create automated workflows that combine ledger events, customer data, and fee logic. * Apply permission rules with Access Manager to control access at scale. Lerian capabilities are modular. Activate and expand them progressively as your architecture matures. # Ledgers Source: https://docs.lerian.studio/en/midaz/ledgers Use Ledgers as the financial backbone of each Organization, tracking Transactions and Operations and enabling orchestrated cross-ledger transfers via bridges. A **Ledger** is the Organization's financial backbone. It keeps a precise record of all Transactions and Operations. A Ledger tracks every financial event: deposits, withdrawals, transfers, and fees. This gives you complete traceability and control. Organizations can use **multiple Ledgers** to segregate financial operations. For example, a bank keeps separate Ledgers for different business lines, regions, or regulatory requirements. A single Ledger can also handle all operations for a simpler setup. ## Ledger structure *** * **Ledger > Organization:** Each Organization can own multiple Ledgers, but each Ledger belongs to only one Organization. This gives you financial separation and accountability. * **Use multiple Ledgers only when you need to segregate data or operations** — for example, internal treasury operations versus customer accounts. Transactions **cannot** move directly between Ledgers. To move funds across Ledgers, orchestrate the transfer through a workflow that keeps both sides consistent. ### How to orchestrate cross-Ledger transactions To move funds between two Ledgers, create an orchestrated workflow. The workflow breaks the transfer into two separate transactions: 1. **Debit from the source Ledger:** Transfer the amount from an account in **Ledger 1** to an **External Account**. This account bridges the two Ledgers and always belongs to one Asset. 2. **Credit to the destination Ledger:** Create a second transaction. It moves the same amount from the **External Account** into the target account in **Ledger 2**. In this two-step process, each Ledger processes a valid, independent transaction. The External Account keeps both sides consistent. You can use [Flowker](/en/flowker/what-is-flowker) to automate this orchestration. Flowker can add validations, conditions, and rollback logic if you need them. ## Key characteristics *** * Ledgers ensure financial integrity and operational transparency. * All Accounts and Transactions exist within a Ledger and form a complete financial system. * Each Ledger keeps a balanced set of Accounts. * You can use multiple Ledgers for segmentation, but cross-ledger transactions need explicit handling. ## Multiple Ledgers *** Midaz lets you create **multiple Ledgers** for each organization, with more flexibility and control. This helps companies with diverse operational needs. For example, they can separate financial records by country, department, or business function. ### Example use case Consider a global enterprise with business in multiple regions: * Ledger 1: US Business * Ledger 2: EU Business * Ledger 3: APAC Business With separate Ledgers, the company keeps region-specific transaction records, tax rules, and currency settings. It still has unified oversight of all operations. ### When to create a new Ledger A Ledger is a **hard boundary**, not just a label. Accounts in different Ledgers cannot transact directly. You must orchestrate every cross-Ledger movement through an External Account (see [How to orchestrate cross-Ledger transactions](#how-to-orchestrate-cross-ledger-transactions)). This cost drives the decision. Create a new Ledger only when the segments rarely move money between each other. Use this heuristic to pick the right level of separation: | Mechanism | What it separates | Reach for it when | | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Ledger** | A complete, self-contained set of books — its own accounts, balances, and asset configuration | You need true isolation: a separate legal entity, country, or regulatory regime, where balances should never mix and cross-segment movements are rare. | | **[Portfolio](/en/midaz/portfolios)** | A grouping of accounts *within* one Ledger | You want a consolidated view of a customer or business unit, but the accounts still transact freely with the rest of the Ledger. | | **[Account](/en/midaz/accounts) / [Account Type](/en/midaz/account-types)** | An individual balance and its rules | A single balance needs its own truth — its own statement, nature, or restrictions. | In short: if two sets of balances often transact with each other, keep them in the **same** Ledger. Separate them with Portfolios or Accounts. Split into multiple Ledgers only when the isolation is worth the orchestration overhead. ## Customizing Ledgers *** In Midaz, you can tailor each Ledger's configuration to your requirements. Assets are scoped to a Ledger, so the same code is an independent record in each Ledger with its own name, type, status, and metadata. **Example** To represent Bitcoin (BTC) across Ledgers, create the BTC asset independently in each Ledger and set the name, status, and metadata each Ledger needs. Amounts in Midaz are arbitrary-precision decimals. There is no per-asset or per-Ledger decimal-place setting — you do not configure a scale when you create an asset. This approach gives you granular control over asset management for your operational or regulatory needs. ## Benefits of using Ledgers in Midaz *** * **Clear organization**: Keep clear, distinct financial records for each part of your business. * **Custom configurations**: Adjust asset settings, currencies, or other parameters per Ledger for local or departmental needs. * **Scalability**: Add or change Ledgers as your organization grows or diversifies. * **Transparency**: Get a complete view across all financial activities. Each Ledger adds to one operational picture. ## Ledger settings *** Each Ledger has configurable settings that control how it validates transactions. For example, you can require every transaction to follow a predefined route. You can also require account types to match your business rules. The Ledger then rejects invalid movements before it records them. You can change these settings at any time through the API. You do not need to restart the service or change code. ### Available settings The following settings are available: | Setting | Default | Description | | --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `validateAccountType` | `false` | When enabled, each new account must use an account type registered for the Ledger. The Ledger rejects the account creation if the type is not registered. | | `validateRoutes` | `false` | When enabled, every transaction must include an associated transaction route. Each operation is also validated against its operation route rules. | Start with both settings disabled while you configure your operation routes and transaction routes. Once your routing setup is complete, enable validation to enforce your business rules. ### How settings affect transactions When you enable `validateRoutes` for a Ledger: 1. Every transaction to that Ledger **must** reference a valid transaction route. 2. The Ledger validates each operation against the corresponding operation route rules. 3. The Ledger rejects any non-compliant transaction before it records the movement. This protects your ledger integrity. When you enable `validateAccountType`: 1. The Ledger checks each new account against its registered account types. 2. If the account type is not registered, the Ledger rejects the account creation. If you enable validation on a Ledger that already processes transactions, existing records do not change. Validation applies only to **new transactions** that you submit after you enable the setting. ### Managing settings via API Use the Ledger Settings endpoints to retrieve and update these configurations: * [Retrieve Ledger Settings](/en/reference/midaz/retrieve-ledger-settings) — View the current settings for a Ledger. * [Update Ledger Settings](/en/reference/midaz/update-ledger-settings) — Change one or more settings with a partial update (deep merge). Settings updates take effect immediately. Midaz applies changes with a deep merge and modifies only the fields you include in the request. ## Managing Ledgers *** You can manage your Ledgers through the API or Lerian Console. ### View API * [List Ledgers](/en/reference/midaz/list-ledgers) - View the details of all Ledgers. * [Retrieve a Ledger](/en/reference/midaz/retrieve-a-ledger) - Get details of a specific Ledger. * [Create a Ledger](/en/reference/midaz/create-a-ledger) - Register a new Ledger. * [Update a Ledger](/en/reference/midaz/update-a-ledger) - Edit the details of an existing Ledger. * [Delete a Ledger](/en/reference/midaz/delete-a-ledger) - Mark a Ledger as deleted. When you delete a Ledger, Midaz does not remove it permanently. Instead, Midaz sets a `deletedAt` timestamp. The Ledger is then excluded from every read and can no longer be used. Its `status.code` is left unchanged. This preserves historical data for audit trails. ### Via Lerian Console You can view, create, edit, and delete Ledgers on the Ledgers page in the Midaz Module of Lerian Console. [**Learn more in the Managing Ledgers guide.**](/en/midaz/console/managing-ledgers-via-console) If you create your **first Ledger** in Lerian Console, you do it as part of the onboarding flow. To learn more, see the [Midaz Module onboarding](/en/midaz/console/midaz-console-onboarding) guide. # Metadata indexes Source: https://docs.lerian.studio/en/midaz/metadata-indexes Create MongoDB metadata indexes on transactions, operations, and routes to speed up queries and filters on custom metadata keys in high-volume Midaz setups. ## Why this matters *** Every entity in Midaz supports [metadata](/en/reference/metadata) — custom key-value pairs that extend the standard data model. Queries that filter a large collection by a metadata field can become slow without an index. A metadata index is a MongoDB index on a specific metadata key. It turns an expensive collection scan into a fast indexed lookup. This matters most in production, where transaction volumes are high and you filter or sort by metadata values. ## How it works *** When you create a metadata index, Midaz builds a MongoDB index on the `metadata.` field of the entity collection. After that, any query that filters by the metadata key uses the index. MongoDB finds the documents directly, without a full collection scan. Indexes are: * **Per-entity**: Each index targets a specific entity type (e.g., `transaction`, `operation`). * **Per-key**: Each index covers a single metadata key. * **Optional uniqueness**: You can require that no two documents share the same value for the indexed metadata key. * **Sparse by default**: The index includes only documents that have the metadata key. This saves storage and speeds up writes. ## Supported entity types *** Midaz supports metadata indexes for these entities: | Entity | Collection | Module | | :------------------ | :----------------- | :---------- | | `transaction` | Transactions | Transaction | | `operation` | Operations | Transaction | | `operation_route` | Operation Routes | Transaction | | `transaction_route` | Transaction Routes | Transaction | | `organization` | Organizations | Onboarding | | `ledger` | Ledgers | Onboarding | | `account` | Accounts | Onboarding | | `asset` | Assets | Onboarding | | `segment` | Segments | Onboarding | | `portfolio` | Portfolios | Onboarding | | `account_type` | Account Types | Onboarding | ## Creating a metadata index *** Use the [Create a Metadata Index](/en/reference/midaz/create-a-metadata-index) endpoint: ```json POST /v1/settings/metadata-indexes/entities/{entity_name} theme={null} { "metadataKey": "tier", "unique": false, "sparse": true } ``` **Parameters:** | Field | Type | Required | Description | | :------------ | :------ | :------- | :-------------------------------------------------------------------------------------------------------------------------------- | | `metadataKey` | string | Yes | The metadata key to index. Must start with a letter and contain only alphanumeric characters and underscores. Max 100 characters. | | `unique` | boolean | No | Whether the index enforces uniqueness across documents. Default: `false`. | | `sparse` | boolean | No | Whether the index only includes documents that have the metadata key. Default: `true`. | **Response:** ```json JSON theme={null} { "indexName": "metadata.tier_1", "entityName": "transaction", "metadataKey": "tier", "unique": false, "sparse": true } ``` ## Listing metadata indexes *** Use the [List Metadata Indexes](/en/reference/midaz/list-metadata-indexes) endpoint. It returns all indexes across all entity types with their usage statistics: ```json GET /v1/settings/metadata-indexes theme={null} [ { "indexName": "metadata.tier_1", "entityName": "transaction", "metadataKey": "tier", "unique": false, "sparse": true, "stats": { "accesses": 1523, "statsSince": "2024-12-01T10:30:00Z" } } ] ``` The `stats.accesses` field shows how many queries used the index since statistics collection started. Use it to find unused indexes that you can safely delete. ## Deleting a metadata index *** Use the [Delete a Metadata Index](/en/reference/midaz/delete-a-metadata-index) endpoint: ``` DELETE /v1/settings/metadata-indexes/entities/{entity_name}/key/{index_key} ``` Deletion takes effect immediately. It affects query performance for any operation that used the index. Before you delete an index, make sure no critical query depends on it. ## Performance considerations *** **When to create indexes:** * You frequently filter transactions or operations by a specific metadata key (e.g., `tier`, `channel`, `partner_id`). * List queries on a large collection are slow when they filter by metadata. * You need to enforce uniqueness on a metadata field (e.g., external reference IDs). **When NOT to create indexes:** * You rarely query by the metadata key — the index only costs storage and slows writes. * The collection is small enough that full scans are fast. * You want to add an index speculatively, "just in case." **Limits:** Midaz does not enforce its own cap on the number of metadata indexes per entity — MongoDB's per-collection index limit applies instead. Creating an index on a key that already has one returns error `0132` (Metadata Index Already Exists). Keep the index count deliberate: list the indexes and delete any with low or zero `accesses`. Start with indexes on the metadata keys you query in production. Use the `stats.accesses` field from the list endpoint to make sure queries use each index. Delete the indexes that no query uses. ## Related pages *** * [Metadata](/en/reference/metadata) — How metadata works across all Midaz entities. * [Create a Metadata Index](/en/reference/midaz/create-a-metadata-index) — API reference. * [List Metadata Indexes](/en/reference/midaz/list-metadata-indexes) — API reference. * [Delete a Metadata Index](/en/reference/midaz/delete-a-metadata-index) — API reference. # Midaz for academic records Source: https://docs.lerian.studio/en/midaz/midaz-for-academic-record-management Use Midaz to record enrollments, grades, and achievements as immutable Ledger transactions — automated transcripts and audit-ready histories. Universities manage thousands of student records across many departments. The data sits in separate enrollment systems, grading platforms, and administrative databases. When an audit starts or a student requests a transcript, staff rebuild a complete history by hand. This process is slow and error-prone. Midaz treats each academic activity as a Ledger transaction. It records every enrollment, grade, and achievement immutably, with a full audit trail — the same way it records financial movements. ### Benefits * **Security and integrity**: Every academic record is an immutable transaction. You cannot change it after you record it, which removes disputes about grades or enrollment history. * **Transparency**: Students, advisors, and administrators see a single, consistent view of the complete academic record. * **Efficiency**: Transcript generation, GPA calculations, and audit preparation become automated queries instead of manual processes. ## What changes with Midaz *** | Without Midaz | With Midaz | | :------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | | Student records spread across enrollment, grading, and admin systems | Single ledger tracks all academic activities as immutable transactions | | Transcript generation requires manual cross-referencing | Query a student's portfolio for a complete, auditable academic history | | Grade disputes involve digging through logs and email chains | Every grade assignment is a timestamped, immutable transaction with full audit trail | | GPA calculations done manually or in disconnected spreadsheets | Balance queries on grade accounts calculate GPA automatically | | Audit preparation takes weeks of data gathering | All records are audit-ready by design — query any time range instantly | ## Using Midaz *** * **Organization & Ledger:** The university registers as an Organization in Midaz. It creates a Ledger to track all academic activities as transactions. * **Assets:** Create an Asset for each course, with a unique identifier. * **Student Portfolios:** For each student, create a Portfolio. The Portfolio acts as the academic record and holds all relevant academic metrics. * **Accounts for academic metrics:** Create accounts for the academic metrics inside each student's Portfolio. Examples include: * **Course enrollments Account**: Records the courses the student enrolls in. * **Grades Account**: Tracks the grades the student receives. * **Attendance Account**: Monitors class attendance. * **Achievements Account**: Records extracurricular achievements and recognitions. * **Transactions**: Use transactions to update accounts based on academic activities: * **Course enrollment**: When a student enrolls in a course, create a transaction. The transaction moves an entry from a course pool account (e.g., `@external/available-courses`) to the student's "Course Enrollments" account. * **Grade assignment**: When an instructor assigns a grade, create a transaction. The transaction moves "funds" (the grades) from the class grade account (e.g., `@external/grades`) to the student's grade account. * **Attendance marking**: To mark attendance, move funds from an attendance account (e.g., `@external/monitors`) to the student's "Attendance" account. * **Achievements**: Similarly, add achievements to the "Achievements" account via transactions. * **Reports**: Query the ledger to build a full academic record for each student, across enrollments, grades, attendance, and achievements. These queries support academic counseling and transcript generation. * For example, retrieve all transactions for the Grades Account. Use the available balance to calculate the student's GPA. # Midaz for multi-account customers Source: https://docs.lerian.studio/en/midaz/midaz-for-multi-account-customers Model multiple Midaz accounts under a single customer — with segregated balances, ledgers, statements, and external identifiers per account. A single customer rarely has a single balance. The same person can hold a main account, a benefit account, a blocked account, a product sub-account, or a promotional balance. Each balance has its own rules, its own statement, and its own reconciliation needs. The common shortcut treats these as labels on one account and sorts them out in application code. That works until balances, ledgers, statements, or operational rules must diverge. At that point, a single account can no longer tell the truth about where the money is. This page shows the reference architecture for **one customer with many Midaz accounts**. It maps each part of the model to a native platform entity. Then it walks through an example: three accounts for the same customer document. ## Why this matters *** For **product and operations teams**, each balance becomes its own account. The Ledger then enforces every segregation rule — a blocked outflow, a benefit-only spend, a promotional balance with an expiry. The rule lives in the Ledger, not in application logic. Each account carries its own statement and reconciliation trail. For **engineering teams**, each external address resolves to a specific account before Midaz posts a transaction. Core banking numbers and payment-rail identifiers each point to one balance. You never guess which balance an incoming event belongs to. There is no separate balance store to keep in sync with the Ledger. | One account with labels | Many accounts per customer | | ------------------------------------------------------- | ----------------------------------------------------------------- | | Balance "types" live in metadata or app code | Each balance is its own Account with its own ledger and statement | | Blocking or restricting funds requires custom logic | Account Type and route rules enforce restrictions at the Ledger | | Statements must be filtered and reassembled per balance | Each account produces a clean, independent statement | | External identifiers all point to the same balance | Each external identifier resolves to a specific account | | Reconciliation mixes unrelated movements | Reconciliation is separated by account by design | ## The reference architecture *** The model is **one owner, N accounts, N external identifiers**: * **One owner** — the customer, identified by a document (CPF, CNPJ, tax ID). The owner represents *who* holds the relationship. It does not carry a balance and does not decide transaction routing. * **N accounts** — each account is a self-contained accounting position, with its own balance, ledger, statement, and rules. * **N external identifiers** — the addresses other systems (a core banking platform, a payment rail) use to reach a specific account. Each identifier resolves to exactly one account. A middleware layer keeps the map between external identifiers and accounts. It resolves each identifier to the correct account *before* the call to Midaz. Midaz remains the source of truth for accounts, balances, and entries. One customer, many accounts — reference architecture When balance, ledger, statement, or operational rule differs by destination, point each identifier at a distinct account. An external identifier is **not** just a nickname for one shared balance. ## How Midaz maps the model *** Every part of this architecture maps to a native Midaz entity. You do not build a separate balance store or invent an accounting layer. | Concept | Midaz entity | What it does | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | The owner (customer) | [**Holder**](/en/midaz/crm/holders) | Identity behind the accounts (`NATURAL_PERSON` or `LEGAL_PERSON`), keyed by `document`. Holds no balance. | | Each balance position | [**Account**](/en/midaz/accounts) | Source of truth for balance, entries, and statement. | | The nature of each balance | [**Account Type**](/en/midaz/account-types) | Classifies an account (main, benefit, blocked) and enables route validation. | | All accounts of one customer | [**Portfolio**](/en/midaz/portfolios) | Groups a customer's accounts to view the total relationship. | | External address → account | [**Account alias**](/en/midaz/accounts#account-aliases) + [**`entityId`**](/en/midaz/accounts#entity-id-external-system-reference) | The alias is how transactions address an account; `entityId` links it to an external system's identifier. | | Banking and regulatory context | [**Instrument (CRM)**](/en/midaz/crm/crm-getting-started) | Attaches branch, account number, and regulatory fields. Links a Holder to a specific account. | Much of what an external "alias registry" would do is already native. The account **alias** is the in-Ledger address. **`entityId`** stores your external system's identifier. The middleware's job is narrow — translate an external rail identifier into the right account alias, then post. ## Prerequisites *** This example assumes a running Midaz environment with the following in place: | Requirement | Details | | --------------------------- | -------------------------------------------------------------------------------------- | | **Midaz** (v3.x.x+) | Core Ledger with an Organization and Ledger already created | | **A registered asset** | `BRL` registered as the operating asset in the Ledger | | **Account Type validation** | Enabled per-Ledger so each account's nature is enforced (see Step 1) | | **CRM** (optional) | Part of the ledger binary — use it to attach identity, banking, and regulatory context | Midaz represents values in the smallest unit of the currency. For BRL, `15000` means R\$ 150.00 (centavos). ## Building three accounts for one customer *** The customer with document `12345678900` needs three accounts. The **main** account is free for ordinary movement. The **benefit** account follows product rules. The **blocked** account accepts inflows but restricts outflows. Turn on validation so every account must declare a registered type. This is what lets the Ledger enforce each account's nature. ```json theme={null} PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings { "accounting": { "validateAccountType": true } } ``` Settings take effect immediately — no redeployment needed. Create one Account Type per balance nature. The `keyValue` is what each account's `type` field must match. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/account-types { "name": "Main Account", "description": "Ordinary account, free for regular movement", "keyValue": "main_account" } ``` Repeat for `benefit_account` (movement under product rules) and `restricted_account`. The `restricted_account` is the **blocked** account: it accepts inflows but conditions or blocks outflows. Each account links to the `BRL` asset and declares its `type`. It carries an `alias` (its in-Ledger address) and an `entityId` (its identifier in your external system). ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/accounts { "name": "Main account — 12345678900", "assetCode": "BRL", "alias": "@cust_12345678900_main", "entityId": "0001/12345-1", "type": "main_account" } ``` Create the benefit account with `alias` `@cust_12345678900_benefit`, `entityId` `0001/88888-2`, and `type` `benefit_account`. Create the blocked account with `alias` `@cust_12345678900_blocked`, `entityId` `0002/77777-0`, and `type` `restricted_account`. The `entityId` is where you store the external address that other systems use to reach this account — your map between Midaz and your source system. Create one Holder for the customer. The same Holder owns all three accounts and keeps identity in one place. In Midaz v4, CRM is part of the ledger binary, so it needs no separate service or port. The organization ID travels in the URL path — see [Getting started with CRM](/en/midaz/crm/crm-getting-started) for the full schema. ```bash theme={null} curl -X POST http://localhost:3002/v1/organizations/{org_id}/holders \ -H "Content-Type: application/json" \ -d '{ "type": "NATURAL_PERSON", "name": "Jane Smith", "document": "12345678900", "contact": { "primaryEmail": "jane.smith@example.com" } }' ``` Save the returned `holderId` — you'll use it in the next step. Create one Instrument per ledger account to attach banking and regulatory context. The Instrument powers CRM-driven features and keeps customer-facing details out of the Ledger. ```bash theme={null} curl -X POST http://localhost:3002/v1/organizations/{org_id}/holders/{holder_id}/instruments \ -H "Content-Type: application/json" \ -d '{ "ledgerId": "", "accountId": "", "bankingDetails": { "branch": "0001", "account": "12345", "type": "CACC", "countryCode": "BR" }, "metadata": { "purpose": "main" } }' ``` Notice how the main account's `entityId` (`0001/12345-1`) decomposes into the `branch` (`0001`) and `account` (`12345`) you record here. That external address now resolves to one specific account. Repeat for the benefit and blocked accounts, and point `accountId` at each one. The result: one customer, three accounts, three distinct external addresses. Each account keeps its own balance, statement, and rules under a single Holder. | Owner | External identifier | Midaz account | Use | Treatment | | ------------- | ------------------- | ------------------- | ----------------------- | ---------------------------------------------- | | `12345678900` | `0001/12345-1` | `@cust_..._main` | Main account | Free for ordinary movement | | `12345678900` | `0001/88888-2` | `@cust_..._benefit` | Benefit account | Movement governed by product rules | | `12345678900` | `0002/77777-0` | `@cust_..._blocked` | Court-ordered / blocked | Inflow allowed, outflow conditioned or blocked | ## The transaction boundary *** When an external event arrives, the resolution happens *before* the call to Midaz. Each layer stays in its lane, and that preserves accounting clarity. A transaction, query, or settlement arrives with an external identifier. The middleware looks up the external identifier and resolves it to the correct Midaz account alias. It also applies status validation (active, blocked, closed). Midaz records the entry against the resolved account and preserves balance and ledger. Statement and reconciliation stay separate by account. | Layer | Responsibility | Should not do | | ---------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | **CRM / registration** | Hold the commercial and identity view of the customer, including the link between document and relationship. | Transaction routing, destination decisions, or settlement rules. | | **Middleware** | Resolve external identifiers to a Midaz account before the transaction, and apply status validation. | Invent balances, duplicate accounting, or depend on the CRM in real time for routing. | | **Midaz** | Record accounts, balances, entries, ledgers, and statements as the financial source of truth. | Know external-rail details beyond the identifiers needed for integration. | An identifier for a blocked or closed account must fail validation **before** the call to Midaz. Routing decisions belong in the middleware. The Ledger stays the source of truth for balances and entries. ## What this unlocks *** * **Real segregation** — each balance has its own ledger and statement. You cannot spend a blocked balance through the main account by accident. * **Unambiguous routing** — every external event has a single, well-defined destination account. * **Centralized identity** — one Holder owns many accounts. Identity and contact data live in one place while balances stay separate. * **Native, not bolted-on** — accounts, types, aliases, and `entityId` are platform primitives, so there's no parallel balance store to reconcile against the Ledger. ## What you need to get started *** | Requirement | Details | | --------------------------- | ----------------------------------------------------------------------------------- | | **Midaz** (v3.x.x+) | Organization, Ledger, and a registered asset | | **Account Type validation** | Enabled per-Ledger via the [Ledger Settings API](/en/midaz/ledgers#ledger-settings) | | **Account Types** | One per balance nature (main, benefit, blocked, …) | | **Accounts** | One per balance, each with an `alias` and an `entityId` | | **CRM** (optional) | A Holder per customer plus an Instrument per ledger account | ## Next steps *** The core financial unit — aliases, `entityId`, and external accounts. Classify accounts and enforce their nature with route validation. Group a customer's accounts to view the total relationship. Centralize identity and attach banking and regulatory context. # Midaz for supply chains Source: https://docs.lerian.studio/en/midaz/midaz-for-supply-chain-management Use Midaz as a ledger for supply chain management, recording every raw material, production, and shipping movement as an immutable, auditable transaction. Manufacturing companies manage complex material flows. Raw materials arrive from many suppliers, move through production stages, become finished goods, and ship to customers. A quality issue or a regulatory audit raises one question. Where did this material come from, and where did it go? Today that answer takes days of cross-referencing spreadsheets and ERP exports. Midaz records every material movement as an immutable ledger transaction. From receiving to production to shipping, you get a complete and auditable history of your supply chain. ### Benefits * **Transparency and traceability**: An immutable audit trail documents every step. Recall investigations and compliance audits become straightforward. * **Efficiency**: Real-time balance queries show inventory levels and production stages. You avoid manual stock checks and planning delays. * **Security**: Tamper-proof records block unauthorized changes to supply chain data. Your regulatory reporting stays accurate. ## What changes with Midaz *** | Without Midaz | With Midaz | | :-------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- | | Material tracking spread across ERP modules, spreadsheets, and emails | Every material movement is a ledger transaction with full audit trail | | Recall investigations take days of cross-referencing multiple systems | Query the ledger for the complete journey of any material — instant traceability | | Inventory counts done manually or through periodic reconciliation | Real-time balance queries show current stock at any stage (raw, in-production, finished) | | Supplier disputes lack authoritative records | Immutable transactions provide tamper-proof evidence of every delivery and movement | | Regulatory audits require weeks of data preparation | All records are audit-ready by design — export any time range on demand | ## Using Midaz *** * **Assets**: Set up each supply chain item as an Asset with a unique code. Assets cover everything from raw materials to final products. * **Portfolios**: Create a portfolio for each supplier and warehouse. A portfolio groups the accounts that track material and goods flow. * **Accounts**: Within each portfolio, create an account for each supply chain stage. For example: * **Raw materials account**: Tracks raw materials held in the warehouse. * **In-production account**: Monitors materials in the production process. * **Finished goods account**: Records completed products ready for distribution. * **Shipment account**: Tracks items in transit to customers or other warehouses. * **Transactions**: Use a transaction to update accounts as materials move through each stage. For example: * **Receiving materials**: When materials arrive, a transaction moves the amount from the supplier's Raw Materials account to the warehouse's Raw Materials account. * **Production**: During production, a transaction moves the amount from the Raw Materials account to the In-Production account. * **Production completion**: When production finishes, a transaction moves the amount from the In-Production account to the Finished Goods account. * **Shipping**: When goods ship, a transaction moves the amount from the Finished Goods account to the Shipment account. * **Reports**: Query Midaz for current inventory levels, production output, and shipment status. Use the data to plan stock, production schedules, and logistics. * **Compliance and audits**: The immutable Midaz ledger keeps supply chain records secure and audit-ready. It meets regulatory requirements and standards. # Getting started with Midaz Source: https://docs.lerian.studio/en/midaz/midaz-getting-started Run Midaz locally in about ten minutes: clone the repo, start infrastructure, create your first Organization, Ledger, Accounts, and process a first transaction. In this guide, you set up a working Midaz environment. You then run the core workflow behind any financial application on the platform. You create an organization, a ledger, and accounts, and then process your first transaction. By the end, you have a running ledger ready for your use case. This can be payments, lending, marketplace settlement, or internal treasury. ## Prerequisites *** Before you begin, install these tools: | Tool | Minimum version | Check command | | ---------------------------------------------------------- | --------------- | ------------------------ | | [Go](https://go.dev/dl/) | 1.26.4+ | `go version` | | [Docker](https://docs.docker.com/get-docker/) | 24+ | `docker --version` | | [Docker Compose](https://docs.docker.com/compose/install/) | 2.20+ | `docker compose version` | | [Make](https://www.gnu.org/software/make/) | 3.81+ | `make --version` | | [Git](https://git-scm.com/) | 2.30+ | `git --version` | Midaz runs on **macOS** (Apple Silicon and Intel) and **Linux** (amd64). On Windows, run it through **WSL2**. ## Step 1 — Clone the repository *** Clone the Midaz repository and move into the project directory. ```bash Terminal theme={null} git clone https://github.com/LerianStudio/midaz.git cd midaz ``` ## Step 2 — Set up environment files *** Midaz uses `.env` files to configure each component. Generate them from the provided examples: ```bash Terminal theme={null} make set-env ``` This command copies `.env.example` to `.env` in each component directory. The default values work for local development. You do not need to change them. Docker must already be running for this step: `make set-env` also generates the `LCRYPTO_*` CRM keys through a Docker one-shot container, and fails if Docker is unavailable. ## Step 3 — Start the infrastructure *** Start the supporting services that Midaz needs: PostgreSQL, MongoDB, Valkey, RabbitMQ, Redpanda, and OpenTelemetry. ```bash Terminal theme={null} make infra COMMAND=up ``` Wait until all containers report a healthy status. Check their status with: ```bash Terminal theme={null} docker compose -f components/infra/docker-compose.yml ps ``` Infrastructure services use the following default ports: | Service | Port | | -------------------- | ----- | | PostgreSQL Primary | 5701 | | PostgreSQL Replica | 5702 | | MongoDB | 5703 | | Valkey (Redis) | 5704 | | Grafana (OTEL) | 3100 | | RabbitMQ AMQP | 3003 | | RabbitMQ Management | 3004 | | Redpanda (Kafka API) | 19092 | ## Step 4 — Start Midaz *** Midaz runs as a single **Ledger** service that includes the onboarding and transaction domains. Start it with: ```bash Terminal theme={null} make up ``` This command starts the infrastructure, if needed, runs the ledger migration container, and then starts the Midaz services — the ledger and Tracer. All ledger APIs are available on **port 3002**. Check that Midaz responds: ```bash Terminal theme={null} curl http://localhost:3002/health ``` You should receive a `200 OK` response. ## Step 5 — Create an organization *** An organization represents the business entity behind the financial operation: your company, a client, or a regulated institution. In production, it maps to the legal entity that holds your ledgers, accounts, and transactions. For the complete endpoint specification, see [Create an Organization](/en/reference/midaz/create-an-organization). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v1/organizations \ -H "Content-Type: application/json" \ -d '{ "legalName": "Acme Corp", "legalDocument": "12345678000100", "status": { "code": "ACTIVE" }, "address": { "country": "BR" } }' ``` Save the `id` returned in the response. You use it in the next steps as `{organization_id}`. ## Step 6 — Create a ledger *** A ledger is an isolated book of records within an organization. You can create separate ledgers for different financial domains, such as payments, fee collection, or settlement. Each ledger has its own accounts and transaction history. For the complete endpoint specification, see [Create a Ledger](/en/reference/midaz/create-a-ledger). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers \ -H "Content-Type: application/json" \ -d '{ "name": "Primary Ledger", "status": { "code": "ACTIVE" } }' ``` Save the returned `id` as `{ledger_id}`. ## Step 7 — Create an asset *** An asset defines the unit of value tracked in the ledger. This can be a fiat currency like BRL or USD. It can also be loyalty points, crypto tokens, securities, or any custom unit your business tracks. You must create at least one asset before you create accounts. For the complete endpoint specification, see [Create an Asset](/en/reference/midaz/create-an-asset). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/assets \ -H "Content-Type: application/json" \ -d '{ "name": "Brazilian Real", "type": "currency", "code": "BRL", "status": { "code": "ACTIVE" } }' ``` ## Step 8 — Create accounts *** Accounts represent the participants or buckets in your financial flow: a customer wallet, a revenue pool, a merchant account, or an internal reserve. Each account links to a single asset and follows double-entry accounting rules. You need at least two accounts to process a transaction: one to debit (source) and one to credit (destination). For the complete endpoint specification, see [Create an Account](/en/reference/midaz/create-an-account). Create a source account: ```bash Terminal theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts \ -H "Content-Type: application/json" \ -d '{ "name": "Revenue Account", "assetCode": "BRL", "type": "deposit", "status": { "code": "ACTIVE" }, "alias": "@revenue" }' ``` Create a destination account: ```bash Terminal theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts \ -H "Content-Type: application/json" \ -d '{ "name": "Customer Account", "assetCode": "BRL", "type": "deposit", "status": { "code": "ACTIVE" }, "alias": "@customer-001" }' ``` ## Step 9 — Process your first transaction *** This is the core action: it moves value between accounts with full traceability. Midaz records every transaction as a balanced operation. It debits the source and credits the destination, so your books stay consistent by design. For the complete endpoint specification, see [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/json \ -H "Content-Type: application/json" \ -d '{ "description": "First transaction", "send": { "asset": "BRL", "value": "1000", "source": { "from": [ { "accountAlias": "@revenue", "amount": { "asset": "BRL", "value": "1000" } } ] }, "distribute": { "to": [ { "accountAlias": "@customer-001", "amount": { "asset": "BRL", "value": "1000" } } ] } } }' ``` This transaction sends **R\$ 10.00** from `@revenue` to `@customer-001`. The value `"1000"` represents 10.00 in BRL's smallest unit, cents. Midaz uses integer values to avoid floating-point errors. This is standard practice in financial systems. ## Step 10 — Verify the balance *** Check the destination account balance to confirm the transaction. For the complete endpoint specification, see [Retrieve a Balance by Account Alias](/en/reference/midaz/retrieve-a-balance-by-account-alias). ```bash Terminal theme={null} curl http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/alias/@customer-001/balances ``` The returned balance should reflect the credited amount. At this point, you have a working ledger that processes real transactions. ## Explore the API *** Midaz can serve its OpenAPI 3.1 spec and interactive API documentation. The docs surface is off by default. To enable it, set `OPENAPI_DOCS_ENABLED=true` in `components/ledger/.env` and restart Midaz. Then access: * **API docs**: `http://localhost:3002/v1/docs` * **OpenAPI spec**: `http://localhost:3002/v1/openapi.json` (or `/v1/openapi.yaml`) ## Observability *** Midaz ships with a preconfigured Grafana instance integrated with OpenTelemetry. * **Grafana dashboard**: `http://localhost:3100` * **Default credentials**: `midaz` / `lerian` From Grafana, you can explore logs, traces, and metrics across all Midaz services. ## Stopping Midaz *** To stop all services: ```bash Terminal theme={null} make down ``` To remove containers and volumes and start from a clean environment: ```bash Terminal theme={null} make clean-docker ``` ## Next steps *** New to Midaz? Start with [Midaz entities](/en/midaz/core-entities) to understand organizations, ledgers, accounts, and transactions. Learn the different ways to create transactions, including JSON, inflow, and outflow, and when to use each. Deploy Midaz to Kubernetes using the official Helm chart. Manage holders and alias accounts to connect real-world identities to your Midaz accounts. Add Fees Engine, Pix, and other capabilities to your Midaz deployment. # Midaz in a global portfolio Source: https://docs.lerian.studio/en/midaz/midaz-in-a-global-portfolio Model multi-currency customer relationships in Midaz — group BRL, USD, and EUR Accounts under a single Portfolio for a unified global banking view. With Midaz, you can build a global account structure like the ones major banks offer. Clients hold accounts in multiple currencies, such as BRL, USD, and EUR. You manage every currency account under a single client profile. ## What changes with Midaz *** | Without Midaz | With Midaz | | :---------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | | Multi-currency accounts require separate systems or complex workarounds | Each currency is an Asset; accounts in different currencies grouped under one Portfolio | | No unified customer view across currencies | A single portfolio shows all of a client's accounts — BRL, USD, EUR — in one place | | Currency-specific reporting built manually per system | Query all accounts under a portfolio via API for consolidated financial views | | Adding a new currency means provisioning new infrastructure | Create a new Asset and an account — the ledger handles the rest | ## Using Midaz *** * **Organization and Ledger:** ABC Bank registers as an Organization in Midaz. It then creates a Ledger to track all customer transactions. * **Assets:** The bank defines an Asset for each currency it supports. * **Customer Portfolios:** Each customer has a Portfolio. It groups their checking, savings, and credit accounts in one place. * **Customer Accounts:** Create an Account for each currency the customer uses. Group these accounts under the customer's Portfolio. ### Example: Maria’s international business needs Maria is an entrepreneur in Spain who runs an e-commerce business that sells handmade crafts worldwide. Her customers are in the U.S., Europe, and Japan, so she manages several currencies. She opened an account with **ABC Bank**, which uses Midaz to manage its transactions. # Midaz in banking Source: https://docs.lerian.studio/en/midaz/midaz-in-banking Discover how Midaz powers retail, corporate, and personalized banking through Portfolios, Segments, and dedicated settlement Accounts. Midaz supports three banking models: **retail banking**, **corporate banking**, and **personalized banking services**. It uses portfolios and segments to model each one. ## What changes with Midaz *** | Without Midaz | With Midaz | | :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------ | | Customer accounts scattered across legacy systems with no unified view | Single ledger with portfolios grouping all accounts per customer | | Adding new products (savings, credit) requires core system changes | New account types and segments configured via API — no code changes to the ledger | | Fee and interest tracking mixed with operational accounts | Dedicated internal accounts for fee income, interest expense, and settlement | | Segmentation (VIP, Student) enforced manually per product | Segments classify accounts, and the Fees Engine waives fees for a whole segment | | Corporate hierarchies require custom workarounds | Native child accounts and multi-portfolio support for subsidiaries | ## Driving innovation in banking *** Banks add products without changing core ledger structures. You configure new account types and segments through the API. ## Retail banking for individual customers *** A retail bank runs on a single organization and a primary ledger. Each customer maps to a portfolio. ### Using Midaz * **Organization & Ledger:** ABC Bank registers as an organization. It creates a "Retail Banking Ledger" for all customer transactions. * **Assets:** The bank defines an asset for every currency it operates. This gives multi-currency support for domestic and international accounts. * **Customer portfolios:** Each customer has a portfolio. It holds their checking, savings, and credit accounts. * **Transactions:** Midaz records deposits, transfers, and payments with full traceability. * **Internal accounts:** Fee and interest accounts track the bank's revenue and expenses. * **Segments for personalization:** You classify accounts into service tiers, such as Standard or Gold. The Fees Engine can waive fees for a tier. * **Child accounts for budgeting:** Customers create sub-accounts inside their savings portfolio for better money management. #### Example: ATM withdrawal John Doe withdraws \$100 from an ATM: \$100 from John’s checking account. \$100 to an "ATM Cash Dispensed" internal account. Midaz debits \$3 from John’s account and credits the Fee Income account. Midaz handles millions of retail customers. ## Corporate banking for business and enterprise customers *** Corporate banking needs a more detailed structure. It often uses separate ledgers or segments to keep it apart from retail banking. ### Using Midaz * **Dedicated Ledger:** ABC Bank creates a "Corporate Banking Ledger" for business accounts. * **Portfolios for enterprises:** Each corporate client, such as ACME Corp, gets a portfolio with accounts for operations, payroll, and taxes. * **Hierarchical accounts:** Child accounts let a business split funds across departments and keep one consolidated balance. * **Bulk transactions:** Midaz supports large operations, such as payroll with many simultaneous debits and credits. * **Segments & Products:** You classify companies as SME or Large Enterprise to offer tailored services. * **Multi-entity clients:** You give each subsidiary its own portfolio under the same organization. #### Example: loan disbursement & supplier payments ACME Corp receives a \$1,000,000 loan: \$1,000,000 to ACME’s loan account. \$1,000,000 from the bank’s Loans Receivable account. * \$100,000 payment to a supplier at ABC Bank (internal transfer). * \$50,000 payment to an external supplier (external transfer recorded in the clearing account). Midaz gives complete visibility into large operations. ## Portfolios and Segments for personalized services *** Banks offer custom financial products. Midaz supports this through portfolios and segments. ### Customization strategies * **Youth & student accounts:** You assign these accounts to a "Student" segment. The Fees Engine waives their fees. * **Premium banking:** You group "Diamond" accounts in a premium segment. You apply fee rules to the whole segment. * **Co-branded & special products:** You define loyalty points as an asset and track them in a customer's account. * **Unified account views:** An application lists all accounts under a portfolio in one call. * **Cross-portfolio operations:** A customer with a personal and a business portfolio transfers funds between them. The accounts stay separate in each portfolio. #### Example: student account perks A new customer signs up as a student. You assign their account to the "Student" segment. The Fees Engine waives their monthly fees. When the customer reaches a set age, you move the account to a different segment. # Midaz models Source: https://docs.lerian.studio/en/midaz/midaz-models Compare Midaz Community and Enterprise models — choose the licensing tier that fits your needs and pair it with SaaS or BYOC deployment. Midaz offers two licensing models: **Community** and **Enterprise**. Community fits developers and teams that want the core ledger at no cost. Enterprise fits organizations that need scale, compliance, and support. ## Deployment flexibility *** Both models are available under Lerian's [BYOC (Bring Your Own Cloud)](/en/deployment-models) model. You deploy and operate the platform in your own infrastructure. Enterprise customers can also choose the **SaaS** model, where Lerian manages the full infrastructure. Your licensing model (Community or Enterprise) defines **what features you get**. Your deployment model (SaaS or BYOC) defines **how the platform runs**. These are independent choices. For a full comparison, see [Deployment models](/en/deployment-models). ## Community model *** The **Community** model gives you full access to Midaz's core features at no cost, with community support. ### Key benefits * Free access to [Midaz's core features](/en/midaz/core-features). * Community support through Discord and GitHub. * Helm Charts for simpler deployments. This model fits developers, startups, and teams that try Midaz without enterprise support or SLAs. ## Enterprise model *** The **Enterprise** model fits organizations that require **scalability, compliance, and dedicated support**. It includes **everything in the Community model**, plus: ### Key benefits * **Lifecycle management** that keeps your infrastructure optimized. * **Software support** for mission-critical operations. * **Advanced access management** and **role-based control**. * **Onboarding assistance** to speed up adoption. * **Software availability SLA** for uptime commitments. * **Incident reports and security compliance**. * **Plugin integration** in a preconfigured environment. Plugins are available only to Enterprise clients. If you want access on another plan, [get in touch](https://lerian.studio/contact). Our team can help you find the right option. ## Choosing the right model *** The tables below help you compare both models and choose the right one. ### Services | | Community | Enterprise | | :------------------------------------ | :--------------------------- | :--------------------------- | | **Community Forum (Github/Discord)** | | | | **Helm Charts available** | | | | **Lifecycle management** | | | | **Observability (through Telemetry)** | | | | **Software support** | | | | **Software availability SLA** | | | | **Incident reports** | | | | **Onboarding assistance** | | | ### Enterprise features | | Community | Enterprise | | :---------------------------------- | :--------------------------- | :--------------------------- | | **Single sign-on (SSO)** | | | | **Workflows** | | | | **Event streaming (webhooks)** | | | | **Observability - Telemetry** | | | | **Cross-organization transactions** | | | | **Access Manager RBAC** | | | | **Multi-tenant support** | | | ### Core features | | Community | Enterprise | | :-------------------------------------- | :--------------------------- | :--------------------------- | | **Native event publisher** | | | | **User credentials** | | | | **Parent account ID** | | | | **Client segments** | | | | **Double-entry** | | | | **Multi-currency/asset Support** | | | | **Multi-organization/ledger Support** | | | | **N:N transactions** | | | | **Native race condition (per-account)** | | | | **Self-reconciliation** | | | | **Wallets portfolio** | | | | **Terraform deploy** | | | | **Visual interface** | | | # Midaz production best practices Source: https://docs.lerian.studio/en/midaz/midaz-production-best-practices Set up Midaz for production with multi-AZ deployment, autoscaling, managed services, and the Kubernetes resilience patterns Lerian recommends. Midaz runs in production at high volume. This guide gives you the deployment, high-availability, and observability patterns Lerian recommends. Follow them to keep downtime low and protect your data. ## Best-fit setup *** Start a production deployment with these choices: * Deploy across multiple availability zones. * Run at least 3 worker nodes with autoscaling. * Separate application and database workloads. * Use managed services such as RDS, ElastiCache, and MongoDB Atlas. * Apply Kubernetes patterns for resilience, security, and observability. * Automate backups and alerting from day one. ## Infrastructure planning *** ### Cluster architecture Plan the cluster for resilience and performance: * Deploy across multiple availability zones. * Run at least 3 worker nodes for high availability. * Enable node autoscaling to absorb workload spikes. * Separate application and database workloads when possible. ### Resource sizing * Match node sizes to the expected workload. * Give critical services enough resources first. * Apply resource quotas to prevent contention. * Monitor usage and tune the sizing over time. ### Storage * Use SSD-backed storage for all database components. * Define a storage class for each cloud provider. * Provision volumes with headroom for growth. * Use replicated or durable storage for critical data. ## Database architecture and high availability *** Midaz uses CQRS (Command Query Responsibility Segregation) to separate reads from writes. This design lets you scale each path on its own. ### PostgreSQL * Use a dedicated primary for writes and replicas for reads. * Enable synchronous replication for critical data. * Configure automatic failover with Patroni or AWS RDS. * Monitor replication lag and consistency. * Prefer managed services such as AWS RDS or GCP Cloud SQL. ### Redis / Valkey * Deploy in cluster mode across multiple zones. * Enable automatic failover with native clustering, or a Sentinel topology via `REDIS_MASTER_NAME`. Verify your client configuration — a managed service (ElastiCache, Memorystore) is the safer default. * Use managed services such as AWS ElastiCache or GCP Memorystore. ### MongoDB * Use replica sets with members across zones. * Monitor role transitions and lag. * Schedule regular backups. * Do not write to secondaries unless you intend to. * Use managed services such as MongoDB Atlas or AWS DocumentDB. ## Messaging infrastructure *** Midaz runs two messaging surfaces, both off by default: * **RabbitMQ** carries the internal async transaction balance-operation pipeline (`RABBITMQ_TRANSACTION_BALANCE_OPERATION_*`, enabled with `RABBITMQ_TRANSACTION_ASYNC=true`) plus the legacy outbound transaction, overdraft, and audit event exchanges. Use a managed RabbitMQ service such as AWS MQ or CloudAMQP in production. * **RedPanda** (via lib-streaming) is the forward-looking event backbone. Set `STREAMING_ENABLED=true` and `STREAMING_BROKERS`; events publish to `lerian.streaming..` topics. Transaction-lifecycle and overdraft events currently publish to both transports during the migration window. CQRS read/write separation is served by PostgreSQL replicas (`DB_*_REPLICA_*` DSNs), not by broker consumers rebuilding read models. ## High availability strategies *** ### Service redundancy * Deploy multiple replicas for every service. * Use anti-affinity rules to spread services across zones. * Apply Pod Disruption Budgets to limit downtime during updates. ### Load balancing * Use ingress controllers with health checks. * Avoid session affinity unless a service requires it. * Enable connection draining for smooth rollouts. ## Security considerations *** ### Network security * Apply Kubernetes network policies to control traffic. * Give each service account minimal permissions. * Secure external access with TLS. * Restrict admin interfaces with IP allowlists. ### Secret management * Use Kubernetes Secrets for credentials and tokens. * Rotate secrets on a regular schedule. * Never hardcode secrets in containers or config files. * Use an external secret manager for a stronger posture. ## Monitoring and observability *** ### Metrics * Monitor the key application and infrastructure KPIs. * Set alert thresholds that lead to action. * Use dashboards for real-time visibility. ### Logging * Centralize logs across all services. * Use a structured format for easier filtering. * Apply log retention and rotation policies. * Define log-based alerts for critical events. ### Tracing * Enable distributed tracing across services. * Sample traces to balance performance and cost. * Correlate traces with logs and metrics for full visibility. ### Alerting * Create clear, reliable alerts. * Tune thresholds to reduce noise. * Route each alert through the right channel. * Keep runbooks for recurring issues. ## Backup strategy *** * Automate regular backups for critical systems. * Store backups in more than one location or region. * Test the restore procedure on a regular schedule. * Keep the backup documentation current and accessible. ## Idempotency *** Protect critical operations against duplicate processing in production: * Send an idempotency key on every transaction create request with the `X-Idempotency` header. * Use explicit, deterministic keys tied to your business IDs (order IDs, payment references), not auto-generated keys. * Read the `X-Idempotency-Replayed` response header to tell a new transaction from a cached replay. * Set the `X-TTL` header in seconds to match your retry window. The default is 300. Use a shorter value for synchronous flows and a longer one for asynchronous flows. All Lerian products support idempotency through their own header conventions. For implementation details and a comparison across products, see [Retries and idempotency](/en/reference/retries-idempotency). ## Final notes *** Align your infrastructure to the Midaz architecture and you gain: * Clean read/write separation with CQRS. * Compatibility with managed cloud services. * A clear path to observability, failover, and secure operations. Review your setup on a regular schedule to keep this foundation solid as you grow. ## What’s next? *** Ready to scale, migrate, or harden your production environment? * Read the [Midaz deployment guide](/en/midaz/deployment). * [Contact our team](https://lerian.studio/contact) for tailored support. # Midaz SDK for Go Source: https://docs.lerian.studio/en/midaz/midaz-sdk-go Build Go applications on top of Midaz with the official v4 SDK — typed pagination, structured errors, and OpenTelemetry observability out of the box. The **Midaz SDK for Go** is the idiomatic v4 client for the Midaz financial-ledger APIs. It gives you typed access to every service — Organizations, Ledgers, Accounts, Transactions, and more — with a single surface for authentication, pagination, errors, logging, and observability. It handles the boilerplate so your code stays focused on business logic. **Coming from v2?** v4 is a clean major version with breaking changes across authentication, pagination, errors, and service access. There is no deprecation window — you swap your import from `/v2` to `/v4` and migrate at the same time. See [Migrating from v2](#migrating-from-v2) before you upgrade. ## Getting started *** ### Step 1 – Install Go Before using the SDK, you **must** install Go on your machine. v4 declares **Go 1.26** in `go.mod`. The public API also uses `iter.Seq2` and `log/slog`. Go to the [official Go website](https://golang.org/dl/). Download the installer for your OS (Windows, macOS, or Linux). [**Follow the installation instructions.**](https://go.dev/doc/install) ### Step 2 – Create or use an existing Go project **Create a Go project:** To create a Go project, use the following command: ```bash Bash theme={null} mkdir my-midaz-app cd my-midaz-app go mod init my-midaz-app ``` **Use an existing Go project:** If you're working in an existing project, make sure there's a `go.mod` file in the root. If not, run the following command to create one: ```bash Bash theme={null} go mod init your-module-name ``` ### Step 3 – Add the Midaz SDK Inside your project directory, run the following command to pull the v4 SDK and add it to your `go.mod` and `go.sum` files: **The module path requires the `/v4` suffix.** If you omit it, Go will resolve a stale pre-v4 release that is missing every change shipped in this version. Always import `github.com/LerianStudio/midaz-sdk-golang/v4`. ```bash Bash theme={null} go get github.com/LerianStudio/midaz-sdk-golang/v4 ``` Using VS Code or GoLand? Your IDE may automatically run `go get` when you import a new package. ### Step 4 – Import the SDK Create or open a `main.go` file and add the following content. The example below builds a client against your local Midaz stack with anonymous authentication, lists organizations, then creates a new one. The example below targets a local Midaz stack with auth disabled. If you don't have one running yet, see [Getting started with Midaz](/en/getting-started) to spin one up before running the snippet. ```go Go expandable theme={null} package main import ( "context" "fmt" "log" "github.com/LerianStudio/midaz-sdk-golang/v4" "github.com/LerianStudio/midaz-sdk-golang/v4/models" ) func main() { // Build a client. v4 requires exactly one auth source — use // midaz.WithAnonymous() for a local stack, or midaz.WithAccessManager(...) // for a development or production environment. c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentLocal), midaz.WithAnonymous(), ) if err != nil { log.Fatalf("midaz.New: %v", err) } defer c.Shutdown(context.Background()) ctx := context.Background() // List the first 5 organizations using a typed list-opts struct. page, err := c.Organizations.ListOrganizations(ctx, models.OrganizationsListOpts{ PageListOpts: models.PageListOpts{Limit: 5}, }) if err != nil { log.Fatalf("ListOrganizations: %v", err) } for _, org := range page.Items { fmt.Printf("- %s (%s)\n", org.LegalName, org.ID) } // Create a new organization. Notice that midaz.CreateOrganizationInput is // the same type as models.CreateOrganizationInput — re-exported on the // midaz package so most user code only needs one import. dba := "Example Inc." org, err := c.Organizations.CreateOrganization(ctx, &midaz.CreateOrganizationInput{ LegalName: "Example Corporation", LegalDocument: "123456789", DoingBusinessAs: &dba, // optional fields are *string — use &local for short literals Address: midaz.Address{ Line1: "123 Main St", City: "New York", State: "NY", ZipCode: "10001", Country: "US", }, }) if err != nil { log.Fatalf("CreateOrganization: %v", err) } fmt.Printf("Organization created: %s\n", org.ID) } ``` This gives you access to: * The Midaz client for calling every API service. * Built-in data models (like `CreateOrganizationInput`). * Auth via **Access Manager** (production) or **Anonymous** (local development). * A typed configuration system that fails fast at construction time. Want to learn more about authentication? Jump to the [Authentication](#authentication) section for the full setup. ### Step 5 – Run the project Run the following command: ```bash Bash theme={null} go run main.go ``` ## SDK architecture *** The **Midaz SDK for Go** is built around clarity and predictability. Every service is reachable directly on the client. Every list method follows the same trio shape. Every error is structured. Every option fails fast at construction time. #### Layered design | Layer | What it handles | | :------------------- | :--------------------------------------------------------------------------------------------- | | **Client** | The main entry point — `midaz.New(...)` wires authentication, retries, and observability. | | **Services** | High-level access to each Midaz domain, exposed as promoted fields on the client. | | **Models** | The core data structures that mirror Midaz's domain logic, re-exported on the `midaz` package. | | **Utility packages** | Modular helpers for config, errors, observability, retries, idempotency, and more. | Services are reached directly on the client — `c.Accounts`, `c.Transactions`, `c.Organizations`. You may still see an embedded `Entity` field in autocomplete, but new code should use the direct service fields shown in this guide. Want to dive deeper? Check the [architecture guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/architecture.md) for the full story behind the rewrite. ### Services The `Services` layer is your access point to every Midaz domain. Each service handles one resource family and ships every method as part of its single interface — no `UseAllAPIs()` toggle, no service registration step. Every service is ready to use the moment `midaz.New()` returns. #### Available services | Service | What it does | | :-------------------- | :----------------------------------------------------- | | `c.Organizations` | Manage organizations. | | `c.Ledgers` | Create and retrieve ledgers. | | `c.Assets` | Define and manage assets. | | `c.AssetRates` | Set up and fetch asset exchange rates. | | `c.Accounts` | Manage accounts and check balances. | | `c.AccountTypes` | Manage account type definitions. | | `c.Portfolios` | Group accounts under portfolios. | | `c.Segments` | Categorize accounts using segments. | | `c.Transactions` | Create and search financial transactions. | | `c.TransactionRoutes` | Define and manage transaction routing rules. | | `c.Operations` | Drill into the atomic operations inside a transaction. | | `c.OperationRoutes` | Define and manage operation routing rules. | | `c.Balances` | Get real-time account balances. | | `c.Holders` | Manage CRM account holders. | | `c.Aliases` | Manage CRM aliases for accounts and entities. | | `c.MetadataIndexes` | Manage searchable metadata indexes. | ### Models Models reflect how Midaz thinks about finance, with each type tied closely to a real-world business concept. You'll use them across every service call — from onboarding accounts to recording multi-leg transactions. In v4, the most common model types are re-exported on the `midaz` package itself. That means `midaz.Account` and `models.Account` are the same type, and most code only needs one import. #### Common model types | Model | What it represents | | :------------------- | :------------------------------------------------------------------- | | `midaz.Organization` | A business entity that owns ledgers and accounts. | | `midaz.Ledger` | A collection of accounts and transactions. | | `midaz.Asset` | A unit of value (currency, token, etc.) that can be stored or moved. | | `midaz.Account` | An account for tracking assets and balances. | | `midaz.Portfolio` | A collection of accounts for grouping and management. | | `midaz.Segment` | A categorization unit for granular organization. | | `midaz.Transaction` | A financial event composed of multiple operations. | | `midaz.Operation` | An individual debit or credit entry within a transaction. | | `midaz.Balance` | The current state of an account's holdings. | Need a builder, an internal request shape, or a deprecated type? Import `github.com/LerianStudio/midaz-sdk-golang/v4/models` directly — every type lives there, and the `midaz` aliases preserve type identity, so the two import paths interoperate cleanly. ### Utility packages Inside the `pkg` folder of the SDK, you'll find utility packages for common dev challenges — config handling, retry policies, and security primitives. They split into two groups. **Core** packages power cross-cutting SDK concerns. **Helper** packages provide domain-specific or low-level utilities. #### Core packages | Package | What it solves | | :-------------- | :---------------------------------------------------------------------------------- | | `auth` | Access Manager OAuth and token lifecycle. Replaces the v2 `access-manager` package. | | `config` | Centralized config handling, env overrides, and custom service URLs. | | `concurrent` | Tools for batching, rate-limiting, and worker pools. | | `errors` | Structured error types, classifiers, and the canonical `Retryable()` predicate. | | `observability` | Tracing, metrics, and logs through one OpenTelemetry provider. | | `retry` | Retry policy options with exponential backoff and jitter. | | `sdkctx` | Per-request context flags — idempotency keys, soft vs hard delete, include-deleted. | | `validation` | Input validation with clear, structured error messages. | #### Helper packages | Package | What it solves | | :------------ | :---------------------------------------------------------------- | | `accounts` | Account-specific helpers and convenience functions. | | `conversion` | Type conversion helpers between models and external formats. | | `data` | Data utilities and faker helpers for testing and demos. | | `format` | Utilities for formatting data the Midaz way (dates, times, etc.). | | `generator` | Demo and mass-data generation for end-to-end scenarios. | | `integrity` | Checksum and integrity verification utilities. | | `performance` | Helpers for tuning bulk operations and high-throughput tasks. | | `security` | Security utilities, including SSRF protection and TLS validation. | | `stats` | Processing statistics and metrics aggregation. | | `transaction` | Transaction-building helpers and fluent builders. | | `utils` | General-purpose helpers used across the SDK. | | `version` | SDK version metadata and identification. | The v2 `pkg/access-manager` package has moved to `pkg/auth` (so the directory matches the package name). The v2 `pkg/pagination` package was removed — its surface lives in `models` and on each service today. ## Authentication *** In v4, the SDK requires exactly one authentication source at construction time. Calling `midaz.New(...)` with neither returns a typed configuration error — no more silent 401 cascades on the first API call. You have two choices: * **`midaz.WithAccessManager(...)`** — production-shape OAuth via the Lerian Access Manager. Recommended for any non-local stack. * **`midaz.WithAnonymous()`** — opt out of authentication entirely. Suitable only for a local Midaz stack with auth disabled. The two options are mutually exclusive. ### Production: Access Manager Plug your Access Manager credentials into `midaz.WithAccessManager`. The SDK eagerly fetches an initial token at construction time, so misconfigurations surface as configuration errors instead of cascading 401s. Replace the values in the `// Configure Access Manager` block with your own credentials before running. ```go Go expandable theme={null} package main import ( "context" "log" "os" "github.com/LerianStudio/midaz-sdk-golang/v4" ) func main() { // Configure Access Manager. ClientID and ClientSecret are typically // loaded from environment variables — never hardcoded. c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentProduction), midaz.WithAccessManager(midaz.AccessManager{ Address: "https://auth.midaz.io", ClientID: os.Getenv("MIDAZ_CLIENT_ID"), ClientSecret: os.Getenv("MIDAZ_CLIENT_SECRET"), }), ) if err != nil { log.Fatalf("midaz.New: %v", err) } defer c.Shutdown(context.Background()) // Use c.Organizations, c.Ledgers, c.Accounts, ... as usual. } ``` The SDK requests a token from your Access Manager, attaches it to every API call, and refreshes it automatically when it expires. ### Local development: Anonymous For a local Midaz stack with auth disabled, opt out explicitly: ```go Go theme={null} c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentLocal), midaz.WithAnonymous(), ) ``` ### Configure via environment variables You can also point the SDK at your Access Manager via environment variables. Export them in your shell or your process manager: ```bash Bash theme={null} export PLUGIN_AUTH_ENABLED=true export PLUGIN_AUTH_ADDRESS=https://your-auth-service.com export MIDAZ_CLIENT_ID=your-client-id export MIDAZ_CLIENT_SECRET=your-client-secret ``` `config.FromEnvironment()` reads the **process environment**, not a `.env` file. If you keep variables in a `.env` file during development, load them with a library like `godotenv` before calling `config.NewConfig(config.FromEnvironment())`. Then opt in to environment loading at config time: ```go Go theme={null} import "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/config" cfg, err := config.NewConfig(config.FromEnvironment()) if err != nil { log.Fatalf("config: %v", err) } c, err := midaz.New(midaz.WithConfig(cfg)) ``` Environment loading is **explicit** in v4 — `config.FromEnvironment()` must be in the option chain. The SDK no longer reads env vars implicitly during construction. Want the full auth walkthrough? Check the [Authentication guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/auth.md) in the SDK repo. ## Multi-tenancy *** **Tenant scope comes from the Access Manager / JWT claims** used to obtain the token. The SDK applies tenant identity automatically from those claims — no extra configuration on the client side. To run calls under a different tenant scope, use a separate set of Access Manager credentials — or build a second client with its own token context. ## Listing and iteration *** Every list endpoint in v4 ships in three flavors. Pick the one that matches your use case — they're consistent across every service. | Method | Returns | Use when | | :------------- | :----------------------------------- | :---------------------------------------------------------- | | `ListXxx` | `*models.ListResponse[T]` (one page) | You want exactly one page and decide when to advance. | | `ListXxxAll` | `iter.Seq2[T, error]` | You want every item; the SDK handles paging internally. | | `ListXxxPages` | `iter.Seq2[*ListResponse[T], error]` | You need page-level metadata for checkpointing or batching. | ### Typed list options Each list method takes a typed opts struct that embeds one of two base structs depending on how the endpoint paginates: | Pagination shape | Endpoints | Base struct | | :--------------- | :------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ | | **Page-based** | Organizations, Ledgers, Assets, Portfolios, Segments, Accounts, AccountTypes, Balances, Holders, Aliases | `models.PageListOpts{Limit, Page, SortDirection, StartDate, EndDate}` | | **Cursor-based** | Transactions, Operations, OperationRoutes, TransactionRoutes, AssetRates | `models.CursorListOpts{Limit, Cursor, SortDirection, StartDate, EndDate}` | Each endpoint also exposes a typed `Filters` sub-struct with only the fields that endpoint actually honors. Setting a field on the wrong shape — for example, `Page` on a cursor endpoint — fails at compile time, not silently at runtime. ### Iterate every item with `ListAll` The most idiomatic shape: a `range` loop over every item across every page. The SDK advances cursors and fetches pages internally. ```go Go theme={null} import ( "github.com/LerianStudio/midaz-sdk-golang/v4" "github.com/LerianStudio/midaz-sdk-golang/v4/models" ) opts := models.AccountsListOpts{ PageListOpts: models.PageListOpts{Limit: 100}, Filters: models.AccountsFilters{ Status: "ACTIVE", AssetCode: "USD", }, } for account, err := range c.Accounts.ListAccountsAll(ctx, orgID, ledgerID, opts) { if err != nil { return fmt.Errorf("list accounts: %w", err) } process(account) } ``` ### Iterate page envelopes with `ListPages` When you need page-level metadata — for checkpointing, batching, or stopping mid-page — iterate over `ListXxxPages` instead. Every entry is a `*ListResponse[T]` with the full `Pagination` block attached. ```go Go theme={null} opts := models.TransactionsListOpts{ CursorListOpts: models.CursorListOpts{Limit: 50}, Filters: models.TransactionsFilters{Status: "APPROVED"}, } for page, err := range c.Transactions.ListTransactionsPages(ctx, orgID, ledgerID, opts) { if err != nil { return fmt.Errorf("page iter: %w", err) } log.Printf("page=%d items=%d next_cursor=%q", page.Pagination.Page, len(page.Items), page.Pagination.NextCursor) for _, tx := range page.Items { process(tx) } if shouldStop(page) { break // The SDK aborts in-flight paging cleanly. } } ``` See the [Pagination guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/pagination.md) in the SDK repo for `HasMore()` semantics, `NextCursor` handling, and the page-vs-cursor decision table. ## Error handling *** Most errors returned by the SDK service layer are `*pkg/errors.Error` with structured fields: | Field | What it carries | | :---------- | :------------------------------------------------------------------------------- | | `Category` | The error class (validation, authentication, network, configuration, and so on). | | `Code` | A stable string code suitable for branching or telemetry. | | `Operation` | The SDK operation that produced the error (e.g. `Accounts.GetAccount`). | | `Resource` | The resource family the error refers to, when relevant. | You can branch with typed predicates, walk fields with `errors.As`, or use the canonical `Retryable()` method to drive retry policy. ### Branch with typed predicates The SDK ships with a full set of `Is*` predicates so you can match errors without poking at internals: ```go Go expandable theme={null} import ( "errors" "fmt" sdkerrors "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/errors" ) acc, err := c.Accounts.GetAccount(ctx, orgID, ledgerID, accountID) if err != nil { switch { case sdkerrors.IsNotFoundError(err): return fmt.Errorf("account not found: %w", err) case sdkerrors.IsAuthError(err): // Matches both 401 and 403 — re-authenticate or fix permissions. return fmt.Errorf("auth failure: %w", err) case sdkerrors.IsValidationError(err): return fmt.Errorf("invalid input: %w", err) case sdkerrors.IsConflictError(err): return fmt.Errorf("already exists: %w", err) case sdkerrors.IsRateLimitError(err): return fmt.Errorf("rate limited: %w", err) case sdkerrors.IsNetworkError(err): return fmt.Errorf("transient transport: %w", err) case sdkerrors.IsConfigurationError(err): return fmt.Errorf("setup mistake: %w", err) } // Walk the structured fields when you need them. var sdkErr *sdkerrors.Error if errors.As(err, &sdkErr) { log.Printf("op=%s resource=%s code=%s retryable=%v", sdkErr.Operation, sdkErr.Resource, sdkErr.Code, sdkErr.Retryable()) } } ``` ### Drive retry decisions with `Retryable()` The `Error.Retryable()` method is the canonical retry-policy source. Use it instead of building your own classification. ```go Go theme={null} var sdkErr *sdkerrors.Error if errors.As(err, &sdkErr) && sdkErr.Retryable() { // Apply your retry logic — backoff, jitter, max attempts, etc. } ``` ### Wrap raw transport errors If you're calling lower-level HTTP code outside the SDK and want the same structured error shape, use `ClassifyTransportError`: ```go Go theme={null} import sdkerrors "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/errors" resp, err := httpClient.Do(req) if err != nil { return sdkerrors.ClassifyTransportError("PaymentService.Charge", err) } ``` ### Local validation: FieldErrors Local input validation surfaces `*pkg/validation.FieldErrors` — a structured collection of per-field complaints from the SDK before any HTTP call. Use `errors.As` to inspect them when validating user input or builders. ```go Go theme={null} import ( "errors" "fmt" "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/validation" ) var fieldErrs *validation.FieldErrors if errors.As(err, &fieldErrs) { for _, fe := range fieldErrs.Errs() { fmt.Printf("- %s: %s\n", fe.Field, fe.Message) } } ``` Want to dive deeper? Check the [Error handling guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/errors.md) in the SDK repo for the full category map, every code, and retry boundary semantics. ## Logging *** In v4, **`*slog.Logger` is the canonical logger surface**. Wire it via `midaz.WithLogger(...)`. The SDK is **silent by default** — it uses `slog.DiscardHandler` until you opt in. That means no surprise log lines in your stdout, and no fighting with the SDK over log format. You decide the handler, the level, and the destination. ```go Go expandable theme={null} package main import ( "context" "log" "log/slog" "os" "github.com/LerianStudio/midaz-sdk-golang/v4" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentLocal), midaz.WithAnonymous(), midaz.WithLogger(logger), ) if err != nil { log.Fatalf("midaz.New: %v", err) } defer c.Shutdown(context.Background()) // SDK retry diagnostics, slow-call warnings, and other internal // log lines now flow through your handler. } ``` Need zap, zerolog, or charmbracelet/log? They all integrate as `slog.Handler` adapters — the SDK doesn't care which backend produces the records, only that it speaks slog. Wiring zap, zerolog, or another backend? The [Logging guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/logging.md) has adapter recipes for every common logging library. ## Observability *** OpenTelemetry is first-class in v4. One observability provider gives you spans, metrics, and OTel-correlated logs through a single wiring point. Configure observability either by passing a fully-built provider, or by passing options that the SDK assembles for you. ### Wire a provider ```go Go expandable theme={null} package main import ( "context" "log" "github.com/LerianStudio/midaz-sdk-golang/v4" "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/observability" ) func main() { ctx := context.Background() provider, err := observability.New(ctx, observability.WithServiceName("payments-api"), observability.WithEnvironment("production"), observability.WithComponentEnabled(true, true, true), // tracing, metrics, logs ) if err != nil { log.Fatalf("observability.New: %v", err) } defer provider.Shutdown(ctx) c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentProduction), midaz.WithAccessManager(midaz.AccessManager{ /* ... */ }), midaz.WithObservabilityProvider(provider), ) if err != nil { log.Fatalf("midaz.New: %v", err) } defer c.Shutdown(ctx) } ``` ### Or pass options inline ```go Go theme={null} import "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/observability" c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentProduction), midaz.WithAccessManager(am), midaz.WithObservabilityOptions( observability.WithServiceName("payments-api"), observability.WithComponentEnabled(true, true, true), ), ) ``` The SDK emits one HTTP span per outbound request with proper W3C `traceparent` propagation. Business log records carry safe IDs only — never payloads, names, addresses, or auth headers. You can also wrap a block of business logic in a span via `Client.Trace`: ```go Go theme={null} err = c.Trace("create-organization", func(ctx context.Context) error { _, err := c.Organizations.CreateOrganization(ctx, input) return err }) ``` `WithObservabilityOptions` and `WithObservabilityProvider` use **replacement semantics**, not merge. Each call replaces any previously installed provider. To start from a baseline, include `observability.WithDevelopmentDefaults` or `observability.WithProductionDefaults` as the first option in the chain. Want to dive deeper? See the [`10-observability-otel` example](https://github.com/LerianStudio/midaz-sdk-golang/tree/main/examples/10-observability-otel) in the SDK repo for span attributes, metric names, and exporter setup. ## Idempotency *** Auto-idempotency is **on by default** in v4. The SDK emits an `X-Idempotency: ` header on every unsafe HTTP request (POST, PUT, PATCH, DELETE). Retries on transient failures then do not double-create resources. You can override the auto-generated key per-call when you need a stable, caller-supplied key — typical for saga steps, outbox rows, or UI-driven submissions. ### Set a stable key per request ```go Go theme={null} import "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/sdkctx" ctx := sdkctx.WithIdempotencyKey(context.Background(), "tx-2026-05-06-001") tx, err := c.Transactions.CreateTransaction(ctx, orgID, ledgerID, input) ``` ### Suppress idempotency for one call For rare fire-and-forget administrative endpoints, suppress the header per-request: ```go Go theme={null} ctx := sdkctx.WithoutAutoIdempotency(context.Background()) err := c.SomeAdminService.DoOneShotThing(ctx, input) ``` ### Disable globally If you don't want auto-idempotency anywhere, turn it off at the client level: ```go Go theme={null} c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentLocal), midaz.WithAnonymous(), midaz.WithIdempotency(false), ) ``` You can also disable it via the `MIDAZ_IDEMPOTENCY=false` environment variable when using `config.FromEnvironment()`. ## Environment variables *** You can configure the SDK with environment variables instead of hardcoded values. Environment loading is **explicit** in v4 — pass `config.FromEnvironment()` in your config option chain to opt in. | Variable | Description | | :-------------------- | :-------------------------------------------------------------------------------- | | `MIDAZ_ENVIRONMENT` | Target environment (`local`, `development`, `production`). | | `MIDAZ_BASE_URL` | Base URL for all Midaz services, used when the service-specific URLs are not set. | | `MIDAZ_LEDGER_URL` | Ledger API URL. It serves the onboarding and transaction endpoints. | | `MIDAZ_CRM_URL` | CRM API URL. Holders and Aliases use it. | | `PLUGIN_AUTH_ENABLED` | Enable Access Manager authentication (`true` or `false`). | | `PLUGIN_AUTH_ADDRESS` | Address of the Access Manager service. | | `MIDAZ_CLIENT_ID` | Client ID for Access Manager authentication. | | `MIDAZ_CLIENT_SECRET` | Client secret for Access Manager authentication. | | `MIDAZ_TIMEOUT` | HTTP request timeout in seconds. | | `MIDAZ_DEBUG` | Enable debug logs (`true` or `false`). | | `MIDAZ_MAX_RETRIES` | Maximum retry attempts for failed requests. Set `0` to disable retries. | | `MIDAZ_IDEMPOTENCY` | Enable automatic idempotency keys for unsafe requests (`true` or `false`). | For the full option matrix across `midaz`, `pkg/config`, and `pkg/sdkctx`, see the [Configuration guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/configuration.md) in the SDK repo. ## Example projects *** The SDK ships with a numbered tour of its core capabilities (examples 01–10) plus a set of advanced and reference examples. The numbered set is **focused tutorials**: each one teaches exactly one concept with the smallest possible body. Browse them in the [examples directory](https://github.com/LerianStudio/midaz-sdk-golang/tree/main/examples) on GitHub. | Example | What it demonstrates | | :---------------------- | :------------------------------------------------------------- | | `01-hello-world` | Minimal init plus the first API call (\~17 body lines). | | `02-auth` | Access Manager authentication (production setup). | | `03-end-to-end` | Org → ledger → asset → account → transaction. | | `04-listing-cursor` | Cursor-based pagination with `iter.Seq2`. | | `05-listing-pages` | Page-based pagination — `List` / `ListAll` / `ListPages`. | | `06-idempotency` | Auto / explicit / suppressed idempotency modes. | | `07-retries` | Default policy, custom policy, disabled retries. | | `08-logging-slog` | `*slog.Logger` integration (v4 logging). | | `09-testing-with-mocks` | `go.uber.org/mock` for unit testing your code against the SDK. | | `10-observability-otel` | Full OpenTelemetry surface (tracing + metrics + logs). | The repository also includes specialized and reference examples — `concurrency`, `configuration`, `context`, `tracing`, `tracing-server`, `pkg-validation-demo`, `mass-demo-generator`, and `workflow-with-entities`. They cover advanced patterns (bounded parallelism, OTel context propagation across processes, mass data generation) for when you've outgrown the focused set. ## Migrating from v2 *** v4 is a **clean-cut major version with no deprecation window**. There is no transitional release, no `// Deprecated:` shim, no backward-compatible alias of the old surface. Swap your import from `/v2` to `/v4` and walk the breaking changes below. The biggest moves to plan for: * **Module path and package name**: `github.com/LerianStudio/midaz-sdk-golang/v4` (was `/v2`), package `midaz` (was `client`). * **Authentication**: `WithAuthToken` is gone. Use `WithAccessManager` for production or `WithAnonymous` for local stacks. One auth source is required at construction time. * **Service access**: `c.Accounts.X` (was `c.Entity.Accounts.X`). The `c.Entity` field still exists for compatibility, but every example uses the short form. * **Pagination**: `models.ListOptions` and its 30 fluent setters are gone. Use the typed per-endpoint opts (`models.AccountsListOpts`, `models.TransactionsListOpts`, ...) and the new `ListAll` / `ListPages` iterators. * **Errors**: `*MidazError` is gone. Service-layer errors now use `*pkg/errors.Error` with `Retryable()` as the official retry source. Local validation may surface `*pkg/validation.FieldErrors`. * **Tenant identity**: `WithTenantID`, the `MIDAZ_TENANT_ID` env var, and the `X-Tenant-ID` header are gone. Tenant scope flows through Access Manager / JWT claims. * **Logging**: `*slog.Logger` replaces the bespoke `observability.Logger` interface as the canonical surface. The SDK is silent by default. For the authentication changes in detail, see the [Authentication guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/auth.md) in the SDK repo. The [examples directory](https://github.com/LerianStudio/midaz-sdk-golang/tree/main/examples) shows the v4 API in practice. ## Explore the APIs *** For more information about the APIs, refer to the following links: # Midaz SDK for TypeScript Source: https://docs.lerian.studio/en/midaz/midaz-sdk-typescript Build typed financial integrations with the Midaz SDK for TypeScript — builder pattern, automatic retries, observability, and strict validation. The Midaz SDK for TypeScript helps you build financial integrations. It gives you a typed, developer-friendly interface over the Midaz financial services platform. You focus on your business logic, not on the transport code. The SDK works with Organizations, Ledgers, Accounts, Transactions, and more. Use it for a simple workflow or for complex operations. A layered, modular architecture supports performance, extensibility, and the developer experience. ### Why use the Midaz SDK for TypeScript? * **Type-safe by design**: Full TypeScript support with precise type definitions. * **Builder pattern**: Fluent, readable interfaces to construct complex objects. * **Robust error handling**: Recovery strategies and clear error signals. * **Observability included**: Tracing, metrics, and logs, ready to use. * **Layered architecture**: Clean separation between client, entities, API, and models. * **Automatic retries**: Configurable retry policies for transient failures. * **Concurrency controls**: Built-in tools to run tasks in parallel with controlled throughput. * **Fast with caching**: In-memory caching for better performance. * **Strict validation**: Catch invalid input early with clear error messages. ## Getting started *** ### Prerequisite * The Midaz SDK for TypeScript **requires** TypeScript **v5.8 or later**. ### Installing the SDK Install the **Midaz SDK for TypeScript** with one of the following commands: ```bash npm theme={null} npm install @lerianstudio/midaz-sdk ``` ```bash yarn theme={null} yarn add @lerianstudio/midaz-sdk ``` After you install it, follow the [*Quick Start Guide*](#quick-start-guide) to learn how to use the SDK. ## Authentication *** The **Midaz SDK for TypeScript** authenticates through the Lerian **Access Manager** (OAuth). For a local stack with authentication disabled, you can build a client without it. You never call a `createClient` factory — build a configuration with `createClientConfigWithAccessManager()` (or `createClientConfigBuilder()` for a no-auth local stack) and pass it to `new MidazClient(config)`. #### Access Manager authentication To integrate with external identity providers over OAuth: ```typescript TypeScript theme={null} import { createClientConfigWithAccessManager, MidazClient } from '@lerianstudio/midaz-sdk'; const client = new MidazClient( createClientConfigWithAccessManager({ address: 'https://auth.example.com', clientId: 'your-client-id', clientSecret: 'your-client-secret', }).withEnvironment('sandbox') ); ``` The Access Manager handles tokens for you: acquisition, caching, and renewal. You do not manage tokens manually. #### Local development (no authentication) For a local Midaz stack with authentication disabled, build a client without the Access Manager: ```typescript TypeScript theme={null} import { createClientConfigBuilder, MidazClient } from '@lerianstudio/midaz-sdk'; const client = new MidazClient( createClientConfigBuilder().withEnvironment('development') ); ``` We offer an [Access Manager plugin](/en/platform/access-manager/access-manager) that you can use. If you'd like to know more about it, [contact us](https://lerian.studio/contact). ## Quick start guide *** The following sections give practical code examples for the **Midaz SDK for TypeScript**. ### Create a client This is the first step. The client is your main entry point to the SDK. It handles authentication and gives you access to all entity services. **Example:** ```typescript TypeScript theme={null} import { createClientConfigBuilder, MidazClient } from '@lerianstudio/midaz-sdk'; const client = new MidazClient( createClientConfigBuilder().withEnvironment('sandbox') // Options: 'development', 'sandbox', 'production' ); ``` ### Create an Asset Create assets with the builder pattern and `createAssetBuilder`. **Example:** ```typescript TypeScript theme={null} import { createAssetBuilder } from '@lerianstudio/midaz-sdk'; const assetInput = createAssetBuilder('US Dollar', 'USD') .withType('currency') .withMetadata({ precision: 2, symbol: '$' }) .build(); const asset = await client.entities.assets.createAsset('org_123', 'ledger_456', assetInput); ``` In this code, you add the required `name` and `assetCode` fields to the builder `const assetInput = createAssetBuilder('US Dollar', 'USD')`. Then you add any other properties with `with*` methods. ### Create an Account Create accounts with the builder pattern and `createAccountBuilder`. **Example:** ```typescript TypeScript theme={null} import { createAccountBuilder } from '@lerianstudio/midaz-sdk'; const accountInput = createAccountBuilder('Savings Account', 'USD') .withType('savings') .withAlias('personal-savings') .build(); const account = await client.entities.accounts.createAccount('org_123', 'ledger_456', accountInput); ``` In this code, you add the required `name` and `assetCode` fields to the builder `const accountInput = createAccountBuilder('Savings Account', 'USD')`. Then you add any other properties with `with*` methods. ### Create a Transaction Create transactions with the builder pattern and `createTransactionBuilder`. **Example:** ```typescript TypeScript expandable theme={null} import { createTransactionBuilder } from '@lerianstudio/midaz-sdk'; const transactionInput = createTransactionBuilder() .withCode('payment_001') .withOperations([ { accountId: 'source_account_id', assetCode: 'USD', amount: 100 * 100, // $100.00 type: 'debit', }, { accountId: 'destination_account_id', assetCode: 'USD', amount: 100 * 100, // $100.00 type: 'credit', }, ]) .withMetadata({ purpose: 'Monthly payment' }) .build(); ``` In this code, you add all properties with `with*` methods. ### Error recovery Use enhanced error recovery for critical operations. ```typescript TypeScript theme={null} import { withEnhancedRecovery } from '@lerianstudio/midaz-sdk/util/error'; const result = await withEnhancedRecovery( () => client.entities.transactions.createTransaction('org_123', 'ledger_456', transactionInput), { maxRetries: 3, enableSmartRecovery: true, } ); ``` ### Clean up resources ```typescript TypeScript theme={null} client.close(); ``` ### Using Access Manager for authentication ```typescript TypeScript expandable theme={null} import { createClientConfigWithAccessManager, MidazClient } from '@lerianstudio/midaz-sdk'; // Initialize the client with Access Manager authentication const client = new MidazClient( createClientConfigWithAccessManager({ address: 'https://auth.example.com', // Identity provider address clientId: 'your-client-id', // OAuth client ID clientSecret: 'your-client-secret', // OAuth client secret tokenEndpoint: '/oauth/token', // Optional, defaults to '/oauth/token' refreshThresholdSeconds: 300, // Optional, defaults to 300 (5 minutes) }) .withEnvironment('sandbox') .withApiVersion('v1') ); // The SDK will automatically handle token acquisition and renewal // You can now use the client as normal const organizations = await client.entities.organizations.listOrganizations(); // For environment-specific configurations with Access Manager const sandboxClient = new MidazClient( createSandboxConfigWithAccessManager({ address: 'https://auth.example.com', clientId: 'your-client-id', clientSecret: 'your-client-secret', }) ); // Clean up resources when done client.close(); ``` ## SDK architecture *** The Midaz SDK uses a multi-layered service architecture for a clean, modular, and scalable developer experience. It has three layers, shown in *Figure 1*. Each layer serves a distinct purpose. * **Client interface**: This is the main entry point for SDK users. It manages configuration such as API keys and environments. It initializes services lazily and exposes all SDK functionality. * **Entity services layer**: This layer holds domain-specific services, such as Accounts, Assets, and Transactions. Each service offers consistent methods: create, get, update, delete, and list. Each service also adds specialized operations for its entity. * **Core services layer**: All entity services use these foundational utilities. They handle HTTP requests, input validation, error processing, observability, configuration, and caching. Layered architecture of the Midaz SDK for TypeScript, with the client interface over the entity services layer over the shared core services layer The SDK architecture emphasizes: * **Consistency** through shared patterns across services. * **Scalability** via dependency injection and service factories. * **Reliability** through enhanced error handling and typed responses. * **Testability** with support for mocking, integration, and contract testing. Want to dive Deeper? Check the following pages for more information about the Architecture: * [Midaz SDK architecture overview](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/overview.md). * [Client interface architecture](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/client-interface.md). * [Service layer architecture](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/service-layer.md). ## Builder pattern *** The **Midaz SDK for TypeScript** uses a builder pattern to help you assemble complex objects in a safe, adaptable way. Instead of a fixed set of inputs, it gives you a step-by-step, fluent, chainable interface. **Builder functions in the SDK:** * Tell you the parameters in advance. * Let you set optional fields with `.with*()` methods and chain them. * Prevent invalid states through a guided structure. * Hide internal complexity for better readability. ### Example Here’s a quick example: ```typescript TypeScript theme={null} const assetInput = createAssetBuilder('USD Currency', 'USD') .withType('currency') .withMetadata({ precision: 2 }) .build(); ``` You can then pass this `assetInput` to the corresponding create method in the SDK. Want to dive deeper? Check the [Builder Pattern in Midaz SDK](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/core-concepts/builder-pattern.md) page for more information. ## Working with entities *** Each entity service covers a distinct part of the financial domain, such as accounts, assets, or transactions. These services create, retrieve, update, and delete data for each type of entity. They also offer specialized features for each use case, so you handle financial data with confidence. | Entity | Description | | :----------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | | [**Organizations**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/organizations.md) | Manage business units and organizational data. | | [**Ledgers**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/ledgers.md) | Structure and manage financial records. | | [**Assets**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/assets.md) | Work with assets such as currencies, commodities, and other value units. | | [**Accounts**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/accounts.md) | Create, retrieve, update, and delete accounts within a Ledger. | | [**Segments**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/segments.md) | Organize portfolios for analytics and reporting. | | [**Portfolios**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/portfolios.md) | Group accounts and assets into meaningful financial collections. | | [**Balances**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/balances.md) | Retrieve and calculate asset balances for accounts. | | [**Asset Rates**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/asset-rates.md) | Handle exchange rates between different asset types. | | [**Transactions**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/transactions.md) | Create and manage transactions that move assets between accounts. | | [**Operations**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/operations.md) | Manage atomic debits and credits that make up a transaction. | You access each service through the SDK client. They follow a consistent structure, so you build and maintain financial features more easily. Want to dive deeper? Check the [Entities pages](https://github.com/LerianStudio/midaz-sdk-typescript/tree/main/docs/entities) for more information. ## Using utilities *** The SDK provides utility modules for common operations: performance, error handling, configuration, and observability. These tools work with the rest of the SDK and help you build financial applications with less effort. | Utility | Description | | :---------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | | [**Account Helpers**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/account-helpers.md) | Simplifies common account-related logic and transformations. | | [**Cache**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/cache.md) | Enables lightweight caching for better runtime performance. | | [**Concurrency**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/concurrency.md) | Helps coordinate and limit concurrent tasks safely and efficiently. | | [**Config**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/config.md) | Centralized configuration setup and access. | | [**Data**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/data.md) | Assists with data formatting and pagination tasks. | | [**Error Handling**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/error-handling.md) | Offers recovery strategies and error processing mechanisms. | | [**HTTP Client**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/http-client.md) | Provides a low-level HTTP interface for direct API calls. | | [**Network**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/network.md) | Adds high-level networking features like retries and backoff. | | [**Observability**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/observability.md) | Captures traces, metrics, and logs to support monitoring and debugging. | | [**Pagination**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/pagination.md) | Handles paginated responses with predictable, consistent helpers. | | [**Validation**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/validation.md) | Validates input and output data to help maintain data integrity. | Want to dive deeper? Check the [Utilities pages](https://github.com/LerianStudio/midaz-sdk-typescript/tree/main/docs/utilities) for more information. ## Error handling *** The **Midaz SDK for TypeScript** helps you handle errors clearly and consistently. When an error occurs during an SDK operation, the SDK throws a structured error. The error includes key fields: * `code`: A short, consistent identifier for the error type. * `message`: A human-readable description. * `statusCode`: The HTTP status code, when available. Handle an error like this: ```typescript TypeScript theme={null} try { await client.transactions.create(transaction); } catch (err) { console.error(`Error (${err.code}): ${err.message}`); // Optionally: inspect err.statusCode } ``` ### Common error codes | Code | Description | HTTP Status | | :-------------------- | :----------------------------------------------------------- | :---------- | | `invalid_input` | Your request is missing required data or has invalid values. | `400` | | `unauthorized` | Authentication failed or credentials are missing. | `401` | | `forbidden` | You're not allowed to perform this action. | `403` | | `not_found` | The resource you're trying to access doesn't exist. | `404` | | `conflict` | The operation conflicts with an existing resource. | `409` | | `internal_error` | Something went wrong on our side. | `500` | | `service_unavailable` | Temporary outage—try again later. | `503` | ### Best practices * **Validate input** before you call SDK methods, to avoid `invalid_input`. * **Check your auth** when you get `unauthorized` or `forbidden`. * **Retry** on transient issues like `internal_error` or `service_unavailable`. * **Use `statusCode` and `message`** to show debug info in development logs. The SDK keeps errors predictable and actionable. **Tip** Want to dive deeper? Check the following pages for more information: * [Error handling in Midaz SDK](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/core-concepts/error-handling.md). * [Error handling architecture](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/error-handling.md). * [Error handling (Utilities)](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/error-handling.md). ## CI/CD pipeline *** We use GitHub Actions for automated, production-ready builds: * Runs tests across multiple Node.js versions. * Enforces code quality with ESLint and Prettier. * Keeps dependencies up to date with Dependabot. * Handles releases automatically with semantic versioning. * Generates changelogs. ## Want to contribute? *** To contribute to the Midaz SDK for TypeScript, start with our [contributing guide on GitHub](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/CONTRIBUTING.md). It covers what you need to get started. ## License *** This project is licensed under the Apache License 2.0. For details, see the [License](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/LICENSE) page. # Operations Source: https://docs.lerian.studio/en/midaz/operations Understand Operations as the smallest units of ledger activity in Midaz — single debit or credit entries on an Account, grouped into balanced Transactions. An operation is the smallest unit of a transaction. Every transaction contains two or more operations (*Figure 1*). Each operation records a change in the balance of one account: * A **Debit operation** records value that moves **out of** an account — the source. * A **Credit operation** records value that moves **into** an account — the destination. The effect on the stored balance depends on the balance `direction`. A normal account (the common case) shrinks on a debit and grows on a credit. A transaction is a set of operations that must net to zero from the ledger's view. For example, a P2P payment with a fee includes: * a **\$100 debit** from the sender's account. * a **\$98 credit** to the recipient's account. * a **\$2 credit** to the platform's fee account. Midaz makes sure a transaction balances before it commits. Operations also carry metadata such as timestamps and references to the transaction they belong to. You create a transaction through the Midaz JSON API. For each transaction, you specify a set of operations. Each operation has an amount, an asset, and a source or destination account. Midaz then checks that the operations balance before it commits them. ## Key characteristics *** * Each operation belongs to a transaction and references a specific account and asset. * Operations include metadata like amount, direction (debit/credit), type, timestamps, references, and more. * Midaz creates operations when you start a transaction and fills them with the data you provide. * Midaz validates all operations against the configured rules before it commits them to the ledger. Every operation has a **`direction`** (`debit` or `credit`) and a **`type`**. The `direction` is the double-entry backbone described above. The `type` classifies the movement. Beyond the standard `CREDIT` and `DEBIT`, the `type` marks special movements: * `ON_HOLD` and `RELEASE` — two-phase fund reservations. * `OVERDRAFT` — system-generated companion operations on the internal overdraft balance. * `BLOCK` and `UNBLOCK` — the label applied to every operation of a transaction created through the dedicated [block and unblock endpoints](/en/midaz/transactions#blocking-and-unblocking-funds). The label overrides only the operation `type`, never its direction or amount. The `direction` always keeps its debit or credit meaning, regardless of `type`. ## Operation Routes *** With Accounting Routes, each operation connects to structured validation logic. That logic lives in [Operation Routes](/en/midaz/transaction-routing-entities). ### What is an Operation Route? An **Operation Route** is a validation rule. It defines which account types or specific accounts a transaction component can use. It also sets the accounting direction (source or destination) and a descriptive annotation. It works as a validation layer between transaction input and ledger structure. Each operation you submit must meet the account requirements and business rules. In short: transactions contain multiple operations. Operation Routes validate each operation against the configured validation rules. ### Operation > Operation Route relationship When you submit a transaction to Midaz, it contains multiple **Operations**. With **Account Types and Accounting Routes enabled**, Midaz validates each operation against the rules in the Operation Routes. This makes sure that: * The specified account matches the expected account type or specific account requirement. * The operation direction (debit/credit) aligns with the route definition. This adds a layer of validation, consistency, and integrity to transaction processing. It matters most for structured financial requirements. If the operations you submit do not match the configured Operation Route requirements, Midaz fails the validation and rejects the transaction. ## Managing Operations *** Midaz generates operations automatically as part of a transaction flow. You cannot create an operation directly. You can query an operation as needed. * [List Operations by Account](/en/reference/midaz/list-operations-by-account) — View all Operations linked to a specific Account. * [Retrieve an Operation by Account](/en/reference/midaz/retrieve-an-operation-by-account) — Get details of a specific Operation for that Account. # Organizations Source: https://docs.lerian.studio/en/midaz/organizations Define the top-level Organization that owns your Midaz Ledgers — represent banks, fintechs, and parent-child hierarchies for subsidiaries. An **Organization** is the top-level entity in Midaz. It represents a financial institution, such as a bank or fintech. An Organization owns and governs the financial data of that institution. It is the primary container for all financial operations. A bank or financial group registers as an Organization in Midaz. The Organization gives its operations a structured environment. **SaaS and multi-tenant environments** — Midaz scopes every Organization to your tenant automatically. When you list Organizations, you see only the ones in your account. The platform enforces [tenant isolation](/en/multi-tenancy) for you. You do not filter by tenant or add a tenant identifier to your requests. ### Sub-Organizations Organizations support **parent-child hierarchies**. A bank uses them to represent subsidiaries, regional branches, or separate business units in one framework. This structure fits financial groups that run several legal entities. Each entity owns its own [Ledgers](/en/midaz/ledgers) and stays operationally independent. The parent-child link records how the entities relate. ### Organization structure * **Organization > Ledgers:** Every Organization owns one or more Ledgers. Each Ledger holds its financial records, such as accounts and transactions, and belongs to one Organization. ### Key characteristics * Organizations are the highest level in the hierarchy. * Each Organization governs its own Ledgers and sets their operational boundaries. ## Flexible hierarchies *** Midaz supports organizational hierarchies for complex structures. If your company has several locations or divisions, you can create **child organizations** for specific branches, offices, or subdivisions. Each child organization connects to a **parent organization**. Each Organization keeps its own data. Records in one Organization do not cross into another. If a single group manages several Organizations, use this hierarchy to set clear boundaries between them. ### Example **Company XPTO** is a Brazilian company with branches in São Paulo and New York. Its structure is as follows: * **Parent Organization**: XPTO Ltda * **Child Organizations**: * XPTO New York * XPTO São Paulo With this structure, the company can: * Set a distinct role for each branch. * Track each branch separately. * Adapt workflows to each branch. ## Benefits of organizational structuring *** * **Scalability**: Add or change branches as the business grows. * **Efficiency**: Manage all levels from one hierarchy. * **Personalization**: Configure each branch on its own. Midaz adapts to your company's structure, whether it is simple or complex. Need help with your Midaz setup? Our consulting service can help you design the structure for your organization, from scratch or from an existing setup. [Contact us](https://lerian.studio/contact) to learn more. ## Managing Organizations *** You manage your Organization through the API or Lerian Console. ### Via API * [Create an Organization](/en/reference/midaz/create-an-organization) — Register a new Organization. * [List Organizations](/en/reference/midaz/list-organizations) — View all Organizations in your workspace. * [Retrieve an Organization](/en/reference/midaz/retrieve-an-organization) — Get details of a specific Organization. * [Update an Organization](/en/reference/midaz/update-an-organization) — Edit an existing Organization. * [Delete an Organization](/en/reference/midaz/delete-an-organization) — Mark an Organization as deleted. When you delete an Organization, Midaz does not remove it from the system. Midaz sets a `deletedAt` timestamp instead. The Organization is then excluded from every read and can no longer be used. Its `status.code` is left unchanged, which preserves historical data for audit trails. ### Via Lerian Console You view, create, edit, and delete Organizations from the **Organizations** page in the Midaz Module of Lerian Console. [**Learn more in the Managing Organizations guide.**](/en/midaz/console/managing-organizations) When you create your **first Organization** in the Console, you do it as part of the onboarding flow. To learn more, see the [Midaz Module onboarding](/en/midaz/console/midaz-console-onboarding) guide. # Portfolios Source: https://docs.lerian.studio/en/midaz/portfolios Group Midaz Accounts under a single customer or business unit as a Portfolio to view the total client relationship and apply policies at the wallet level. A **Portfolio** is a logical group of Accounts that belong to a single entity, such as a customer or business unit. It groups the related Accounts under one structure and makes them simpler to manage. Think of a Portfolio as a **wallet** that holds multiple asset-specific Accounts for a customer. For example, a retail banking customer might have a Portfolio that includes: * A BRL checking account. * A USD savings account. * A credit card account. With a Portfolio, an institution can view a customer’s total relationship across all accounts. The institution can also apply policies at the Portfolio level. ## Portfolio structure *** * **Portfolio > Accounts**: A Portfolio groups one or more Accounts. These Accounts usually link to one customer or entity. * **Portfolio > Ledger > Organization**: A Portfolio belongs to an Organization through its Ledger. ## Key characteristics *** * Portfolios commonly represent **customers** in a banking context. * They simplify financial aggregation and give customer-level insights. * You can also create internal Portfolios for operational use cases. ## Managing Portfolios *** You can manage your Portfolios either through the API or through Lerian Console. ### Via API * [List Portfolios](/en/reference/midaz/list-portfolios) — View all Portfolios in your workspace. * [Retrieve a Portfolio](/en/reference/midaz/retrieve-a-portfolio) — Get details of a specific Portfolio. * [Create a Portfolio](/en/reference/midaz/create-a-portfolio) — Register a new Portfolio. * [Update a Portfolio](/en/reference/midaz/update-a-portfolio) — Edit an existing Portfolio. * [Delete a Portfolio](/en/reference/midaz/delete-a-portfolio) — Remove a Portfolio. ### Via Lerian Console You can view, create, edit, and delete Portfolios from the Portfolios page in the Midaz Module of Lerian Console. [**Learn more in the Managing Portfolios guide.**](/en/midaz/console/managing-portfolios) # Receiving events from Midaz Source: https://docs.lerian.studio/en/midaz/receiving-midaz-events Compare RabbitMQ direct bindings and the managed Streaming Hub for receiving Midaz ledger events, and choose the right transport for your integration setup. Midaz emits a stream of domain events. It emits one each time something important happens in the ledger. The ledger posts a transaction, a balance draws overdraft, or you create an account. Your system can react to those changes for reconciliation, notifications, downstream projections, or analytics. To react, consume the event stream instead of polling the REST API. This guide covers the two supported ways to receive Midaz events and how to choose between them: 1. **RabbitMQ direct** — bind your own queue to Midaz's AMQP exchanges. 2. **Streaming Hub** — subscribe through Lerian's managed fan-out service. ## The two approaches at a glance *** Midaz publishes domain events over two transports: * A **RabbitMQ (AMQP)** publish to topic exchanges that the Midaz deployment owns. * An **internal event bus** publish that the Streaming Hub consumes and fans out to per-tenant subscribers. Coverage differs by event. Transaction-lifecycle and overdraft events publish to **both** transports during the migration window. Audit append-log entries go to RabbitMQ only. The remaining catalog events — entity, route, holder, instrument, fee, limit, and rule events — go to the internal event bus only. That gives you two integration surfaces for the *same* underlying events: | Event | RabbitMQ direct | Streaming Hub | | -------------- | ------------------------------------ | ---------------------------------------------------- | | Transport | AMQP 0-9-1 | Webhook / Pull (HTTP) / SQS / RabbitMQ / EventBridge | | You connect to | Midaz's broker | A control-plane REST API | | Coupling | Tight (Midaz infra) | Loose (managed edge) | | Best for | Self-hosted / co-located deployments | External / SaaS integrators | If you do not operate your own Midaz broker, use the Streaming Hub. Direct RabbitMQ is for integrators who run inside or adjacent to the Midaz deployment and own the broker. ## Option A — RabbitMQ direct *** ### How it works The Midaz ledger publishes events through an internal producer to a set of **topic exchanges**. A consumer binds its own queue to the relevant exchange with a routing-key pattern. The consumer then reads messages over AMQP. Publisher facts (from the ledger service): * **Content type:** `application/json` * **Delivery mode:** persistent * **Headers:** the producer injects OpenTelemetry trace-context into every message * **Tenancy:** the producer is multi-tenant and publishes messages to a **tenant-specific vhost** Event exchanges and their routing keys: | Exchange | Routing key pattern | Emits | | ----------------------------------------- | ------------------------------------------------------------------- | ------------------------------- | | `transaction.transaction_events.exchange` | `midaz.transaction.` (e.g. `midaz.transaction.APPROVED`) | Transaction lifecycle envelopes | | `transaction.overdraft_events.exchange` | `midaz.balance.overdraft.` (`drawn` / `repaid` / `cleared`) | Overdraft events | | `audit.append_log.exchange` | `audit.append_log.key` | Audit append-log entries | **Midaz enables these exchanges by default.** Midaz treats each flag as enabled unless you explicitly set it to `false`. The bundled example environment configuration ships all three flags set to `false`. A stack that starts from that example emits nothing until you override the flags. ### For self-hosted operators A dedicated environment flag controls each exchange. Midaz treats a flag as enabled unless you explicitly set it to `false`. The bundled example configuration ships all three flags set to `false`: | Exchange | Env flag | | ----------------------------------------- | ------------------------------------- | | `transaction.transaction_events.exchange` | `RABBITMQ_TRANSACTION_EVENTS_ENABLED` | | `transaction.overdraft_events.exchange` | `RABBITMQ_OVERDRAFT_EVENTS_ENABLED` | | `audit.append_log.exchange` | `AUDIT_LOG_ENABLED` | On a stack that starts from the example configuration, set the flag to `true` in your Midaz deployment configuration. You can also remove the `false` value instead. This keeps the matching exchange active. ### Message shape Transaction events wrap the domain object in an envelope: ```json theme={null} { "source": "midaz", "eventType": "transaction", "action": "APPROVED", "timestamp": "2026-07-06T12:00:00Z", "version": "", "organizationId": "...", "ledgerId": "...", "payload": { "...full transaction JSON..." } } ``` The routing key encodes the action, for example `midaz.transaction.APPROVED` or `midaz.balance.overdraft.drawn`. You can bind narrowly and avoid a filter in code. ### Setup example Connection parameters come from the Midaz deployment's RabbitMQ configuration. These include `RABBITMQ_HOST`, `RABBITMQ_PORT_HOST` (the AMQP port used to dial the broker — `3003` in the bundled infrastructure; `RABBITMQ_PORT_AMQP` is the management port despite its name), a consumer user such as `RABBITMQ_CONSUMER_USER`, and `RABBITMQ_VHOST`. In multi-tenant deployments the producer resolves a per-tenant vhost; a single-tenant deployment uses the one static `RABBITMQ_VHOST`. For TLS, set `RABBITMQ_TLS=true` in multi-tenant mode, or `RABBITMQ_URI=amqps` in single-tenant mode. ```python theme={null} import pika params = pika.URLParameters("amqps://consumer:***@midaz-rabbitmq:5671/") conn = pika.BlockingConnection(params) ch = conn.channel() # The transaction and overdraft exchanges ship in Midaz's bundled RabbitMQ # definitions; declare passively or match their topology. The balance-operation # exchange also ships there but is internal — it feeds Midaz's own async balance # pipeline (RABBITMQ_TRANSACTION_BALANCE_OPERATION_*), so do not consume from it. # The audit append-log exchange is NOT in those definitions — declare it yourself. exchange = "transaction.transaction_events.exchange" # Bind a durable queue you own to the routing keys you care about. ch.queue_declare(queue="my-consumer.transactions", durable=True) ch.queue_bind( queue="my-consumer.transactions", exchange=exchange, routing_key="midaz.transaction.*", # all statuses ) def on_message(chan, method, props, body): handle(body) # application/json chan.basic_ack(method.delivery_tag) # native, per-message ack ch.basic_qos(prefetch_count=10) ch.basic_consume(queue="my-consumer.transactions", on_message_callback=on_message) ch.start_consuming() ``` ### When to use it * You **operate or co-locate** the Midaz deployment and already own the broker. * You want **native AMQP semantics** — per-message `ack`/`nack`, prefetch/QoS, dead-letter exchanges you control, competing consumers on one queue. * Your stack is already RabbitMQ-native and you want the lowest-latency, in-cluster hop. ### Trade-offs * Couples you to Midaz's internal broker topology and credentials. * The bundled example configuration ships the exchanges **disabled**. A stack that starts from it needs the operator to re-enable them. * No managed retry/dead-letter/auto-disable — you own delivery reliability on the consumer side. * Not viable for a third party that has only network access to a hosted Midaz. ## Option B — Streaming Hub *** ### How it works The Streaming Hub is Lerian's fan-out delivery edge. It consumes events from Midaz's internal event bus and delivers each matched event to a **per-tenant subscriber sink** that you register. You never touch Kafka or the broker. You register a subscription through a REST control plane and choose a delivery method. Supported sink kinds: * **`webhook`** — the hub POSTs each event to your HTTPS endpoint, **HMAC-signed** with a per-subscription signing secret. * **`pull`** — you poll `GET /v1/events`. The read is the acknowledgment (cursor-as-ack), with no inbound endpoint. * **`sqs`**, **`rabbitmq`**, **`eventbridge`** — the hub delivers into *your* AWS queue, *your* RabbitMQ broker, or *your* EventBridge bus. The hub adds delivery reliability on top of the raw stream. It provides a durable inbox with consume-once dedup, per-sink retry with backoff, dead-lettering on exhaustion, and auto-disable of failing endpoints. ### Events available Query the catalog to see the event types you can subscribe to: ```http theme={null} GET /v1/catalog Authorization: Bearer ``` ```json theme={null} { "events": [ { "eventType": "transaction.posted", "topic": "lerian.streaming.ledger_transaction.posted", "schemaVersion": "1.0.0", "schemaMajor": 1, "description": "A transaction was posted." } ] } ``` Midaz publishes a broad catalog keyed as `.` (all at schema `1.0.0`), including: * `organization.*`, `ledger.*`, `account.*`, `asset.*`, `portfolio.*`, `segment.*` (`created` / `updated` / `deleted`) * `operation-route.*`, `transaction-route.*` (`created` / `updated` / `deleted`) * `balance.created`, `balance.config-changed`, `balance.deleted` * `balance.overdraft-drawn`, `balance.overdraft-repaid`, `balance.overdraft-cleared` * `transaction.posted`, `transaction.committed`, `transaction.canceled`, `transaction.reverted` ### Authentication The control plane is **100% `lib-auth` JWT (Bearer)** — the hub mints no credential of its own. Present a plugin-auth-issued JWT on every `/v1` call: ```http theme={null} Authorization: Bearer ``` The hub derives the tenant only from the validated JWT claims (`tenantId`, then `owner` as a fallback), never from the request body. A machine client obtains its token from plugin-auth's **client-credentials** flow. It exchanges an application id and secret for a short-lived access token. ### Setup example — webhook subscription ```http theme={null} POST /v1/subscriptions Authorization: Bearer X-Idempotency: Content-Type: application/json { "name": "my-webhook", "sink_kind": "webhook", "endpoint": "https://hooks.example.com/lerian", "event_types": ["transaction.posted", "transaction.committed"], "schema_major": 1, "plan_tier": "standard" } ``` `201 Created` returns the subscription **and the signing secret exactly once**. Store it immediately. You can rotate the secret later, but you can never read it again: ```json theme={null} { "subscription": { "id": "0190b8e2-...", "verification_state": "active", "...": "..." }, "signingSecret": "whsec_...ONCE..." } ``` Verify the HMAC signature on every inbound webhook with that secret. Do this before you trust the payload. Use `POST /v1/subscriptions/:id/ping` to send a signed synthetic event. This confirms that your endpoint works. ### Setup example — pull subscription (serverless-friendly) Create it with `"sink_kind": "pull"` (omit `endpoint` — the server synthesizes one), then poll: ```http theme={null} GET /v1/events?subscription_id=&limit=100 Authorization: Bearer ``` ```json theme={null} { "events": [ { "seq": 42, "ceId": "...", "eventType": "transaction.posted", "receivedAt": "2026-07-06T12:00:00Z", "payload": { "...": "..." } } ], "next_cursor": 42 } ``` The read is the ack: the highest `seq` returned advances a durable, monotonic cursor. Replay the server-issued `next_cursor` as `?after=` on the next call. Each event carries `ceId` for your own dedup. ### Queue sinks (SQS / RabbitMQ / EventBridge) You create a queue-kind subscription without a credential, and it starts in `pending_verification`. It emits nothing until you supply an outbound credential via `PUT /v1/subscriptions/:id/credential`. The hub probes that credential (connect and auth) and, on success, flips the subscription to `active`. For a RabbitMQ sink, set `endpoint` to `"/"`. The broker host lives in the encrypted credential. For AWS sinks, fetch the IAM trust policy and `ExternalId` from `GET /v1/subscriptions/:id/setup-artifacts`, then wire the cross-account grant before the credential PUT. ### When to use it * You are an **external integrator** with only network access to a hosted Midaz. * You want **serverless** delivery — a webhook endpoint or an HTTP pull loop, no broker to run. * You want **managed reliability** — dedup, retry/backoff, dead-lettering, auto-disable, signed webhooks — without building it yourself. * You want to fan the same events into **AWS-native** infrastructure (SQS/EventBridge). ### Limitations * No SSE or WebSocket transport — delivery is webhook push or HTTP pull (plus the queue sinks). * The catalog is **global** (identical regardless of which tenant authenticates). * Pull consumers own their cursor position — a forward seek past the cursor permanently skips the gap. Use `?after=` to replay from any prior `seq`. *** ## Decision table | Criterion | RabbitMQ direct | Streaming Hub | | ----------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------- | | **Your stack** | Already RabbitMQ / AMQP-native, in-cluster | Anything that speaks HTTPS (or AWS SQS/EventBridge) | | **Deployment model** | You run / co-locate Midaz and own the broker | Hosted / SaaS Midaz, external integrator | | **Granular ack** | ✅ Native per-message `ack`/`nack`, prefetch, DLX | Partial — webhook 2xx or pull cursor-as-ack; no per-message nack | | **Serverless** | ❌ Needs a long-lived AMQP consumer | ✅ Webhook endpoint or stateless pull loop | | **Simplicity to start** | ❌ Broker creds, vhost, exchange must be enabled | ✅ One authenticated `POST /v1/subscriptions` | | **Managed reliability** | ❌ You own retry / DLQ / backoff | ✅ Dedup, retry/backoff, dead-letter, auto-disable | | **Latency** | Lowest (in-cluster hop) | Slightly higher (fan-out edge) | | **Auth model** | Broker username/password (+ optional TLS) | plugin-auth JWT (Bearer); signed webhooks | | **Event scope** | Transaction / overdraft / audit exchanges (must be enabled) | Full Midaz catalog via `/v1/catalog` | | **Fan into AWS** | ❌ Build it yourself | ✅ SQS / EventBridge sinks | **Rule of thumb:** if you own the broker and want raw AMQP control, go **RabbitMQ direct**. In every other case — external integration, serverless, managed reliability, AWS delivery — use the Streaming Hub. *** ## Security considerations * **Least-privilege credentials.** For RabbitMQ direct, connect with a **consumer-scoped** user (not the publisher/default user), restricted to the tenant vhost, and enable TLS (`amqps://`). For the Streaming Hub, scope the plugin-auth application to the tenant it represents and rotate the client secret. * **Verify webhook signatures.** Always validate the HMAC-v1 signature with your per-subscription signing secret before you act on a webhook. Treat an unsigned or mismatched request as hostile. * **Guard the signing secret.** The hub shows it once at create or rotate, and never again. Store it in a secrets manager, then rotate it via `POST /v1/subscriptions/:id/secret/rotate` (24h dual-sign overlap) if the secret leaks. * **HTTPS-only endpoints.** Webhook sinks must be `https://`. The hub SSRF-validates endpoints at create and at credential PUT, and it rejects plaintext or private targets. * **Tenant isolation is claim-derived.** The hub reads tenant identity only from validated JWT claims (or `ce-tenantid` on ingest), never from the request body, so never send `tenant_id` in a payload. * **Idempotency & dedup.** Send `X-Idempotency` on mutating control-plane calls. On the data plane, dedup on `ceId` (Streaming Hub), the message id, or your own key (RabbitMQ), because both transports are at-least-once. * **Don't expose Midaz internals.** Never share the internal balance-operation exchange or broker credentials with external consumers, and front third parties with the Streaming Hub instead. # Recommended workflow Source: https://docs.lerian.studio/en/midaz/recommended-workflow Follow the recommended Midaz onboarding workflow to create your Organization, Ledgers, Assets, Account Types, and Accounts in the correct order, step by step. You have explored how Midaz works, from its core entities to its architecture and security. This page turns that knowledge into a working setup. A structured workflow keeps every transaction clear, traceable, and auditable from the start. Follow the steps below in order. They set up your Organization, Ledgers, Assets, Account Types, and Accounts. ## Setting up and managing Ledgers *** When you onboard to Midaz, follow these steps to build a solid base for your financial infrastructure: Workflow for setting up and managing ledgers when onboarding to Midaz ### Step 1: [Set up the Organization](/en/reference/midaz/create-an-organization) Your organization in Midaz represents your bank or business entity. If you have not created it yet, set it up in the Console or API. Make sure the core details are accurate, such as the legal name and identifiers. This context underpins all ledger operations. ### Step 2: [Create the Ledgers](/en/reference/midaz/create-a-ledger) Create a Ledger under your organization to hold financial records. Most setups start with a primary ledger for all customer-facing operations (for example, "Main Banking Ledger"). At first it contains no accounts. It serves as the base layer for all financial activity. ### Step 3: [Define Asset types](/en/reference/midaz/create-an-asset) Register the assets your ledger supports. These can include currencies (for example, BRL, USD), points, or crypto tokens. You use these assets across your account and transaction configuration. You can add more assets as your product portfolio grows. ### Step 4: [Create Account Types](/en/reference/midaz/create-an-account-type) To enforce account classification rules, define your **Account Types**. These are structured categories, for example `user_wallet`, `treasury`, or `revenue`. You reference them later during account creation or transaction validation. When you **enable** Account Type validation, each new account must use a registered Account Type. By default, Account Types are optional. This keeps classification consistent across your ledger. Each Account Type has a `keyValue`. The `keyValue` must be unique and alphanumeric, with dashes and underscores allowed. You use these identifiers in validation flows and transaction routes. ### Step 5: Configure Accounting Routes Use **Operation Routes** and the **Transaction Route** (the `transactionRoute` resource in the API) to define and enforce validation for your transactions. * First, [**create the Operation Routes**](/en/reference/midaz/create-an-operation-route). Each Operation Route validates one leg of the transaction. For example, the debit must come from an account of type `user_wallet`, and the credit must go to the alias `@external/BRL`. * Then, [**create the Transaction Route**](/en/reference/midaz/create-transaction-route). It combines the individual operation routes into a complete flow. An example is a cashout transaction with specific debit and credit validations. After you configure routing and enable validation on the ledger, all transactions must match the defined routing structure. Midaz rejects any transaction that does not match. The `route` field in the transaction payload must include the ID of the **Transaction Route** you created. ### Step 6: Manage your Ledgers Keep your ledgers clean and scalable with the following best practices: * Use Midaz's RBAC system to assign permissions and control access per ledger. * Plan separate ledgers when needed, for example "Testing", "Internal", or "Multi-entity". Document their ownership. * Review and reconcile ledger data regularly, especially when you integrate with external settlement systems. * Midaz enforces double-entry logic, so every movement is fully traceable. Do not change balances manually. ## Configuring Accounts, Portfolios, and Segments *** This section shows how to structure customer data and product entitlements in Midaz. Workflow for configuring accounts, portfolios, and segments to structure customer data and product entitlements in Midaz ### Step 1: [Create a customer Portfolio](/en/reference/midaz/create-a-portfolio) Create a **Portfolio** for each new customer. Portfolios act as the logical containers for accounts. Link each portfolio to your CRM or internal system through metadata (for example, an Entity ID). ### Step 2: [Set up customer Accounts](/en/reference/midaz/create-an-account) Each financial product or asset the customer holds becomes a separate **Account** under their portfolio. * A checking account in BRL and a savings account in USD are separate accounts. * If you enable [Account Types](/en/reference/midaz/create-an-account-type), the `type` field must match one of the pre-registered values. * Use account aliases to simplify downstream operations. Account Types standardize account categorization across your ecosystem. They improve routing, reporting, and integrations. ### Step 3: [Apply segmentation](/en/reference/midaz/create-a-segment) Use Segments to categorize portfolios or accounts (for example, `standard`, `vip`, `enterprise`). These tags drive dynamic logic based on customer segmentation, such as fee exemptions or tiered interest. ### Step 4: Establish sub-accounts (optional) For organizations or special cases, you can configure sub-accounts. Use clear naming conventions to keep traceability (for example, "Acme USD - Payroll"). ### Step 5: [Set the initial balance](/en/reference/midaz/create-a-transaction-using-json) Use the **Transactions API** to add the opening balance. This keeps traceability and double-entry compliance: * Debit the funding source. * Credit the new account. * Do not update balances manually. Midaz tracks every value through transactions. ### Step 6: Verify and review Before you run operations, check the following: * Confirm each account has the correct assets and types. * Validate account balances. * Check that you applied segmentation. * Confirm the setup in the API or the Console. ### Step 7: Ongoing Account management Manage lifecycle operations through the API or Console to keep system integrity: * Update account details as needed. * Close accounts only once the balance reaches zero. * Reclassify portfolios/accounts if segmentation changes. ## Implementing double-entry transaction management *** Midaz guarantees ledger integrity through strict enforcement of double-entry logic. Every transaction must contain at least one debit and one credit operation. Workflow for implementing double-entry transaction management, where every transaction carries at least one debit and one credit operation ### Step 1: Structure the transaction Structure your **Transaction** object: * **After you configure your Transaction Route**, insert its ID into the `route` field of your transaction payload. * Define the source and destination accounts, with their asset and value details. * Add descriptive metadata to keep traceability and clarity. ### Step 2: Choose your integration method Use the **Transactions API** for programmatic transaction creation. ### Step 3: Enforce double-entry validation Midaz checks that the sum of credits matches the sum of debits. Midaz rejects a transaction that has a mismatch or a structural violation. ### Step 4: Implement idempotency Use idempotency keys to prevent duplicates from network retries. Midaz recognizes a repeated key and replays the original response instead of processing the request twice. ### Step 5: Track and reconcile transactions Use built-in tools to: * Track submitted and posted transactions. * Audit metadata and timestamps. * Reconcile transaction batches for compliance. ### Step 6: Submit corrections via reversals Do not edit or delete transactions. Submit a reversal instead. Midaz links each reversal to its original transaction for full traceability. ### Step 7: Optimize complex workflows Use multi-operation transactions to: * Process interest accruals. * Execute batched payouts. * Handle conditional flows with routing logic. Midaz keeps atomicity across all legs. Either the full transaction posts, or nothing does. # Rest API Source: https://docs.lerian.studio/en/midaz/rest-api Integrate with Midaz through a versioned REST API — JSON payloads, explicit versioning, and a predictable, resource-based design. The Midaz REST API gives you programmatic access to the ledger. It uses standard HTTP methods and status codes. ## Key features *** * **RESTful design**: The API follows REST principles for a predictable structure. * **JSON format**: The documented API uses JSON for requests and responses. * **Versioning**: Explicit version prefixes keep changes backward-compatible. The API exposes the core Midaz resources: organizations, ledgers, accounts, assets, portfolios, transactions, and operations. Each resource has its own endpoints. For technical details on the available endpoints, see the [API Reference](/en/reference/introduction). # Scalability strategies Source: https://docs.lerian.studio/en/midaz/scalability-strategies Scale Midaz to millions of Accounts and Transactions per day — partitioning by Ledgers, horizontal service scaling, and read replicas. Midaz handles millions of accounts and transactions per day. You start small and scale horizontally as demand grows, with no re-architecture. For a business overview, see [About Midaz](/en/midaz/about-midaz). How you deploy and use Midaz affects its performance. To handle large banking operations, use ledgers to segment workloads and scale each service on its own. ## Structuring entities for High Transaction Volumes *** ### Partitioning by Organizations, Ledgers, and Accounts Does your bank process millions of transactions each day? Distribute the workload across many accounts, ledgers, or organizations. For example, segment customers by last-name initial, or assign a separate ledger to each region. Each Midaz ledger is an independent book. Midaz processes the transactions in a ledger together. This split stops a single ledger from becoming a bottleneck. Midaz scales out: different ledgers can run on separate instances or database partitions. ### Lock-oriented Operations Midaz applies each balance batch atomically in a single Redis Lua script, so a batch either lands in full or rolls back in full. Concurrency is handled optimistically: each balance carries a version, and on overdraft repayment paths a version mismatch aborts the whole batch with error `0174` so the caller re-reads state and retries. The `@external` account skips the balance-floor and overdraft checks. It can go negative without limit, so transactions against it never fail on available funds. Each transaction still commits atomically — all operations apply, or none do. ### Horizontal scaling of services Midaz uses a microservices architecture. You scale each component on its own — for example, transaction processing and queries. Start small. As demand grows, scale the services that need it. For high loads, deploy several instances of the transaction service behind a load balancer. Use Kubernetes (K8s) pod scaling. ### CQRS and read replicas Midaz uses CQRS (Command Query Responsibility Segregation) to separate write and read operations. You optimize each path on its own. Route heavy reporting and query loads to read replicas. This keeps transaction commits fast. Reads do not slow down writes, and writes do not slow down reads. ### Batching and N:N transactions Where possible, batch related operations into a single transaction. This reduces overhead. Use N:N transactions for bulk work such as mass payouts. For example, one batch of 100 debits and 100 credits is often faster than 100 separate transactions. This works when the operations belong to the same logical set. Midaz processes these N:N transactions in one atomic commit. ### Monitoring and iterative scaling Monitor the key metrics: transaction latency, throughput, resource use (CPU and memory), and database performance. Use the Midaz observability tools (OpenTelemetry and Grafana), or send the metrics to your own monitoring stack. Scale out or up before you reach a performance threshold. Midaz is modular and source-available, so you deploy the parts you need. ## Handling multi-currency and multi-entity Operations *** ### Multi-currency strategies Midaz supports multi-currency transactions with a separate asset account for each currency. To scale well: * Configure every currency asset you need. * Handle currency-specific logic — such as rounding and conversion rates — in your integration layer. * For frequent currency conversion, such as forex trading, run the conversion in a separate service. Midaz records the debits and credits, but an external FX-rate service supplies the rates. * You rarely need a separate ledger per currency, unless policy requires it. Instead, use account types and the transaction-routing API to group accounts by currency for clear reporting. * If one currency covers most transactions, keep all currencies in the same ledger. If multi-currency operations grow too complex, split them into a separate ledger per currency group. ### Multiple Entities: Consolidation and separation For a bank that operates across countries or legal entities: * Use the organization hierarchy to segment entities. Give each entity its own base currency, local assets, and ledgers. * Model inter-entity transactions as external movements. One organization credits its external account, and the other debits its own. * You can run several organizations on one Midaz instance. Allocate enough resources: the microservices process all requests together, so total volume matters. * You can deploy a separate Midaz instance per isolated entity, but you lose unified visibility. One cluster with internal organization separation is usually more practical. ## Performance optimization best practices *** ### Indexing and queries Use the Midaz APIs to read data, not direct database queries. These APIs carry predefined indexes for the core identifiers: accounts, portfolios, and transactions. ### Indexing custom metadata fields Midaz creates no default index on the metadata collections — metadata documents are keyed by `entity_id`. Create the indexes you need explicitly through the Metadata Index API. By default, Midaz does **not** index **custom metadata fields**. Clients fill these fields freely as key-value pairs. Frequent queries or filters on non-indexed fields can trigger full collection scans. These scans slow down as the data volume grows. Create an index when you regularly use a custom metadata field in **search filters or sort order**. With an index, MongoDB finds the documents fast and cuts query response time. **Recommendations:** * Index only the metadata fields you query or sort on often. * Do not index rarely-accessed fields, because each extra index adds write overhead. Manage metadata indexes through dedicated APIs: * [Create a Metadata Index](/en/reference/midaz/create-a-metadata-index) * [List Metadata Indexes](/en/reference/midaz/list-metadata-indexes) * [Delete a Metadata Index](/en/reference/midaz/delete-a-metadata-index) ### Testing at scale Before full deployment, simulate high traffic with load-testing tools. Create millions of accounts, run heavy transaction volumes, and monitor performance. The Midaz architecture (microservices and CQRS) targets high-scale operations, but real-world tests help you tune your setup. Look for linear scalability: more application instances should raise throughput in proportion. # Segments Source: https://docs.lerian.studio/en/midaz/segments Group Accounts into Segments to deliver tailored benefits — VIP rates, student perks, fee waivers, and personalized rules across Portfolios. A **Segment** is a group of Accounts that share a characteristic, such as a product tier or a customer type. Financial institutions use Segments to apply tailored rules, services, and conditions to each group. Each Segment has a **Segment ID**, a unique identifier. Financial institutions use this ID to track and manage each Segment. #### Examples * **Black or diamond (Cards)**: a premium group of card accounts for customers who want exclusive benefits. * Included benefits: VIP lounge access, insurance coverage, a dedicated concierge, and loyalty rewards. * **Student account (Banking Accounts)**: a group of banking accounts for students. * Included benefits: no account fees, cashback on education purchases, and discounts on cinemas and events. ## Segment structure *** * **Segment > Accounts:** You tag each Account with a `segmentId` (a Segment UUID) to add it to a Segment. * A Segment belongs to one Ledger. Accounts in different Portfolios can share the same Segment. ## Key characteristics *** * Segments give you a scalable way to apply tailored policies to Accounts. * A Segment can include many Accounts. ## Managing Segments *** You manage your Segments through the API or Lerian Console. ### Via API * [List Segments](/en/reference/midaz/list-segments) — View all Segments in your workspace. * [Retrieve a Segment](/en/reference/midaz/retrieve-a-segment) — Get the details of a specific Segment. * [Create a Segment](/en/reference/midaz/create-a-segment) — Register a new Segment. * [Update a Segment](/en/reference/midaz/update-a-segment) — Edit an existing Segment. * [Delete a Segment](/en/reference/midaz/delete-a-segment) — Remove a Segment you no longer use. ### Via Lerian Console You view, create, edit, and delete Segments on the Segments page in the Midaz Module of Lerian Console. [**Learn more in the Managing Segments guide.**](/en/midaz/console/managing-segments) # Structuring bank operations with Midaz Source: https://docs.lerian.studio/en/midaz/structuring-bank-operation Design Organizations, Ledgers, Portfolios, and Segments for retail and corporate banking — best practices for scalable hierarchies. Midaz supports retail and corporate banking through a clear entity hierarchy. This page gives best practices to structure organizations, ledgers, accounts, portfolios, and segments. A good structure keeps operations simple as you scale. ## Optimal hierarchy of Organizations and Ledgers *** ### Single vs. multiple Organizations A bank that operates as a single legal entity needs one Organization in Midaz. For a banking group with subsidiaries, use a parent organization over several child organizations. This structure matches your corporate governance. It also isolates data per entity. The organization structure should reflect your corporate structure. ### Using multiple Ledgers Within an organization, choose the number of ledgers from your operational needs. Keep a **primary ledger** for customer transactions. Add ledgers for special purposes, such as treasury operations or regulatory segmentation. Use multiple ledgers only when necessary to keep complexity low. A transfer between ledgers needs external orchestration through the API. ## Efficient account structuring for retail and corporate banking *** ### Retail customers – Portfolio per customer For retail banking, create a **Portfolio for each customer**. Each portfolio holds one account per asset type, such as checking, savings, or credit card. This structure makes balances easy to read per customer. Use [CRM](/en/midaz/crm/crm-data-security) to manage personal data such as tax IDs, addresses, and banking aliases — all in one place. ### Corporate clients – hierarchical accounts For corporate banking, use **child accounts** within portfolios to reflect internal structure. A company can hold a parent account for its main funds. It can add child accounts for subdivisions, such as payroll or expense tracking. ### Internal accounts Create internal accounts for revenue, expenses, and settlement flows. Named accounts like "Fee Income – USD" or "Interest Expense – USD" keep financial reporting clear. ## Worked example: a typical digital bank *** This example shows how a digital bank can structure its Midaz deployment. The bank offers checking accounts, savings, and Pix transfers. ``` Organization: "Neobank S.A." │ ├── Ledger: "Main Ledger (BRL)" │ │ │ ├── Segments │ │ ├── "Standard" (default tier) │ │ ├── "VIP" (premium customers) │ │ └── "Business" (corporate accounts) │ │ │ ├── Account Types │ │ ├── "checking" → everyday transactions │ │ ├── "savings" → interest-bearing deposits │ │ ├── "settlement" → internal clearing │ │ └── "fee-revenue" → bank fee collection │ │ │ ├── Internal Accounts │ │ ├── Settlement Account (clearing for Pix, TED) │ │ ├── Fee Revenue Account │ │ └── Interest Expense Account │ │ │ └── Customer Portfolios │ ├── Portfolio: "Maria Silva" │ │ ├── Checking Account (BRL) — Segment: VIP │ │ └── Savings Account (BRL) — Segment: VIP │ │ │ └── Portfolio: "Tech Corp Ltda." │ ├── Main Checking (BRL) — Segment: Business │ ├── Payroll Account (BRL) — Segment: Business │ └── Expense Account (BRL) — Segment: Business │ └── Ledger: "Treasury" └── FX and interbank positions ``` ### How a Pix transfer flows through this structure Maria sends R\$ 500 via Pix from her checking account. The "Pix Out" Transaction Route applies. It defines a debit from Maria's checking account and a credit to the Settlement Account. It can also add a fee operation. Midaz creates four operations atomically. It debits R$ 500 from Maria and credits R$ 500 to Settlement. It also debits a R$ 0.50 fee from Maria and credits R$ 0.50 to Fee Revenue. Midaz emits a transaction event. The bank's notification service reads the event and sends Maria a push notification. An external integration reconciles the Settlement Account with the central bank's Pix infrastructure. ### Decision guide: when to split ledgers | Scenario | Recommendation | | :---------------------------------------- | :------------------------------------------------------------- | | Single currency, single business unit | One ledger is enough | | Multiple currencies with occasional FX | One ledger, separate asset accounts per currency | | Separate legal entities or subsidiaries | One ledger per entity, under separate organizations | | Regulatory requirement for data isolation | Separate ledgers, or separate organizations for full isolation | | Treasury and trading operations | Dedicated treasury ledger alongside operational ledger | When in doubt, start with a single ledger. You can always split later as operational complexity grows. If you split too early, you add orchestration overhead without clear benefit. # Accounting Routes Source: https://docs.lerian.studio/en/midaz/transaction-routing-entities Validate every Transaction with Accounting Routes and Operation Routes — enforce structure and business rules before recording any movement. Accounting Routes are Midaz's two-layer validation system for financial transactions. **Accounting Routes** define the full transaction pattern. **Operation Routes** validate each operation inside that pattern. Together they keep every transaction structurally correct and compliant with your business rules. **Naming:** The Lerian Console and product documentation call this concept **Accounting Routes**. In the API and SDKs, the `transactionRoute` resource represents the transaction-level route, with `transaction-route` endpoints. The two terms refer to the same thing. * **Accounting Routes** define the complete structure of a transaction: the required sequence of operations that forms a valid financial event. * **Operation Routes** define the rules for each operation (or "leg") of that transaction. Each rule sets the expected account type or specific account, the accounting annotation, and the debit or credit side. When you submit a transaction, Midaz validates it in two layers. The Accounting Routes layer checks that the overall structure matches the predefined pattern. The Operation Routes layer checks that each component meets account requirements and business rules. If any part of the transaction fails these checks, Midaz rejects it before it records the transaction. This protects the integrity of your ledger and does not limit its flexibility. You define the validation patterns through Operation Routes and Accounting Routes. Midaz checks that your transactions comply with these rules before it processes them. ## What are Accounting Routes for? *** Accounting Routes provide structured control over your financial operations by separating transaction logic from business code. Instead of hardcoding validation rules in your application, you configure reusable patterns. These patterns make every financial movement follow your organization's requirements. These entities link Transactions and Operations from the Midaz ledger to higher-level abstractions. These abstractions help you integrate specialized plugins and external systems, especially for **accounting and treasury**. The structured annotations and classifications create a standardized vocabulary that other components can understand and use. This approach delivers: * **Consistency**: All transactions follow predefined structures regardless of where they originate. * **Flexibility**: Adapt your ledger design to match your business needs without code changes. * **Integrity**: Automatic validation prevents malformed transactions from affecting your ledger. * **Maintainability**: Centralized configuration makes it easier to update financial rules as your business evolves. * **Interoperability**: Business-semantic fields let you integrate accounting plugins and external financial systems. Accounting Routes keep your financial data structured and validated for simple transfers and complex multi-party transactions. They also provide the semantic foundation for advanced integrations. ## Working with Accounting Routes *** To use Accounting Routes, you complete a one-time setup and then execute transactions. The steps below show the full process. ### Initial Setup #### 1. Configure Ledger for transaction route validation To activate transaction route validation for a specific Ledger, enable the validation settings through the [Ledger Settings API](/en/midaz/ledgers#ledger-settings). This controls whether transactions in that Ledger must comply with your configured routes. ```json PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings theme={null} { "accounting": { "validateRoutes": true, "validateAccountType": true } } ``` * **`validateRoutes`**: When enabled, every transaction must reference a valid transaction route. * **`validateAccountType`**: When enabled, Midaz rejects an account whose `type` is not a registered Account Type. This gates **account creation**, not transactions — the `account_type` rule on an Operation Route is enforced by `validateRoutes`, independent of this flag. Settings changes need no redeployment: you update them at any time through the API. The write invalidates the settings cache, but reads are cached for **5 minutes**, so allow up to that long for a change to be observed by every replica. #### 2. Create Operation Routes Create Operation Routes that define validation rules and behavior for individual transaction components. **Key fields:** * **title**: Brief label that identifies the operation route. * **code** (deprecated): a legacy external reference kept for backward compatibility. The engine does **not** write it to operations. Instead, it records the resolved rubric's `code` (from `accountingEntries`) as `routeCode` on each operation. * **description**: Optional detailed explanation. * **metadata**: Key-value pairs for business context and custom categorization. * **operationType**: The accounting direction for this route — `source`, `destination`, or `bidirectional`. * `source` — Identifies accounts where funds originate (debit side). * `destination` — Identifies accounts that receive funds (credit side). * `bidirectional` — Applies to both sides of the transaction, as both source and destination. * **account**: Optional validation rules that set a required account type or a specific account. * **ruleType**: Type of account validation rule (`account_type`, `alias`). * **validIf**: The expected value that must match for validation to pass. * **accountingEntries**: Optional accounting entries for each action type. See [Accounting Entries](#4-configure-accounting-entries-actions) below. Configure account rules based on your needs: **Option A: No Account Rule** If you don't need account validation for the operation route, omit the account object: ```json JSON theme={null} { "title": "Fee Collection", "description": "Operation route for collecting service fees from user transactions", "metadata": { "businessUnit": "payments", "category": "revenue" }, "operationType": "source" } ``` **Option B: Account Validation Rule** If you need account validation for the operation, configure account rules based on your ledger setup: * **Target Specific Account** Validate against a specific account using its alias. ```json JSON theme={null} { "title": "Fee Revenue Collection", "description": "Operation route for crediting collected fees to revenue account", "metadata": { "businessUnit": "payments", "category": "revenue" }, "operationType": "destination", "account": { "ruleType": "alias", "validIf": "@external/BRL" } } ``` * **Target Account Type** Validate against specific account types. ```json JSON theme={null} { "title": "User Cashout Fee", "description": "Operation route for collecting fees from user cashout transactions", "metadata": { "businessUnit": "payments", "category": "fee" }, "operationType": "source", "account": { "ruleType": "account_type", "validIf": ["user_wallet", "asset"] } } ``` **Option C: With Accounting Entries** Attach accounting entries directly to the operation route through the `accountingEntries` field. This field maps each transaction-lifecycle stage to the correct double-entry accounting codes. See [Configure Accounting Entries (Actions)](#4-configure-accounting-entries-actions) below for the full action-type model, the debit/credit requirements, and the validation matrix. A route with accounting entries configured: ```json JSON theme={null} { "title": "Pix Cash-in - Current Account", "description": "Operation route for receiving Pix payments into current account", "operationType": "source", "accountingEntries": { "direct": { "debit": { "code": "1.1.001", "description": "Cash - Available funds" }, "credit": { "code": "3.1.001", "description": "Service Revenue" } }, "hold": { "debit": { "code": "1.1.002", "description": "Clearing Values" }, "credit": { "code": "2.1.001", "description": "Pending Obligations" } }, "commit": { "debit": { "code": "2.1.001", "description": "Pending Obligations" }, "credit": { "code": "3.1.001", "description": "Service Revenue" } }, "cancel": { "debit": { "code": "2.1.001", "description": "Pending Obligations" }, "credit": { "code": "1.1.002", "description": "Clearing Values" } } }, "account": { "ruleType": "alias", "validIf": "@current_account" }, "metadata": { "channel": "pix" } } ``` The `operationType` field also supports `bidirectional`. A bidirectional route operates in both directions. Use it for routes that both send and receive, or for operations that you may need to reverse. #### 3. Build Accounting Routes Complete your setup by combining Operation Routes into Accounting Routes (the `transactionRoute` resource in the API). These define your complete transaction patterns. Each pattern maps how operations work together to form balanced financial events that match your business processes. The `operationRoutes` field uses an array of objects with `operationRouteId` rather than a plain array of UUID strings. ```json JSON theme={null} { "title": "Fee Transaction", "description": "Complete transaction for collecting fees from user cashout operations", "metadata": { "transactionType": "cashout_fee", "businessFlow": "withdrawal_processing" }, "operationRoutes": [ { "operationRouteId": "0197e6aa-1695-734a-a8c3-8c79e0ad32c2" }, { "operationRouteId": "0197e675-37cc-71d7-96c2-f58000f33aa0" } ] } ``` #### 4. Configure Accounting Entries (Actions) Each Operation Route can include **Accounting Entries**. These structured rubrics define how Midaz records debit and credit entries for each transactional event: `direct`, `hold`, `commit`, `cancel`, and `revert`, plus three supplementary keys — `overdraft`, `block`, and `unblock` — that describe accounting impact but are **not** valid transaction-route actions. The engine uses them to resolve which accounts it debits and credits for each action. They also determine the `routeCode` and `routeDescription` annotations on each operation. The `accounting.validateRoutes` setting in the [Ledger Settings](/en/midaz/ledgers#ledger-settings) controls this behavior. When you enable it, Midaz rejects an operation route that is missing or does not match, and returns `0117 ErrAccountingRouteNotFound`. When you disable it, route resolution is best-effort. A missing rubric leaves `routeCode` empty and does not stop the transaction. The **[Accounting Entries](/en/midaz/accounting-entries)** page documents the full model in detail. This includes the accounting-entry actions, the debit/credit requirements per operation type, the graceful and strict validation modes, and configuration examples. This section covers only how rubrics attach to Operation Routes. At the route level, you supply accounting entries through the `accountingEntries` block. See **Option C** under [Create Operation Routes](#2-create-operation-routes) above. Each action takes one entry with a `debit` rubric, a `credit` rubric, or both, depending on the route's `operationType`: * **Source** routes require the **debit** rubric. * **Destination** routes require the **credit** rubric. * **Bidirectional** routes require **both** debit and credit rubrics. ##### Accounting entries validation matrix Not every combination of `operationType` and action is valid. Midaz enforces a strict validation matrix when you create or update an Operation Route. If the rules do not match, Midaz rejects the request before it persists the route. This matrix is critical for integrators. An invalid combination returns error `0166` (field required) or `0162`/`0165` (scenario not allowed for the direction). **source** | Action | Debit | Credit | Notes | | :------- | :------- | :------- | :------------------------------------------------------ | | `direct` | Required | Optional | Standard one-step transaction at origin | | `hold` | Required | Required | Reserves funds — moves available → on\_hold | | `commit` | Required | Optional | Finalizes a two-phase transaction | | `cancel` | Required | Required | Releases reserved funds — moves on\_hold → available | | `revert` | — | — | Not allowed (error `0165`). Use `bidirectional` instead | **destination** | Action | Debit | Credit | Notes | | :------- | :------- | :------- | :------------------------------------------------------ | | `direct` | Optional | Required | Standard one-step transaction at destination | | `hold` | — | — | Not allowed (error `0162`) | | `commit` | Optional | Required | Finalizes a two-phase transaction | | `cancel` | — | — | Not allowed (error `0162`) | | `revert` | — | — | Not allowed (error `0165`). Use `bidirectional` instead | **bidirectional** | Action | Debit | Credit | Notes | | :------- | :------- | :------- | :---------------------------------- | | `direct` | Required | Required | Both sides of the double entry | | `hold` | Required | Required | Both sides of the double entry | | `commit` | Required | Required | Both sides of the double entry | | `cancel` | Required | Required | Both sides of the double entry | | `revert` | Required | Required | Only direction that supports revert | If an entry has neither `debit` nor `credit`, Midaz rejects it, regardless of operation type or action. **Additional rules:** * **Reserve group atomicity**: On `source` and `bidirectional` routes, if you define `hold`, you must also define `commit` and `cancel` (and vice versa). These three actions form an atomic group there; you can't configure one without the others. On `destination` routes, `hold` and `cancel` are not allowed (error `0162`), so `commit` can be configured without them; it still requires `direct` per the rule below. * **Direct is mandatory**: If you define any other action (`hold`, `commit`, `cancel`, `revert`, `overdraft`, `block`, `unblock`), you must also define `direct`. It serves as the baseline entry for the operation route. * **`overdraft` requires both rubrics** on every `operationType`, including `source` and `destination`. * **`block` and `unblock` mirror `direct`**: debit on a `source` route, credit on a `destination` route, both on `bidirectional`. * Any key outside these eight is rejected with error `0053` (Unexpected Fields). When you design your operation routes, start with the `direct` action. Add `hold`/`commit`/`cancel` only if you need two-phase transaction support. Add `revert` only on `bidirectional` routes. ### Ongoing Operations #### 5. Execute Validated Transactions With your routing configuration in place, you can now submit transactions. In the transaction request, **include the ID of the Accounting Route you created**. Midaz then validates the transaction against your routing patterns. This keeps all financial operations consistent and correct. For the Accounting Route and Operation Routes configured above, Midaz composes the following validation structure: ```bash text theme={null} Transaction Route: "Fee Transaction" (ID: 5656daa5-5b2a-4637-955f-e43bafceaf5d) ├── Operation Route 1: "User Cashout Fee" (ID: 0197e6aa-1695-734a-a8c3-8c79e0ad32c2) │ ├── Type: source │ ├── Account Rule: account_type ["user\_wallet", "asset"] │ └── Validates: source operations in transactions └── Operation Route 2: "Fee Revenue Collection" (ID: 0197e675-37cc-71d7-96c2-f58000f33aa0) ├── Type: destination ├── Account Rule: alias "@external/BRL" └── Validates: destination operations in transactions ``` For route properties on Midaz transactions, an appropriate payload request: ```json JSON expandable theme={null} { "routeId": "5656daa5-5b2a-4637-955f-e43bafceaf5d", "description": "Cashout fee collection transaction", "send": { "asset": "BRL", "value": "10", "source": { "from": [ { "accountAlias": "@user/wallet_123", "amount": { "asset": "BRL", "value": "10" }, "description": "Fee debit from user wallet", "routeId": "0197e6aa-1695-734a-a8c3-8c79e0ad32c2" } ] }, "distribute": { "to": [ { "accountAlias": "@external/BRL", "amount": { "asset": "BRL", "value": "10" }, "description": "Fee credit to revenue account", "routeId": "0197e675-37cc-71d7-96c2-f58000f33aa0" } ] } } } ``` When you submit this transaction, Midaz validates two things. The `@user/wallet_123` account must match the `user_wallet` account type rule. The `@external/BRL` account must match the exact alias. Both checks confirm that the transaction follows your routing patterns. ##### Route fields on operations When you enable route validation and configure accounting entries, every processed operation includes two extra fields. Midaz populates these fields from the matched rubric: * **routeCode** — The `code` of the resolved `AccountingRubric` for that operation's action and direction. * **routeDescription** — The description of the resolved accounting rubric. Midaz populates it alongside `routeCode`. These fields link each operation to its accounting classification. Downstream systems such as [Reporter](/en/reporter/what-is-reporter) can then produce accurate financial reports without extra lookups. ## Managing Operation and Accounting Routes *** To **configure your Operation Routes**, use the following endpoints: * [Create an Operation Route](/en/reference/midaz/create-an-operation-route) — Define a new accounting rule for your operations. * [List Operation Routes](/en/reference/midaz/list-operation-routes) — View all configured Operation Routes. * [Retrieve an Operation Route](/en/reference/midaz/retrieve-an-operation-route) — Get detailed information on a specific Operation Route. * [Update an Operation Route](/en/reference/midaz/update-an-operation-route) — Modify existing accounting rules. * [Delete an Operation Route](/en/reference/midaz/delete-an-operation-route) — Remove an outdated or unused Operation Route. To **configure your Accounting Routes** (the `transactionRoute` resource in the API), use the following endpoints: * [Create a Transaction Route](/en/reference/midaz/create-transaction-route) — Define new routing logic to connect transactions to accounting operations. * [List Transaction Routes](/en/reference/midaz/list-transaction-routes) — View all configured Transaction Routes. * [Retrieve a Transaction Route](/en/reference/midaz/retrieve-a-transaction-route) — Get details of a specific Transaction Route. * [Update a Transaction Route](/en/reference/midaz/update-a-transaction-route) — Modify existing routing criteria. * [Delete a Transaction Route](/en/reference/midaz/delete-a-transaction-route) — Remove routes that are no longer applicable. # Transactions Source: https://docs.lerian.studio/en/midaz/transactions Record financial events with Midaz double-entry Transactions — multiple Balances, debits, credits, and full traceability across Accounts. A **Transaction** in Midaz records a complete financial event. A transaction often uses multiple accounts and balances. Midaz runs on a double-entry accounting system that keeps every financial movement balanced. With the **multiple balances** feature, each operation specifies the account and the **balance key** to use. You can then debit or credit different logical balances of the same account (for example, `credit`, `operational`, or `collateral`). If you do not provide a `balanceKey`, the transaction uses the **default balance**. ## Double-entry accounting *** The double-entry system follows one principle. Every transaction has two entries: a debit and a credit. This structure records all financial activity and keeps your accounts balanced. Each transaction affects two accounts and keeps them in balance: * **Debits** show the value received or the resources consumed. * **Credits** show the value given or the resources provided. Midaz tracks and balances every debit and credit automatically. ### Example In this example, you transfer R\$1000 from one account to another. The transaction has two operations: * One operation to debit R\$1,000.00 from the source account. * One operation to credit R\$1,000.00 to the destination account. Midaz captures both entries automatically. You can view and analyze these movements through the API or Lerian Console. ## N:N Transactions (Many-to-Many) *** Traditional financial systems limit transactions to one-to-one or one-to-many relationships. Midaz supports N:N transactions. A single transaction can use multiple source and destination accounts. ### Examples * **Marketplace payout**: a single escrow account pays multiple sellers, and each seller pays a platform fee. * **Peer-to-peer with fees**: one transaction debits the payer and credits both the payee and a fee account. Midaz processes each case as a single atomic transaction. It debits and credits all parties together. ## Atomicity and integrity *** Transactions are atomic. **Either all operations succeed, or none do.** Partial financial events do not occur. If any part of a transaction fails validation — for example, one account has insufficient funds — Midaz does not apply the transaction. The ledger stays consistent. ## Transaction source *** A transaction in Midaz can start from a single source or from multiple sources. The sum of the values in `source` must equal the value after `send`. It must also equal the sum of the values in `distribute`. ### Single source In a single-source transaction, Midaz takes the amount from one source account. You can also name a specific balance. #### Example In this example (*Figure 1*): * Midaz takes BRL 30.00 from `@account1` (balance `credit`). * It sends 100% to `@destinationAccount1` (balance `operational`) Single-source transaction moving BRL 30.00 from one source account to a single destination account **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 **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 **Code examples** ```json JSON Example theme={null} { "description":"single destination transaction", "send":{ "asset":"BRL", "value":"30.00", "source":{ "from":[ { "accountAlias":"@external/BRL", "amount":{ "asset":"BRL", "value":"30.00" } } ] }, "distribute":{ "to":[ { "accountAlias":"@destinationAccount1", "share":{ "percentage":100 } } ] } } } ``` ### Multi-destination In a multi-destination transaction, Midaz divides the amount among multiple destination accounts. You can distribute values by shares, fixed amounts, or the remaining balance. #### Example In this example (*Figure 4*): * Midaz takes BRL 100 from the source account (`@account1`). * 38% of the amount goes to account 2 (`@account2`). * 50% goes to account 3 (`@account3`). * A fixed BRL 2.00 goes to account 4 (`@account4`). * The remaining amount goes to account 5 (`@account5`). Multi-destination transaction splitting BRL 100.00 from one source account across five destination accounts by percentage and fixed amounts **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 **Code examples** ```json JSON Example theme={null} { "description":"multi-source and multi-destination transaction", "send":{ "asset":"BRL", "value":"4000.00", "source":{ "from":[ { "accountAlias":"@account1", "share":{ "percentage":25 } }, { "accountAlias":"@account2", "share":{ "percentage":25 } }, { "accountAlias":"@account3", "share":{ "percentage":40 } }, { "accountAlias":"@account4", "share":{ "percentage":10 } } ] }, "distribute":{ "to":[ { "accountAlias":"@donation1", "share":{ "percentage":25 } }, { "accountAlias":"@donation2", "share":{ "percentage":25 } }, { "accountAlias":"@donation3", "share":{ "percentage":25 } }, { "accountAlias":"@donation4", "share":{ "percentage":25 } } ] } } } ``` ## Transaction statuses *** Every transaction in Midaz has a status. The status reflects its current stage in the lifecycle. You need these statuses to design transaction flows, configure event consumers, and read ledger data. | Status | What it means | Affects balances? | How it's created | | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------- | | `CREATED` | A reversal transaction was initiated and is being processed. This is a transient status that automatically progresses to `APPROVED` once the reversal completes. | Yes | [Revert a Transaction](/en/reference/midaz/revert-a-transaction) | | `APPROVED` | The transaction completed successfully. Funds have been moved between accounts. | Yes | Direct transaction (no `pending` flag), commit of a `PENDING` transaction, or automatic progression from `CREATED` | | `PENDING` | A two-phase transaction is awaiting confirmation. Funds are reserved in `on_hold` but not yet transferred. | Yes (reservation) | [Create a Transaction](/en/reference/midaz/create-a-transaction-using-json) with `"pending": true` | | `CANCELED` | A two-phase transaction was canceled. Reserved funds are released back to `available`. | Yes (release) | [Cancel a Pending Transaction](/en/reference/midaz/cancel-a-pending-transaction) | | `NOTED` | An annotation transaction recorded in the ledger without affecting balances. Operations preserve the double-entry structure but all balance fields are zeroed. | No | [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) | Use the `NOTED` status to import legacy transactions, record audit trails, and log compliance events. It fits any case where the transaction must exist in the ledger but the balances settled elsewhere already. ### Status transitions Transactions follow predictable paths through these statuses: * **Standard flow:** → `APPROVED` (single step) * **Two-phase flow:** → `PENDING` → `APPROVED` (commit) or `CANCELED` (cancel) * **Reversal flow:** → `CREATED` → `APPROVED` (automatic) * **Annotation flow:** → `NOTED` (terminal, no transitions) Once a transaction reaches `NOTED` or `CANCELED`, it cannot transition further. Both are terminal statuses. ## Transaction flow *** When a transaction starts, Midaz validates: * The accounts involved. * The specified balances (`balanceKey`, or `default` if not given). * Permissions (`allowSending`, `allowReceiving`). * Sufficient available funds in the selected balance. If validation passes and the transaction is **not** pending (Two-Phase Transaction flow), Midaz transfers the amount **immediately**. It moves the amount from the source account to the destination account, from the available balance. This process is synchronous. On success, the transaction status becomes `APPROVED`. Initiate this type of transaction **only** if you intend to commit it to the ledger immediately. For transactions that need validation or approval first, use the `pending` flag to create a **Two-Phase Transaction**. ## Two-Phase Transaction *** In this flow, Midaz creates the transaction with status `PENDING`. Midaz does not move funds right away. Instead, it reserves the amount in the correct balance (`balanceKey`, or `default` if you do not provide one). * Midaz moves the reserved funds from `available` to `on_hold`. * Midaz records **one** operation, typed `ON_HOLD`, on the source balance. The destination balance is untouched: no debit or credit is posted yet. * You must explicitly `commit` to execute the transfer, or `cancel` to release the funds. The Two-Phase Transaction feature supports [Flowker](/en/flowker/what-is-flowker). You reserve funds at the start of a workflow and run validations later. Midaz guarantees execution if the workflow approves the transaction. In *Figure 6*, you can see an example of a two-phase transaction with anti-fraud. Two-phase transaction in an anti-fraud workflow, reserving funds first and committing or cancelling them after validation ### Two-Phase Transaction flow #### 1. Create a Two-Phase Transaction * Use the [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) endpoint with `"pending": true`. Midaz validates accounts, the specified balances (`balanceKey`), permissions (`allowSending`, `allowReceiving`), and available funds. If valid: * Midaz reserves funds in the correct balance. * Midaz sets the transaction status to `PENDING`. * Midaz stores the metadata and posts the source `ON_HOLD` operation; no debit or credit reaches the destination yet. #### 2. Commit or cancel the pending transaction * **Commit**: finalizes the transaction. Funds move from `on_hold` to the destination balance, and Midaz adds the `DEBIT` and `CREDIT` operations — so a committed two-phase transaction carries three operations in total (`ON_HOLD`, `DEBIT`, `CREDIT`). * Use the [Commit a Pending Transaction](/en/reference/midaz/commit-a-pending-transaction) endpoint. * Status: `APPROVED`. * **Cancel**: releases the reserved funds back to `available` in the same balance. * Use the [Cancel a Pending Transaction](/en/reference/midaz/cancel-a-pending-transaction) endpoint. * Status: `CANCELED`. ## Past Transactions *** Midaz also supports past transactions. Institutions can import legacy financial events and keep historical accuracy. * Use the optional `transactionDate` field to set the original date of the transaction. * Transactions with financial impact recalculate the historical balance state as if Midaz processed them on that date. * Transactions created through the [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) endpoint validate structure but do **not** affect balances. They suit audits, compliance, and imports where balances must stay unchanged. ### Example ```json theme={null} { "description": "past transaction example", "transactionDate": "2025-01-01T13:38:31.064Z", // optional "send": { "asset": "BRL", "value": "1000", "source": { "from": [ { "accountAlias": "@external/BRL", "amount": { "asset": "BRL", "value": "1000" } } ] }, "distribute": { "to": [ { "accountAlias": "@account1_BRL", "amount": { "asset": "BRL", "value": "1000" } } ] } } } ``` Submit all past transactions before you start live operations. Midaz then recalculates balances consistently across the ledger. ## Transactions with no financial impact *** Midaz can create transactions that it records in the ledger but that do not affect account balances. These transactions keep structural integrity and leave balances unchanged. This feature is useful when you need to: * Import **legacy transactions** but keep balances unchanged. * Record **audit or compliance events**. * Add **business operations** that the ledger must track but that do not move funds. ### How does it work? When you create a transaction without financial impact: * Midaz stores the `balance` and `balanceAfter` fields as 0 to preserve double-entry validation. * Each operation has a `balanceAffected` (boolean) field: * true → the operation affects the account balance. * false → Midaz records the operation in the ledger but does not change balances. Even when Midaz updates no balances, it enforces double-entry rules. This keeps consistency across all transactions in the ledger. #### Example ```json theme={null} { "description": "annotation example", "transactionDate": "2025-01-01T13:38:31.064Z", "send": { "asset": "BRL", "value": "1000", "source": { "from": [ { "accountAlias": "@external/BRL", "amount": { "asset": "BRL", "value": "1000" }, "balanceAffected": false } ] }, "distribute": { "to": [ { "accountAlias": "@account1_BRL", "amount": { "asset": "BRL", "value": "1000" }, "balanceAffected": false } ] } } } ``` #### Related endpoint * [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) — Record a transaction without financial impact in the ledger. ## Real-time event publishing *** Midaz supports real-time event publishing through RabbitMQ. You can track the status of your transactions as they happen. After you enable it, every transaction generates an event: `APPROVED`, `PENDING`, `CANCELED`, `CREATED`, or `NOTED`. External systems subscribe to these events through topic-based routing. For more about how to publish and consume transaction events, see the [Event publisher](/en/midaz/event-publisher) page. ## Inflows, outflows, and external accounts *** Midaz uses a double-entry ledger. All value that enters or leaves the system must pass through one special account: the **External Account**. Midaz represents this account as `@external/{{assetCode}}`. It acts as the bridge between Midaz and the external financial world (banks, PSPs, payment rails, and so on). ### Why does this matter? When you first initialize the ledger, all accounts — including `@external` — start with a zero balance. To reflect real-world balances, such as institutional funds held outside Midaz, you must **initiate a transaction that injects funds into Midaz accounts and debits the external account**. This is the only way to bring funds into Midaz. ### Inflows – Adding value into the Ledger To credit an internal account from outside the ledger: * **Source**: `@external/{{assetCode}}` (e.g., `@external/BRL`). * **Destination**: One or more internal accounts (e.g., `@organization.main`). **Example: First deposit into the Ledger** Your institution holds R\$10,000 in a real-world bank and wants to bring it into Midaz. You create a transaction: | Source | Destination | Amount | | :------------ | :---------- | :--------- | | @external/BRL | @accountA | BRL 10,000 | This debits the external account and credits your internal account. The external account now shows a negative balance. This is expected: it represents the total amount your organization brought into the ledger. ### Outflows – Moving value out of the Ledger To move value from the ledger to an external destination: * **Source**: One or more Midaz accounts. * **Destination**: `@external/{{assetCode}}`. **Example: A Pix transfer from Ledger to an external bank** | Source | Destination | Amount | | :-------- | :------------ | :-------- | | @accountA | @external/BRL | BRL 1,000 | This debits `@accountA` and credits the external account. Your system then transfers the funds to the recipient through SPI or another integration. ### Behavior and balance rules * `@external/{{assetCode}}` can have a **zero or negative** balance, but never **positive**. * Its balance is always the **inverse** of the combined balance of all Midaz accounts that hold that asset. * Every inflow increases internal liquidity and reduces the external account balance (i.e., simulates a deposit). * Every outflow does the reverse. All value that moves between the outside world and the Midaz ledger must go through the external account. Nothing enters or leaves the system without a formal transaction. This gives you full traceability, balance integrity, and compliance with double-entry principles. ## Setting a custom transaction date *** The `transactionDate` field lets you set a custom date for a transaction, independent of when you submit it to the API. * **Optional.** If you omit it, Midaz uses the current timestamp. * **Accepted formats:** * ISO 8601 with timezone: `2026-01-15T10:30:00Z` * ISO 8601 without timezone: `2026-01-15T10:30:00` * Date only: `2026-01-15` * **Constraint:** you cannot use a future date. A future date returns error `0121`. * **Constraint:** you cannot use it on `PENDING` transactions. A `transactionDate` with `"pending": true` returns error `0122`. ### Use cases * Record transactions that occurred in the past (for example, same-day corrections) * Import historical financial data into a new ledger * Reconcile with external systems that use a different posting date ## Transaction Routes *** The **Transaction Routes** API enables structured, validated transaction processing in Midaz. The Lerian Console and product documentation call this concept **Accounting Routes**. The API resource and endpoints keep the `transactionRoute` / Transaction Routes name. Both refer to the same transaction-level route. The Transactions API runs financial events: debits and credits between accounts. Transaction Routes define templates for **how** to structure and validate these events. This keeps them consistent and correct. Think of it as the validation layer. It makes business transactions follow **predefined patterns** and keep a **proper financial structure**. For example, a **fee**, a **deposit**, or a **payout** may need different account types, validation rules, and structures. You do not handle validation separately for each transaction. Instead, you configure predefined rules. These rules tell Midaz: "*When the user submits this type of transaction, validate it against these account requirements and structure patterns.*" Each Transaction Route combines multiple **Operation Routes**. An Operation Route defines one component of a transaction. It sets the account requirements, the direction (source or destination), and the validation rules for each "leg" of the financial event. Do not use the `route` field in `FromTo` inputs — use `routeId` instead. `routeId` accepts a UUID that references an Operation Route created through the Operation Routes API. Midaz keeps the `route` field for backward compatibility, but it will remove the field in a future version. ### Why does it matter? With **Transaction Routes**, you: * Keep a consistent transaction structure across your application. * Make your ledger more maintainable, predictable, and reliable. * Validate financial events against predefined patterns. * Configure transaction templates without code changes. * Maintain data integrity through structured validation. ## Initiating a transaction *** When you create transactions through the API, always implement **idempotency** to prevent duplicate processing. Midaz provides built-in idempotency support through the `X-Idempotency` header. Validate the `X-Idempotency-Replayed` response header to tell new transactions from cached replays. See [Retries and idempotency](/en/reference/retries-idempotency) for details. Use the JSON transaction API to initiate a transaction. ### JSON request shape in v2 Each transaction side uses one representation: `from` or `sources`, and independently `to` or `destinations`. Do not send both representations for the same side, and do not send an explicit `null` for an unused scalar field. Each `sources` or `destinations` item requires `account` and exactly one value expression: `amount` or `share`. The `remaining` expression is not accepted in v2. Each array accepts at most 500 items. `share.percentage` is 1–100; `share.percentageOfPercentage` is 0–100, where `0` means no narrowing. V2 create requests have a 1 MiB body limit. ### Using the JSON endpoint JSON endpoints provide a flexible, developer-friendly standard for data interchange. They give you precise control over request structures for custom workflows and specific use cases. They work with many programming languages, which makes integration and debugging easier. * To create a transaction with JSON, use the [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) endpoint. If you need to reserve funds **before** you complete the transfer, set the `pending` field to `true` (Two-Phase Transaction flow). ## Reverting a transaction *** Midaz supports transaction reversal. You can undo an approved transaction. Midaz creates a mirror transaction that inverts the original debits and credits. This mechanism keeps full audit trails and cancels the financial impact on account balances. Reversal creates a **new transaction** that compensates for the original. The original transaction remains in the ledger history for complete traceability. Revert sends no idempotency key of its own, so Midaz derives one. Read the `X-Idempotency-Replayed` response header: `true` means you received a cached reverse rather than a freshly created one. Treat a replay as a signal to verify the origin's state before retrying. ### How does it work? When you revert a transaction, Midaz automatically: 1. **Inverts operations**: * CREDIT operations become source operations (`from`). * DEBIT operations become destination operations (`to`). 2. **Creates a new transaction** with: * Same amount and asset code. * Same description and metadata. * Inverted operations (recipients become senders, senders become recipients). * Initial status: `CREATED` (not `PENDING`) → then progresses to `APPROVED`. * `parentTransactionID` that references the original transaction. 3. **Processes the reversal** through the standard transaction flow: validation, balance updates, and history recording. ### Example Consider this scenario: **Original transaction**: * Account A (debit -100) → Account B (credit +100) **Reversal transaction created**: * Account B (debit -100) → Account A (credit +100) **Result**: * Account A returns to its previous balance (receives back the -100). * Account B returns to its previous balance (loses the +100). * Both transactions remain in the ledger history for audit purposes. * The reversal transaction includes a `parentTransactionID` that points to the original. ### Reversal restrictions Midaz enforces strict rules to keep ledger integrity. A reversal fails in these cases: #### 1. Transaction already has a reversal * Midaz allows only one reversal per transaction. * This prevents multiple reversals of the same transaction. #### 2. Transaction is already a reversal * You cannot revert a transaction that is itself a reversal. * This prevents "reversals of reversals." #### 3. Transaction status is not APPROVED * You can revert only approved transactions. * You cannot revert a transaction with status `PENDING`, `CREATED`, or `CANCELED`. #### 4. Transaction cannot be reverted * This happens when the transaction has no valid operations to invert. * For example, a transaction without standard CREDIT or DEBIT operations. #### 5. An operation route on the transaction is not bidirectional * Every operation that carries a `routeId` must reference an Operation Route whose `operationType` is `bidirectional`. * A `source` or `destination` route cannot be reverted: Midaz returns error `0150` (Route Not Bidirectional). * Plan for this when you design routes — see [Accounting Routes](/en/midaz/transaction-routing-entities). Midaz reverses the CREDIT and DEBIT operations. It does not reverse ON\_HOLD or RELEASE operations. ### Use cases Transaction reversal helps in several operational scenarios: #### 1. Incorrect payment reversal A customer paid BRL 500 to the wrong supplier. * Revert the transaction. * Funds return to the customer's account. * The customer can start a new payment to the correct supplier. #### 2. Purchase cancellation A store processed a BRL 1,000 sale, but the customer cancels the purchase. * Revert the sale transaction. * Funds return to the customer's account. #### 3. Operational error correction An operator created a transaction with the wrong amount. * Revert the incorrect transaction. * Create a new transaction with the correct amount. #### 4. Product return A customer purchased and paid BRL 200, but returned the product. * Revert the payment transaction. * The customer receives a refund. #### 5. Integration failure compensation A transaction is approved but fails in an external system. * Revert to undo the accounting operation. * Balances return to their previous state. ## Blocking and unblocking funds *** Some scenarios require you to flag funds as **blocked** — a compliance hold, a court order, a fraud investigation — and later **release** them. Midaz supports this with two dedicated endpoints. These endpoints create transactions whose operations are typed `BLOCK` and `UNBLOCK`. These transactions accept the **same body** as the [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) endpoint, with two key differences: * **Always posted immediately.** Midaz ignores the `pending` field in the request body and overrides it to `false`. Block and unblock transactions are never two-phase. They go straight to `APPROVED`. * **Operations are typed `BLOCK` or `UNBLOCK`.** This classification distinguishes them in the ledger and in operation queries. You can audit blocked-fund movements without a look at the metadata. Midaz is **agnostic about the business reason** to block or unblock funds. Record the reason in the `metadata` field. A Block transaction records a **ledger movement** with `BLOCK`-typed operations. This differs from the balance-level controls in [Balances](/en/midaz/balances): permission flags (`allowSending` / `allowReceiving`) and collateral balances. Those controls restrict movement but record no transaction. Use a collateral balance for a standing operational restriction. Use a Block transaction when you need an auditable ledger entry. * Use the [Create a Block Transaction](/en/reference/midaz/create-a-block-transaction) endpoint to block funds. * Use the [Create an Unblock Transaction](/en/reference/midaz/create-an-unblock-transaction) endpoint to release previously blocked funds. ## Managing transactions *** You can manage your Transactions through the API or Lerian Console. ### Via API * [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) — Submit a transaction directly using a JSON payload. * [Commit a pending transaction](/en/reference/midaz/commit-a-pending-transaction) — Finalize a reserved transaction. * [Cancel a pending transaction](/en/reference/midaz/cancel-a-pending-transaction) — Release reserved funds without executing. * [Revert a Transaction](/en/reference/midaz/revert-a-transaction) — Create a reversal transaction to undo an approved transaction. * [Create an Inflow Transaction](/en/reference/midaz/create-an-inflow-transaction) — Register incoming funds from external sources into the Ledger. * [Create an Outflow Transaction](/en/reference/midaz/create-an-outflow-transaction) — Move funds from internal accounts to the external world. * [Create a Block Transaction](/en/reference/midaz/create-a-block-transaction) — Flag funds as blocked with `BLOCK`-typed operations. * [Create an Unblock Transaction](/en/reference/midaz/create-an-unblock-transaction) — Release previously blocked funds with `UNBLOCK`-typed operations. * [List Transactions](/en/reference/midaz/list-transactions) — View all Transactions in your workspace. * [Retrieve a Transaction](/en/reference/midaz/retrieve-a-transaction) — Get details of a specific Transaction. * [Update a Transaction](/en/reference/midaz/update-a-transaction) — Edit the metadata of an existing Transaction. * [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) — Record a transaction without financial impact in the ledger. ### Via Lerian Console You can do all Transaction management actions — view, create, and cancel — through Lerian Console. Learn more in the [Managing Transactions](/en/midaz/console/managing-transactions) guide. # Transactions Source: https://docs.lerian.studio/en/midaz/transactions-overview Overview of Transactions in Midaz — balances, overdraft, events, async processing, bulk recording, and operations that keep financial value moving safely. Transactions are where financial value moves. This section covers balances, transaction processing, events, and operations. ## Content *** Here, you'll find: * [**Balances**](/en/midaz/balances): The value an account holds. Balances reflect every operation over time and support multiple balance keys to segment funds within one account. * [**Balance Overdraft**](/en/midaz/balance-overdraft): Controlled negative balances. When a balance goes negative, Midaz caps its available amount at zero and records the deficit as a debit on a companion overdraft balance. * [**Transactions**](/en/midaz/transactions): Complete financial events built on Midaz's double-entry accounting. Every transaction pairs debits and credits, and they must balance before Midaz commits it. * [**Transaction amount**](/en/midaz/amount): How Midaz stores financial values as precise decimals. This keeps values accurate across all asset types, from fiat currency to high-precision crypto tokens. * [**Event publisher**](/en/midaz/event-publisher): Real-time event publishing for transaction updates. When you enable it, every transaction emits an event to a dedicated RabbitMQ exchange. External applications consume it without a tight dependency. * [**Receiving events from Midaz**](/en/midaz/receiving-midaz-events): Consume ledger events in two ways. Bind directly to Midaz's RabbitMQ exchanges, or subscribe through the Streaming Hub. * [**Async transaction processing**](/en/midaz/async-transaction-processing): How Midaz decouples transaction writes with RabbitMQ for higher throughput and lower latency. It covers sync and async modes, built-in resilience, and balance synchronization. * [**Bulk Recorder**](/en/midaz/bulk-recorder): Batch transaction processing that groups multiple writes into one bulk operation. It reduces database round trips and increases throughput for high-volume workloads. * [**Operations**](/en/midaz/operations): The smallest unit of financial activity — a single ledger entry, either a debit or a credit on a specific account. Operations form the building blocks of every transaction. # Midaz use cases Source: https://docs.lerian.studio/en/midaz/use-cases Explore Midaz use cases across banking, multi-account customers, account closure, global portfolios, academic records, and supply chain traceability scenarios. See how organizations across industries use Midaz to solve operational problems, from banking and payments to supply chain traceability and academic records. Each use case shows what changes with Midaz. It maps the platform's entities to the domain and gives step-by-step transaction examples. ### Content * [**Banking**](/en/midaz/midaz-in-banking): Understand how Midaz supports banking operations. * [**Midaz for multi-account customers**](/en/midaz/midaz-for-multi-account-customers): Model multiple accounts under a single customer, with segregated balances, rules, and statements. * [**Closing a customer account**](/en/midaz/closing-a-customer-account): How to close a customer account in line with BACEN account-closure rules. * [**Global portfolio**](/en/midaz/midaz-in-a-global-portfolio): Configure a global account structure similar to those at major banks. * [**Academic records**](/en/midaz/midaz-for-academic-record-management): Manage academic records for an educational institution. * [**Supply chain**](/en/midaz/midaz-for-supply-chain-management): Manage the supply chain for a manufacturing company. See [Plugin use cases](/en/rails/pix/pix-use-cases) for scenarios that combine Midaz with Pix or the Fees Engine. # What is Midaz? Source: https://docs.lerian.studio/en/midaz/what-is-midaz Learn what Midaz is: Lerian's source-available core ledger for double-entry accounting, multi-currency operations, and audit-ready compliance reporting. **Midaz** is Lerian's source-available core ledger for financial infrastructure. It covers account management, transaction processing, multi-currency operations, and compliance reporting. The source code is public on [GitHub](https://github.com/LerianStudio/midaz). You configure your financial structure once — organizations, ledgers, accounts, and transaction rules. Midaz handles the rest: double-entry accounting, balance tracking, audit trails, and event-driven integrations. ## Why use Midaz *** A legacy core means rigid schemas, vendor lock-in, and slow release cycles. Every new product or regulation requires months of integration work. Midaz solves this: * **Own your ledger**: Source-available means full transparency, customization, and no vendor lock-in * **Move fast**: Modular architecture lets you add products and integrations without re-architecting * **Stay compliant**: Immutable audit trails, double-entry enforcement, and regulatory-ready reporting out of the box * **Scale confidently**: Handle thousands to millions of transactions per day with horizontal scaling ## What Midaz does *** * **Central ledger**: Real-time account and transaction management with double-entry guarantees * **Multi-currency support**: Manage traditional currencies, digital assets, and loyalty points in the same ledger * **Accounting Routes**: Define reusable transaction templates that enforce business rules automatically * **Automated triggers**: Respond to transactions in real time — trigger notifications, compliance workflows, or analytics feeds * **Plugin ecosystem**: Extend with Pix, TED, and more — or build your own ### Built for every financial player * **Fintechs**: Power digital wallets, credit platforms, and embedded finance products * **Banks**: Modernize legacy systems incrementally without full migration risk * **Enterprises**: Streamline payments, collections, and reconciliation across business units ## Common use cases *** Launch checking accounts, savings products, and instant transfers. Structure your accounts with portfolios per customer, enforce business rules through transaction routes, and generate compliance reports automatically. Manage multi-currency accounts for international customers. Each currency gets its own asset accounts while portfolios tie everything together for a unified customer view. Offer Pix connectivity with automated fee management. Combine Midaz's ledger and Fees Engine with the Pix plugin to process instant payments, calculate fees, and maintain settlement accounts — all in real time. Handle complex multi-party settlements where a single payment splits into seller payouts, platform fees, and tax withholdings — all processed atomically as one transaction. Use Midaz beyond finance: track academic credits, supply chain materials, loyalty points, or carbon credits. Any domain that needs immutable, auditable tracking of units of value. ## How Midaz works *** Create your Organization, Ledgers, Assets, and Account Types to reflect your business model. Set up customer Portfolios and Accounts. Group them into Segments for differentiated rules and pricing. Configure Accounting Routes and Operation Routes to enforce how money moves — including validations, fees, and double-entry compliance. (Accounting Routes are exposed as `transactionRoute` in the API.) Process transactions through the API. Midaz handles balance updates, operation creation, and audit logging atomically. Enable event publishing to trigger downstream systems — notifications, analytics, compliance workflows — the moment a transaction completes. Use Reporter to generate regulatory and business reports from your ledger data, or query balances and transactions directly via the API. ## Integrations with the Lerian ecosystem *** Midaz is a stand-alone product that can optionally integrate with other components of the Lerian ecosystem. | Product | Integration | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------- | | [Matcher](/en/matcher/what-is-matcher) | Optional: Reconcile ledger transactions against external data sources | | [Reporter](/en/reporter/what-is-reporter) | Optional: Generate regulatory and business reports from ledger data | | [Flowker](/en/flowker/what-is-flowker) | Optional: Orchestrate pre-ledger validation workflows (anti-fraud, approvals) | | [Tracer](/en/tracer/what-is-tracer) | Optional: Real-time transaction validation and spending controls | Midaz works independently or as part of the Lerian ecosystem. Use it alone or connect it to other Lerian products as your needs evolve. ## Next steps *** Understand Midaz's domains, entities, and how they relate to each other. Install and configure Midaz to start building your financial infrastructure. Explore detailed use cases for banking, global portfolios, Pix, and more. Browse endpoints, request/response schemas, and try the API directly. # Open by design Source: https://docs.lerian.studio/en/open-by-design Why Lerian builds open-by-design financial infrastructure around Midaz, our source-available core — transparency, independence, extensibility, and trust. We believe financial infrastructure should be inspectable, extensible, and owned by those who operate it. Open by design is not a positioning strategy for us. It is an architectural principle. Midaz, our core ledger, is source-available under Elastic License 2.0 because financial systems require transparency, control, and long-term sustainability. Fetcher, our data ingestion service, is source-available too. The rest of the Lerian portfolio — Reporter, Matcher, Flowker, Tracer, Lender, Streaming Hub, and our plugins — is available to licensed customers, with repositories maintained internally, while preserving the same principles of transparency, auditability, and customer ownership of data and operations. ## Why Source-available at the core? *** A source-available core creates structural advantages — not just community engagement. By keeping Midaz source-available, we enable: * **Transparency**: Every line of the core ledger is visible and auditable. No hidden logic. No opaque behavior. * **Independence**: Institutions control their stack and retain access to the source code of the ledger that holds their money. * **Extensibility**: Teams can adapt the ledger to their regulatory, operational, and product requirements. * **Trust by design**: Visibility reduces systemic risk. You understand how your core infrastructure behaves — because you can inspect it. ## What this means for your business *** For decision makers evaluating Lerian, an open-by-design platform translates into concrete business benefits: * **Source-code access**: Midaz is source-available under Elastic License 2.0, so your team retains access to the ledger source code. * **Third-party auditability**: Your security team, auditors, or regulators can inspect the Midaz code directly. * **Extensibility**: Your engineers can inspect and extend Midaz to meet operational requirements. ## Join the community *** Building an open core only works when it is collaborative. Whether you are a developer, architect, or financial operator, you can: * Contribute code * Propose architectural improvements * Report issues * Build extensions and integrations * Share implementation patterns We welcome practical contributions — not just discussions. [Join our Discord community →](https://discord.lerian.studio/) # Our values Source: https://docs.lerian.studio/en/our-values Learn Lerian's mission and the values that guide our work — strength, courage, integrity, audacity, and leadership in financial infrastructure. At Lerian, we combine technology and financial expertise to help institutions manage their operations through a modular platform. We help institutions build systems they can control as their needs evolve. ## Our Mission *** We help financial institutions adopt a modular platform that supports independent operations and evolution. We design adaptable financial software anchored by Midaz, our source-available core under Elastic License 2.0. Institutions can inspect and extend the ledger while assembling the products and platform services they need. ## Our Commitment *** Our commitment to scalable design ensures our solutions grow alongside the needs of our clients. We believe in building technology that adapts to your needs, not the other way around. ## Our Values *** At the core of our work are values that inspire confidence, trust, and innovation: ### Be Your Own Strength Pursue excellence, face challenges with resilience, and embrace growth. We believe that true strength comes from within—from the ability to adapt, learn, and overcome obstacles. ### Be Your Own Courage Navigate uncertainty boldly and embrace risk with confidence. In a rapidly changing financial landscape, courage means taking calculated risks and pushing boundaries. ### Be Your Own Integrity Uphold transparency, ethics, and trust in every decision. Integrity is not negotiable—it's the foundation of everything we build and every relationship we nurture. ### Be Your Own Audacity Innovate fearlessly, push boundaries, and take initiative. Audacity drives us to challenge conventions and create solutions that others might consider impossible. ### Be Your Own Leader Forge your own path with vision, self-development, and confidence. Leadership is about taking ownership, inspiring others, and creating lasting impact. # Products, plugins & plans Source: https://docs.lerian.studio/en/products-and-plugins How Lerian's offering is structured — products, platform services, Brazil Rails, plugins, and their licensing. Lerian is a complete, modular core banking platform. Its products, shared platform services, Brazil Rails, and plugins let you control what you adopt and when. Plugins extend a host product for specific markets or regulations — you adopt them as your needs evolve. **Products** are standalone solutions built to deliver end-to-end capabilities without relying on another platform. Each product offers a complete feature set and can be deployed and evolved independently. [Midaz](https://github.com/LerianStudio/midaz) and [Fetcher](https://github.com/LerianStudio/fetcher) are source-available under Elastic License 2.0 (ELv2); other Lerian products are closed source and available to licensed customers. **Plugins** are optional modules that extend the functionality of a specific product. They cannot run independently — they are built to work inside a product's architecture, leveraging its data models and infrastructure. | | Products | Plugins | | ---------------- | --------------------------------------------------------------------------------- | ---------------------------- | | **Deployment** | Standalone | Requires a host product | | **Licensing** | Midaz & Fetcher: source-available under ELv2 · Others: closed source and licensed | Premium (Enterprise) | | **Scope** | Full platform capabilities | Specialized business logic | | **Independence** | Runs on its own | Depends on a product context | ## Plans and what's included *** There are two ways to adopt Lerian: * **Midaz and Fetcher are source-available under ELv2.** You can inspect, self-host, and build on the core ledger — including its embedded CRM — and the data extraction service under the license terms. * **The Enterprise plan** bundles everything you need to operate at scale: consulting for Midaz, the Midaz plugins (such as Pix and TED), and the **Lerian Console** — the web interface for managing it all. The Console isn't sold on its own; it comes with Enterprise. Lerian's other products — **Reporter**, **Matcher**, **Tracer**, **Flowker**, **Lender**, and **Streaming Hub** — are standalone and licensed separately. Reporter, Matcher, Tracer, and Flowker are built to work with a ledger (Lerian's Midaz or another), not to depend on it, while Lender runs its credit journeys on the Midaz ledger; once licensed, you can also operate them from the Console as part of an Enterprise setup. Streaming Hub sits at the platform's delivery edge, fanning out platform events to your external systems through signed webhooks, queues, or a pull API. Pricing depends on your setup and scale. [Talk to our team](https://lerian.studio/contact) to find the right plan for your use case. ## Our products *** Lerian products are modular, composable platforms designed to cover core financial operations. They work together but can also be adopted individually. Each product is built for production use. [Explore our products →](/en/about-products) Shared **[Platform services](/en/platform/platform-introduction)** provide access management, lifecycle management, observability, and deployment support across the products you adopt. **[Brazil Rails](/en/rails/what-are-brazil-rails)** connect the platform to Brazil's payment and regulatory rails. ## Our plugins *** Plugins add business-specific capabilities to a product without modifying its core. They follow a microservices architecture, with their own versioning, deployment, and monitoring. Today, all available plugins are part of the **Midaz ecosystem**, offering specialized capabilities for payment orchestration — Pix, TED, and the Payments module for boletos and bill payment. [Explore our plugins →](/en/about-plugins) ## Brazil rails *** Plugins are one of two ways the platform reaches Brazil's payment rails. Interface plugins integrate a third-party connectivity provider (JD, BTG), while **Lerian native messaging** is Lerian-owned software that connects your institution straight to BACEN and the market infrastructures under its supervision — no intermediary. Both models settle through Midaz. [Explore Brazil Rails →](/en/rails/what-are-brazil-rails) # Before you begin Source: https://docs.lerian.studio/en/reference/getting-started Review the sandbox, Try It, and licensing pages before integrating with Lerian APIs so you know how to authenticate, test, and access each product. This section covers the tools and policies you should know before integrating with Lerian's APIs. Use the sandbox to test your requests safely, and review the licensing pages to understand usage rights and third-party attributions. ## Available pages ***
Test your integrations in a secure, isolated environment with simulated data. Interact with our APIs directly from the documentation. Understand how Lerian's software license works, including usage rights and conditions. Review the third-party licenses and credits used by Lerian products.
# Lerian's License Source: https://docs.lerian.studio/en/reference/lerians-license Lerian licenses authorize access to paid products and plugins with organization-scoped and global license types, plus activation and validation rules. A **license** is a key that authorizes access to Lerian features or services under specific contractual conditions, typically aligned with billing cycles (e.g., monthly, annual). This mechanism is similar to how software products like Microsoft Office or GitHub Enterprise manage access through activation keys. In Lerian's ecosystem, licenses play a critical role in: * Ensuring customers are in good standing before accessing paid features, such as **products**, **plugins or enterprise features**. * Managing which features are available per client. * Enabling Lerian to scale support and sales operations securely and efficiently. In short, only clients with valid licenses, as defined in their contracts, can access specific software components. A valid license must be associated with the organization making the request. Otherwise, access will be denied. ## License types *** In Lerian’s ecosystem, licenses can have two different scopes, depending on the plugin: ### Organization-specific licenses (non-global) Applies to a **single organization**. Example: **Fees Engine**, **Reporter**. You can use the license with any number of ledgers inside that organization. ### Global licenses Valid for **multiple organizations**. Example: **Access Manager.** ## License usage workflow *** The steps below apply to both license types: * **For organization-specific plugins**: provide the `organization_id` that will use the license. * **For global plugins**: inform Lerian that you need a global license; no `organization_id` is required. Lerian’s team follows the internal process to create and activate the license using the provided details. Lerian sends the **License Key** (`LICENSE_KEY`) to the client. Once you receive your license key, you must configure your `.env` file so the system knows **which license to validate** and **which organizations it applies to**. Always configure the correct environment variables before using a plugin. Incorrect or missing values will result in license validation errors. **Organization-specific licenses (non-global)** These licenses apply to one organization only. ```bash Bash theme={null} # LICENSE LICENSE_KEY= ORGANIZATION_IDS=, ``` * **LICENSE\_KEY**: The key provided by Lerian. * **ORGANIZATION\_IDS**: The ID of the organization linked to the license. **Global licenses** These licenses have a global scope and must **always** use the value `global` in the `ORGANIZATION_IDS` variable. ```bash Bash theme={null} # LICENSE LICENSE_KEY= ORGANIZATION_IDS=global ``` * **LICENSE\_KEY**: The key provided by Lerian. * **ORGANIZATION\_IDS**: Must be set to `global` for global licenses. ## License-related errors *** When a license is invalid or expired, API responses or plugin behaviors may return one of the specific error messages: | Code | Title | Message | | :------- | :------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | LCS-0001 | Internal server error | An internal server error occurred. Please try again later or contact support if the problem persists. | | LCS-0002 | No organization IDs configured | The application is not configured with any organization IDs. | | LCS-0003 | No valid licenses found for any organization | No valid licenses were found for any of the configured organizations. Please check your license keys and ensure at least one organization has a valid license. | | LCS-0010 | Organization ID header is missing | The X-Organization-ID header is missing. Please ensure the header is included in the request. | | LCS-0011 | Organization ID header is unknown | The organization ID '12345' was not found in the list of configured organizations. Please verify you are using a valid organization ID. | | LCS-0012 | Failed to validate organization license | License validation failed for organization ID 12345. This could be due to network issues, invalid license keys, or server-side problems. Please verify your license key is correct and that you have network connectivity to the license server. | | LCS-0013 | Organization license is invalid | The license for organization ID '12345' is not valid and has no grace period active. Please renew your license or contact support for assistance. | | LCS-1001 | Validation error | Could not validate license key. Please try again later or contact your system administrator. | | LCS-1002 | License not found | There is no license for this license key. | | LCS-1003 | License expired | The license key has expired. | | LCS-1004 | License suspended | The license key has been suspended due to payment issues or administrative action. | | LCS-1005 | Fingerprint mismatch | The license key or organization ID are not valid for this license. | | LCS-1006 | Product mismatch | There is no license for this product. | For more information about your license or to request an upgrade, please contact the Lerian commercial team. # Licenses & credits Source: https://docs.lerian.studio/en/reference/licenses-and-credits Third-party open source components used by Lerian products and the licenses under which each dependency is distributed to customers. Lerian is committed to the responsible and transparent use of third-party open source software. When our products or plugins include external components that require attribution, we list them here and comply with all licensing terms. ## Garble (BSD-3-Clause License) *** Some Go-based plugins use [Garble](https://github.com/burrowers/garble) to obfuscate binaries during the build process. Garble is distributed under the [BSD-3-Clause License](https://github.com/burrowers/garble?tab=BSD-3-Clause-1-ov-file#readme). Lerian acknowledges and credits the original authors of the Garble project, in compliance with license terms. We periodically review the licenses of our dependencies to ensure full compliance. If you have any questions about our use of third-party software, please don't hesitate to contact us. # Using our Sandbox Source: https://docs.lerian.studio/en/reference/using-our-sandbox An isolated Lerian environment that mirrors production behavior and powers the Try It experience inside the documentation for safe hands-on API testing. The sandbox helps you explore Lerian’s APIs in a safe and predictable environment. It mirrors production behavior while keeping your real data untouched. Every request you send from our documentation is routed through this environment, giving you a complete view of how each endpoint behaves. The **sandbox fully powers the Try It experience**. When you test an endpoint, the response you see comes from this environment. If you haven't seen how the Try It feature works yet, check out the [Using the Try It feature](/en/reference/using-try-it) page. The sandbox is only available inside the documentation. You cannot access it directly through external tools. ## Base URLs *** These are the URLs the documentation uses behind the scenes when you test an endpoint: | Service | Sandbox Base URL | | -------------- | --------------------------------------------------------------------------------------------------- | | Midaz | `https://ledger.sandbox.lerian.net` | | Access Manager | `https://auth.sandbox.lerian.net` (Auth APIs) `https://identity.sandbox.lerian.net` (Identity APIs) | | CRM | `https://midaz-crm.sandbox.lerian.net` | | Fees Engine | `https://fees.sandbox.lerian.net` | | Reporter | `https://reporter.sandbox.lerian.net` | All examples and test boxes in the docs already use these URLs. ## What you can do *** The sandbox lets you: * Run live requests from the documentation * Inspect request and response structures * Understand authentication headers and data formats * Learn how each API behaves without setting up your own environment Click **Try It** on any endpoint to run a live request and check how the API responds. ## Limitations *** The sandbox is designed for exploration. Some restrictions are intentional: * It only works inside the documentation * Data resets frequently * Some endpoints use fixed or sample responses * Authentication rules may be simplified depending on the API For full integration tests, use your own environment or reach out to our team to request a dedicated development space. # Using the Try It feature Source: https://docs.lerian.studio/en/reference/using-try-it Send live requests to the Lerian sandbox directly from the documentation with real API responses in a controlled environment — no local setup required. The **Try It** feature lets you interact with our APIs directly from the documentation. It gives you an embedded playground where you can test real requests and responses in a safe and controlled environment. Every request sent through Try It runs against our sandbox. If you want to understand how the sandbox works under the hood, check the page [Using our Sandbox](/en/reference/using-our-sandbox). ## What you can do *** With **Try It**, you can: * Send live requests to our sandbox environment * Inspect full HTTP request and response details * Adjust headers, parameters, and body payloads * Test different authentication tokens or organization IDs * Explore error handling and validation scenarios with mock data ## Before you start *** You will need the required headers for each API. Examples include `Authorization`, `X-Lerian-Id`, or `X-Organization-Id`. Each endpoint specifies exactly which headers you must use. The sandbox resets automatically every day. Any entity you create, such as Organizations, Ledgers, or Portfolios, will be deleted within 24 hours. When testing dependent endpoints, create the required entities again before sending new requests. For example, you must create an Organization before creating a Ledger. Use the returned `organization_id` in the path. ## How to use it *** The **Try It** feature is available on every endpoint page in the **API Reference**. If the Try It button is missing, reload the page or check that you are browsing the API Reference section. Open any endpoint in the **API Reference**. Click the **Try It** button next to the endpoint path. Fill in the input fields such as query parameters, body content, or headers. Click **Send Request** to run the call. Review the response body, status code, and headers shown below the request. You can copy your request as `curl` or `fetch` using the code tabs. It is a simple way to move from testing in the docs to running calls in your local environment. # Our release cycle Source: https://docs.lerian.studio/en/updates-and-versioning/release-cycle Understand how Lerian plans, builds, tests, and releases — from continuous weekly delivery to production releases — so teams know what to expect. Lerian plans and reviews work on a **weekly rhythm**, delivering improvements continuously as release candidates. **Production releases** follow a separate, more deliberate cadence to keep environments stable. We're currently refining this cadence, so production timing may be adjusted as the process evolves. Throughout this work, we focus on **evolving all our products**, continuously improving existing capabilities and, when applicable, **developing new products**. Every change is carefully validated before reaching production. ## How it works *** * **Planning**: We align scope, priorities, and technical direction. This is where requirements are defined and the roadmap takes shape. * **Development**: The team builds and iterates on new features, enhancements, and platform improvements based on the planned scope. * **Testing and validation**: Changes go through thorough testing and validation to ensure reliability, performance, and consistency across the platform. * **Release**: Changes are consolidated into a production release, with documentation and release notes published alongside the rollout. # 2025 release schedule Source: https://docs.lerian.studio/en/updates-and-versioning/release-cycle-2025 Track Lerian's 2025 release cycles with the planned start and end dates for each stage so you can align your delivery and rollouts. In 2025, our release cycles were the following: ## First cycle *** | Stage | Start Date | End Date | | :------------ | :--------- | :------- | | **Weeks 1–6** | April 7 | May 16 | | **Weeks 7–8** | May 19 | May 30 | ## Second cycle *** | Stage | Start Date | End Date | | :------------ | :--------- | :------- | | **Weeks 1–6** | June 2 | July 11 | | **Weeks 7–8** | July 14 | July 25 | ## Third cycle *** | Stage | Start Date | End Date | | :------------ | :---------- | :----------- | | **Weeks 1–6** | July 28 | September 5 | | **Weeks 7–8** | September 8 | September 19 | ## Fourth cycle *** | Stage | Start Date | End Date | | :------------ | :----------- | :---------- | | **Weeks 1–6** | September 22 | October 31 | | **Weeks 7–8** | November 3 | November 14 | ## Fifth cycle *** | Stage | Start Date | End Date | | :------------ | :---------- | :-------------- | | **Weeks 1–6** | November 17 | December 26 | | **Weeks 7–8** | December 29 | January 9, 2026 | # 2026 release schedule Source: https://docs.lerian.studio/en/updates-and-versioning/release-cycle-2026 Lerian's 2026 release cadence is being revised — the updated schedule, cycle stages, and target dates will be published here once the new structure is defined. We're revising our release cadence for 2026. As soon as the new structure is defined, we'll publish the updated schedule here. In the meantime, we announce each release as it happens in our [News](/en/news/news). # Our release schedule Source: https://docs.lerian.studio/en/updates-and-versioning/release-schedule Track Lerian's release schedules organized by year — planned cycles, feature freeze windows, and the start and end dates of every stage across products. Here you can find the detailed schedules for each Lerian release cycle, organized by year. Each schedule lists the planned cycles with the start and end dates for each stage. To understand how our release cycle works, see the [Our release cycle](/en/updates-and-versioning/release-cycle) page. Planned cycles for 2026, including start and end dates for each stage. Cycles completed in 2025 with dates for each stage. # Version governance Source: https://docs.lerian.studio/en/updates-and-versioning/versioning-governance Understand Lerian's versioning governance — how changes are communicated, how dependencies stay compatible, and our backward-compatibility promise. Clear governance keeps the versioning process transparent and reliable. Here we detail how changes are communicated, how dependencies are managed, and our long-term commitment to backward compatibility. This gives you full visibility into how Midaz evolves and how we protect your stability along the way. ## Communication of changes *** We communicate every change clearly so teams can prepare and adapt with confidence. * **Release notes** include new features, improvements, bug fixes, known issues, and breaking changes with migration paths. * **Documentation updates** reflect API changes, new configurations, and updated behaviors. * **Pre-release announcements** are shared in advance for major versions. ## Versioning of dependencies *** Dependencies are versioned with the same principles to ensure stability and compatibility. * **Internal dependencies** follow the same versioning scheme as Midaz. * **External dependencies** are managed to stay compatible throughout a major version lifecycle. ## Backward compatibility commitment *** This versioning policy applies to every new release, ensuring stability across the platform. We commit to maintaining the defined **support windows** for all future versions, so teams can adopt upgrades with confidence and without unexpected disruptions. By preserving backward compatibility, we reduce the risk of breaking changes and give organizations the flexibility to plan upgrades on their own schedule. This approach keeps integrations stable while allowing Midaz to evolve at a sustainable pace. For details about timelines and schedules, see the [Release cycle](/en/updates-and-versioning/release-cycle) page. # Versioning process Source: https://docs.lerian.studio/en/updates-and-versioning/versioning-process Understand how versioning works in Lerian — what to expect between releases, how semantic versioning is automated, and how to plan upgrades across majors. Versioning is not just a naming convention; it’s an automated process that ensures consistency across releases. This section explains how automation drives version generation, how support timelines are defined, and what to expect when moving between versions. ## Versioning automation *** To ensure an automated and predictable versioning process, we follow **semantic versioning** principles. ### How it works *** Identifies commits made since the last version. Determines the new version based on predefined rules. Automatically generates and publishes the new version. ## Version support timeline *** Midaz follows a clear support policy that defines what you can expect from each version throughout its lifecycle. * **Active support (current major version):** includes feature enhancements, bug fixes, and security patches. * **Security support (post-active support):** limited to critical security patches only. Older versions do not receive further updates or support. ## Upgrade path recommendations *** Upgrading Midaz versions follows clear recommendations to keep the process predictable and safe. * **Minor version upgrades** (`1.0.0 → 1.1.0`) are simple and require little effort. * **Major version upgrades** (`1.2.0 → 2.0.0`) may involve planning and testing. * Every major version comes with a clear **upgrade guide** to support the transition. # Versioning scheme Source: https://docs.lerian.studio/en/updates-and-versioning/versioning-scheme Learn how Lerian applies semantic versioning, how we manage breaking changes, and how to interpret release candidate designations used for pre-release builds. The versioning scheme describes how we assign and increment version numbers. It clarifies the meaning of major, minor, and patch releases, how we handle breaking changes, and the role of pre-release tags. By following this structure, you can anticipate the impact of each update and plan migrations confidently. ## Semantic versioning *** We follow a **modified semantic versioning** scheme in the format **X.Y.Z \[-designation]**, where: | Version type | Increment frequency | Characteristics | Example | | --------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------- | | **X (Major Version)** | Evaluated every two development cycles, but **only released if there are significant changes**. | **May introduce breaking changes**.

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 *** Breaking changes are part of the natural evolution of the platform. They happen when an update alters or removes behavior in a way that is not fully compatible with previous versions. To keep this process predictable, we follow strict rules and communicate early, allowing your teams to prepare with confidence. * Breaking changes are introduced **only in a major version**. * They are **announced in advance**, always with clear migration guides and practical examples. * Deprecated features **emit warnings** before removal, giving teams time to adjust without sudden impact. * We aim to **minimize disruption** by grouping breaking changes together and providing alternative solutions whenever possible. ### Examples * **Field replacement**: The free-text `route` field in transaction payloads was superseded by `routeId` (UUID); `route` remains accepted for backward compatibility but no longer drives validation. * **Validation rules**: The `ACCOUNT_TYPE_VALIDATION` and `TRANSACTION_ROUTE_VALIDATION` environment variables were replaced by the Ledger Settings API, which controls accounting validation per ledger without redeployment. * **Deprecation cycle**: The `scale` field was removed from transaction amounts in v3, when amount handling moved to a numeric system. ## Pre-release designations *** We use the following pre-release designations **when applicable**: | Designation | Description | Characteristics | Example | | :----------- | :------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------- | :-------------- | | **-alpha.N** | Early development builds. | **Potentially unstable, not for production use**.

May contain incomplete features. | *1.1.0-alpha.1* | | **-beta.N** | Feature-complete builds undergoing testing. | **Suitable for testing environments.**

Feature-frozen, focusing on stability and bug fixes. | *1.1.0-beta.1* | | **-rc.N** | Release Candidates. | **Expected to be stable and production-ready**.

Only critical bug fixes applied before the final release. | *1.1.0-rc.1* | ### Examples **Potential releases** * *1.0.0* → Initial stable release * *1.0.1* → Patch with bug fixes * *1.1.0-alpha.1* → Alpha for next minor * *1.1.0-beta.1* → Beta for next minor * *1.1.0-rc.1* → Release candidate * *1.1.0* → Stable minor release * *1.2.0* → Next minor release * *2.0.0* → New major version # Our versioning strategy Source: https://docs.lerian.studio/en/updates-and-versioning/versions Understand Lerian's versioning strategy — increments, pre-release tags, automation, upgrade paths, and how changes are communicated. Our versioning strategy defines how versions are incremented, how pre-release tags are applied, and how automation keeps everything consistent across the system. Each release follows a [clear cycle](/en/updates-and-versioning/release-cycle) that helps teams plan ahead and adopt updates with confidence. Alongside the technical rules, we provide guidance on upgrade paths, outline our support policy, and communicate changes in a transparent way. This ensures that moving between versions is safe, predictable, and aligned with your operational needs. # Welcome to our guides Source: https://docs.lerian.studio/en/welcome-to-our-guides Navigate Lerian's product guides — practical walkthroughs that turn complex financial infrastructure into real-world solutions across Midaz and plugins. At Lerian, **Guides are more than walkthroughs. They're your go-to resource for turning complex infrastructure into real-world solutions**. ## How to navigate *** Each guide is structured to help you integrate confidently and move fast. To access a product’s guide: You’re already in the **Guides** section of our documentation. Open the dropdown menu in the left sidebar and select the product you’re working with. You’ll land in a dedicated space with product-specific content, including related plugins and real-world examples. # Who we are Source: https://docs.lerian.studio/en/who-we-are Learn about Lerian's mission, identity, and the principles that guide our approach to modern, open financial infrastructure and core banking software. At Lerian, we build financial infrastructure that gives institutions control. We combine deep financial expertise with modern, open technology to help organizations operate independently, scale confidently, and adapt without friction. We don’t believe in black boxes. We believe in clarity, ownership, and long-term autonomy. ## What we do *** We help financial institutions design and operate modern core banking ecosystems using open technologies. By providing a programmable, modular foundation, we enable teams to move faster without compromising control over their operations or their architecture. Our platform is built for institutions that demand more than off-the-shelf solutions: * **Digital banks** launching new financial products * **Traditional banks** modernizing legacy cores incrementally * **Fintechs** building embedded finance and payment platforms * **Enterprises** streamlining payments, collections, and reconciliation across business units ## Our principles *** Our approach to building financial software is guided by three core principles that prioritize the long-term success of our clients: #### Independence over vendor lock-in We build systems that you control. By using standard interfaces and open formats, we ensure that your roadmap is never dictated by a single vendor's priorities. #### Transparency over opacity Financial systems require absolute clarity. We design our infrastructure to be inspectable and auditable, eliminating hidden logic and opaque behaviors that introduce systemic risk. #### Architecture over shortcuts We prioritize robust, scalable design over quick fixes. Our solutions are built to handle real-world complexity, ensuring that your infrastructure grows alongside your business needs without requiring constant re-platforming. ## Explore further *** Understand the problems we solve and how our platform helps financial institutions regain control. Discover the core values and commitments that drive our team and shape our culture. ## Get in touch *** Have questions? Exploring a partnership? Evaluating architecture? Let’s talk. Reach out to our team to discuss how Lerian can support your infrastructure needs. # Why Lerian Source: https://docs.lerian.studio/en/why-lerian Understand how Lerian helps financial institutions modernize core banking without vendor lock-in — open ledgers, modular plugins, and flexible deployment. Modern financial institutions face a common set of challenges: legacy systems that cost too much to maintain, adapt too slowly, and lock you into vendors who dictate the roadmap. Lerian gives you back control. **New to core banking?** If terms like *Ledger*, *debit and credit*, or *chart of accounts* aren't yet second nature, start with the fundamentals first — they'll make everything here click into place. **[Begin with Core Banking Fundamentals](/en/fundamentals/core-banking/what-is-core-banking)** ## The problems we solve *** | Challenge | How Lerian helps | | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Vendor lock-in** | Midaz, our core ledger, is source-available, so you own the foundation of your infrastructure. No single vendor controls your roadmap | | **Slow time-to-market** | Modular platform lets you launch new products in weeks, not months | | **Compliance burden** | Built-in audit trails, regulatory reporting, and immutable records reduce manual compliance work | | **Reconciliation errors** | Automated reconciliation replaces manual spreadsheet matching, catching discrepancies in real time | | **Operational cost** | Replace multiple disconnected systems with one composable platform | | **Scaling limitations** | Handle millions of transactions per day without re-platforming | ## What the platform includes *** | Business need | Lerian product | What it does | | :----------------------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------- | | Central financial record-keeping | **Midaz** | Manages accounts, balances, and transactions with full audit trails | | Regulatory and business reporting | **Reporter** | Generates compliance reports (BACEN, SOX) and business analytics automatically | | Transaction reconciliation | **Matcher** | Compares internal records against bank statements and payment processors — flags discrepancies | | Real-time transaction controls | **Tracer** | Validates transactions against spending limits and business rules before they execute | | Workflow automation | **Flowker** | Automates approval flows, anti-fraud checks, and payment orchestration | | Data ingestion from external systems | **Fetcher** | Connects to external databases and extracts data on schedule, feeding Reporter, Matcher, and your own services | | Credit products and loan management | **Lender** | Defines loan products and manages the loan lifecycle — applications, disbursement, and interest accrual | | Event delivery to your systems | **Streaming Hub** | Fans platform events out to your systems through signed webhooks, queues, or a pull API | ## Deployment flexibility *** You choose how to run the platform: * **Fully managed (SaaS)**: Lerian handles everything. Your team focuses on configuration and business logic. Fastest path to production. * **Your infrastructure (BYOC)**: Deploy in your own cloud or on-premises. Full control over data residency, security, and compliance boundaries. Both options support the full product portfolio. Choose based on your regulatory requirements and operational maturity. [Compare deployment models in detail →](/en/deployment-models) ## Built for trust and independence *** * **Source-available core**: Midaz, our ledger, is source-available — inspect, audit, and extend the core that holds your money. No black boxes at the foundation * **No vendor lock-in**: Standard interfaces and open formats across the platform. You can always move * **Compliance-ready**: Immutable audit trails, double-entry enforcement, and regulatory reporting ship built in — not bolted on * **Incremental adoption**: Start with one product and expand as needed. No big-bang migration required ## Who uses Lerian *** * **Digital banks** launching new financial products * **Traditional banks** modernizing legacy cores incrementally * **Fintechs** building embedded finance and payment platforms * **Enterprises** streamlining payments, collections, and reconciliation across business units ## Next steps *** Understand the four pillars that form our architectural philosophy. Choose your path — evaluate the platform or start building. Understand the differences between fully managed and self-hosted deployment. Have questions? Exploring a partnership? Let's talk. # About Midaz Console Source: https://docs.lerian.studio/en/midaz/about-midaz-console See how the standalone Midaz Console moved into the Lerian Console Midaz Module in v3.5.0+, and what maintenance mode means for older source-available installs. The **Midaz Console** was the visual administrative interface for Midaz. You used it to configure organizations, ledgers, accounts, assets, and transactions, with no direct API calls. From Midaz v3.5.0, the Midaz source-available repository no longer includes the Console. Its features moved into the **Lerian Console** as the [Midaz Module](/en/midaz/console/midaz-module) — a dedicated product module in Lerian's unified platform. ## What changed *** The Midaz Console evolved from a standalone source-available interface into an integrated module in the Lerian Console. This change separates Midaz, the core ledger engine, from the platform tools that manage it. | Aspect | Before (prior to v3.5.0) | Now (v3.5.0+) | | :--------------- | :-------------------------------- | :-------------------------------------------------------- | | **Distribution** | Bundled with the Midaz repository | Part of the Lerian Console | | **License** | Source-available | Commercial (Lerian Console) | | **Scope** | Standalone admin interface | Integrated product module with cross-product capabilities | | **Development** | Community-driven | Maintained and developed by Lerian | Midaz itself remains fully source available. Only the Console — the visual management interface — moved to the Lerian Console platform. ## For existing users *** If you run a Midaz version before v3.5.0, the Console remains part of your source-available installation. It works as expected in those versions. The standalone Midaz Console is in **maintenance mode**. It receives critical security patches only, with no new features or improvements. To keep active development and support, migrate to the Lerian Console. ## Moving forward *** The [Midaz Module](/en/midaz/console/midaz-module) in the Lerian Console provides everything the standalone Console offered. It adds unified authentication, cross-product navigation, and ongoing feature development. To see what the Midaz Module offers, read the documentation: Full documentation for the Midaz Module within the Lerian Console. Learn about the unified platform for managing all Lerian products. # Auditability Source: https://docs.lerian.studio/en/midaz/auditability Understand the scoped audit mechanisms in Midaz: CRM protection events and optional Ledger transaction audit messages. Midaz does not provide one universal audit trail for every platform action. Its audit behavior is scoped to the mechanism that produces the data. ## CRM protection audit events *** When CRM runs with envelope encryption, Midaz emits **Protection Audit Events** for encryption-protection activity. These events are persisted per Organization and can be read through the protection-audit endpoint. This mechanism is not available in legacy encryption mode. The audit writer is best-effort: a failed audit emission is logged, but it does not block the CRM operation. Use protection audit events to investigate encryption-protection activity for an Organization. Do not interpret them as a complete record of every Ledger or platform action. ## Ledger transaction audit messages *** The Ledger publishes transaction audit messages unless `AUDIT_LOG_ENABLED` is explicitly set to `false`. The built-in environment example sets this flag to `false`. Keep the flag set to `false` until your downstream consumer, its retention, and its investigation workflows are configured. These messages are sent to the configured audit queue for downstream storage and processing. Midaz does not expose them as a general queryable Ledger audit-record API; the receiving system owns retention, indexing, and investigation workflows. ## Designing your audit trail *** Choose the mechanism that matches the data you need to observe: * Use **Protection Audit Events** for CRM envelope-encryption activity. * Enable **Ledger transaction audit messages** only when your downstream consumer is configured to retain and process them. * Keep controls, retention, and evidence requirements in the system responsible for the data after Midaz emits it. For help designing an audit and retention architecture around Midaz, [contact us](https://lerian.studio/contact). # Community support Source: https://docs.lerian.studio/en/midaz/community-support Connect with the Midaz community on GitHub Discussions and Discord to ask questions and share feedback, or use GitHub Issues to report bugs. Midaz is a source-available, mission-critical service, and a growing community of builders powers its evolution. If you're using the Community version, this page outlines the channels where you can ask questions, report bugs, and share ideas. If you're an **Enterprise** customer, use the [**Lerian Support Portal**](/en/midaz/lerian-support-portal) to get guaranteed response times and access to our dedicated support team. ## GitHub Discussions *** Use the [Discussions tab](https://github.com/LerianStudio/midaz/discussions) in our public repository to: * Ask questions or request help. * Propose enhancements. * Share use cases or feedback. * Participate in existing threads. We monitor this space regularly and encourage open conversation. Don’t be shy — your question might help others too. ## Report a bug *** Use [GitHub Issues](https://github.com/LerianStudio/midaz/issues) to report a bug. Include the behavior you observed, the expected result, and enough context for maintainers to reproduce it. ## Join our Discord *** For real-time conversations, join the [Lerian Discord community](https://discord.gg/DnhqKwkGv3). This is the place for: * Quick questions and peer support. * Staying in the loop with the latest updates. * Talking directly with maintainers and other users. Look for the **#midaz** channel to dive right in. ## Contribute *** If you’re ready to go a step further: * Check out issues labeled `good first issue` or `help wanted`. * Fork the repo, make your changes, and submit a PR. * Share your own plugin ideas or integrations. All contributions are welcome, whether you're fixing a typo or designing a new feature. New to source available? Start by reading our [contribution guidelines](https://github.com/LerianStudio/midaz/blob/main/CONTRIBUTING.md). We keep them beginner-friendly. ## Not sure where to start? *** You don’t need permission to get involved. Ask. Suggest. Explore. Midaz gets better with every question and every idea shared. Let’s build something amazing, together. # Setting up accounting in the Console Source: https://docs.lerian.studio/en/midaz/console/accounting-setup-in-console Walk through a Console-only guide to build your accounting model in Midaz — from planning a chart of accounts to running your first Pix payment. This guide shows you how to set up accounting in the Midaz Module of the Lerian Console. You use only the screens and forms in the Console — no code. It is the Console companion to the developer-focused [Accounting Walkthrough](/en/midaz/accounting-walkthrough). You reach the same result, but through **what you click and configure**, not what you send to an API. It targets product managers, implementation teams, and developers who prefer to model their accounting in the UI. We keep technical terms where they matter. We always explain what they mean when you see them on screen. You will not see any JSON or API calls here. You do everything below through Console forms. To automate the same setup later, follow the technical reference each section links to. ## What you are building *** Accounting in Midaz uses **double-entry bookkeeping**. You do not do the bookkeeping by hand — the Console and the ledger enforce it for you. Keep one rule in mind: > Every transaction moves value **from** one place **to** another. One side gives (debit), one side receives (credit), and the two always balance. You model your accounts and routes once. From then on, the ledger balances every transaction automatically and keeps it auditable. The setup follows a logical order. Each layer builds on the one before it: Decide which balances your product needs (customer funds, fees, settlement, treasury, revenue). Define the categories that classify your accounts. Open the actual balance containers, each with a **Type** and an Asset. You select the Type for non-external Accounts; enabling **External account** sets it to `external` automatically. When **Validate Account Type** is enabled, the Type of every non-external Account must match a registered Account Type. Set the rules for which accounts can join each transaction and how the ledger posts entries. Work top to bottom. Routes are much easier to build once you already know which accounts represent customers, treasury, fees, and settlement. ## Step 1 — Plan your chart of accounts *** In traditional accounting, a **Chart of Accounts (CoA)** is the master list of every account category your business uses: assets, liabilities, revenue, and expenses. It also defines how you classify each movement against those categories. In Midaz there is **no single "Chart of Accounts" screen to fill in**. Instead, your CoA emerges from the building blocks you create in the Console: Assets, Account Types, and Accounts. You plan it up front, mostly as a paper (or whiteboard) exercise. Before you open the Console, list the balances your product needs. For a typical payments product that might be: | Balance you need | What it represents | | ---------------- | --------------------------------- | | Customer funds | Money your end users hold | | Settlement | Funds waiting to clear | | Fee revenue | Fees you collect | | Fee expense | Fees you pay to providers | | Treasury | Your own internal operating funds | This list is your blueprint. The next steps turn each line into something concrete in the Console. Before any account can exist, it needs an **Asset** — the unit of value it holds (for example `BRL`). If you have not created your assets yet, start with [Creating an Asset](/en/midaz/console/creating-an-asset). ## Step 2 — Create your Account Types *** **Account Types** are the categories that classify your accounts. Think of them as labels like `customer`, `treasury`, or `fee` that group accounts by their role. Later, Accounting Routes use these labels to decide which accounts a transaction can use. In the Console you create one Account Type per category from your blueprint — not one per individual customer. Open the New Account Type form and define a category with a clear name and a stable key value. A typical payments setup uses these Account Types: | Account Type | Use it for | | ------------ | -------------------------- | | `customer` | Liquid customer balances | | `settlement` | Funds awaiting clearing | | `fee` | Fees collected as revenue | | `treasury` | Internal operations | | `expense` | Fees paid out to providers | The **Key Value** of an Account Type (for example `customer`) is what routes and accounts rely on. Keep it short, lowercase, and stable — changing it later means recreating the accounts and routes that depend on it. The Account Types menu appears only after you enable **Validate Account Type** in your Ledger settings. To turn it on, see [Managing Ledgers](/en/midaz/console/managing-ledgers-via-console#ledger-settings). ## Step 3 — Create your Accounts *** **Accounts** are the balance containers — the things that hold value and that money moves between. Each account has a **Type** and one Asset (its currency). You select the Type for non-external Accounts; enabling **External account** sets it to `external` automatically. When **Validate Account Type** is enabled, the Type of a non-external Account must match a registered Account Type (external Accounts skip this check). A human-readable **alias** identifies it; `@customer_123_brl` is one common convention. For each line in your blueprint, create one Account in the Console. Open the New Account form, choose its Type and Asset, and give it a clear alias. When you fill in the form, a few choices are permanent and worth getting right the first time: | Field | Why it matters | | ----------------- | ------------------------------------------------------------------------------------------- | | **Account Alias** | The name routes and transactions use to find the account. Cannot be changed after creation. | | **Type** | The Account Type that classifies it. Cannot be changed after creation. | | **Asset** | The currency or unit it holds. Cannot be changed after creation. | The Console **locks Alias, Type, and Asset when you save the account**. To change any of them, create a new account. Double-check before you save. ### Understanding what a balance actually shows When you open an account in the Console, its balance is not a single number. Midaz splits every balance into two **buckets**. You always know what you can spend and what the ledger holds back. In plain language: | Bucket | What it means when you look at an account | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Available** | Money that is free to spend or send **right now**. This is the number that goes up and down with normal payments. | | **On hold** | Money that a pending operation has **reserved** but not yet finalized. It still belongs to the account. The ledger sets it aside, and you cannot spend it until the hold is confirmed or cancelled. | Both amounts appear as exact decimal values (for example, `12.50`). There is no scale factor to apply when you read them. **On hold** powers two-step payments. When you authorize a payment but do not yet capture it, the amount moves from **Available** to **On hold**. Confirming the payment releases it to the destination. Cancelling returns it to Available. You see each of these movements on the account at every stage. ## Step 4 — Create your Accounting Routes *** With **Validate Routes** enabled, each transaction must specify a valid Accounting Route in the ledger. An Accounting Route is a reusable rule for one kind of transaction, such as a *Pix transfer* or a *fee charge*. It answers three questions: * Which accounts can send on the **source** side? * Which accounts can receive on the **destination** side? * Which debit and credit **entries** should the ledger post when it runs? The Console builds these through a guided 3-step wizard, so you do not have to assemble anything by hand. See how the Accounting Routes page works and what each part of the wizard does. Walk through the 3-step wizard to define a route, its operation rules, and its entries. To decide how to shape a route, see [Accounting rules](/en/midaz/console/mc-accounting) — it explains the choices in plain terms. The essentials: Each rule inside a route applies to one side of a transaction: * **Source** — the sending side (where value comes from). * **Destination** — the receiving side (where value lands). * **Bidirectional** — the same rule applies to both sides, for cases where one kind of account can both send and receive. A valid route needs at least one Source **and** one Destination, or a single Bidirectional rule. Each rule checks accounts in one of two ways: * **Account Type** — the rule accepts any account of a given category (for example, any `customer` account can send). Use this for flexible, scalable flows. * **@Alias** — the rule accepts only one exact account (for example, only `@fee_revenue` can receive). Use this for fixed operational accounts like treasury, fees, or settlement. Scenarios decide how the debit and credit entries get recorded: * **Direct** — a one-step movement, posted immediately. * **Two-Step** — a hold-then-commit flow. It uses separate entries to reserve, confirm, and cancel funds, and it drives the **On hold** bucket. * **Reversal** — entries the ledger records to undo a completed transaction. * **Overdraft** — entries the ledger records when a debit exceeds the account's available funds and draws on an overdraft line. * **Block** — entries the ledger records when funds on an account are blocked or unblocked. Turn on **Validate Routes** in Ledger settings **only after** the routes you need already exist. If validation is on but a matching route is missing, those transactions will fail. ## Putting it together — a simple Pix payment *** Let's run the whole flow in the Console for a basic **Pix cash-out**: a customer sends BRL out of their wallet to a settlement account. Assume your `BRL` asset already exists. From the **Account Types** page, create: * `customer` — for end-user balances. * `settlement` — for funds leaving to the outside world. See [Creating an Account Type](/en/midaz/console/creating-an-account-type). From the **Accounts** page, create: * `@customer_123_brl` — Type `customer`, Asset `BRL`. The customer's wallet. * `@external_brl` — Type `settlement`, Asset `BRL`. Where funds settle when they leave the ledger. See [Creating an Account](/en/midaz/console/creating-an-account). `@external_brl` is a regular, ledger-owned settlement Account. This example uses it so the route can validate the `settlement` Account Type. It is not the canonical external Account `@external/BRL`, which Midaz creates automatically together with the `BRL` Asset; the `@external/` alias prefix is reserved, so you cannot create that Account yourself. For money that actually enters or leaves Midaz, use `@external/BRL`. See [Common mistakes to avoid](/en/midaz/common-mistakes-to-avoid). From the **Accounting Routes** page, start the wizard and build a `Pix cash-out` route: * A **Source** operation rule validating Account Type `customer` (the wallet sends). * A **Destination** operation rule validating Account Type `settlement` (the settlement account receives). * A **Direct** accounting scenario, with a debit entry on the source and a credit entry on the destination. See [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route). Create a transaction that moves, say, `100.00 BRL` from `@customer_123_brl` to `@external_brl` using your `Pix cash-out` route. See [Creating a Transaction](/en/midaz/console/creating-a-transaction). Open each account and look at the balance: * `@customer_123_brl` — **Available** drops by `100.00`. * `@external_brl` — **Available** rises by `100.00`. Both movements share the same transaction, giving you a clean, balanced audit trail. Need an authorize-then-capture flow instead of an instant one? Use a **Two-Step** scenario on the route. You will then watch the amount move into **On hold** when reserved, and out of it when you confirm or cancel. ## What to do next *** You now have a working accounting model, built entirely in the Console. To go deeper, use these technical references. They cover how to automate the setup, understand the entities, and turn ledger activity into reports: The end-to-end developer version of this guide, including the data model and double-entry detail. How the core accounting primitives relate to one another. The technical model behind Accounting Routes, operation routes, and entries. The full balance model behind available and on-hold amounts. # Adding a Portfolio to an Account Source: https://docs.lerian.studio/en/midaz/console/adding-portfolio-to-account Link a Midaz Account to a Portfolio from the Console — either from the Accounts list or the edit form — to group Accounts under one customer or wallet. Use Portfolios to group Accounts that belong to the same Entity. To add an Account to a Portfolio, follow one of these procedures: ## Adding from the Accounts page If the Account does not belong to a Portfolio yet, add it directly from the **Accounts** page. Follow these steps: Click the **Link** option in the **Portfolio** column of the Account you want to add. The Console displays the form to edit the Account. Switch to the **Portfolio** tab and select the **Portfolio** you want. Click the **Save** button to link the Account to the selected Portfolio. ## Add by editing the Account You can also edit the **Account** to add or change its Portfolio: On the **Accounts** page, find the Account you want to edit. Click the three dots () in the **Actions** column. Select **Details** to open the Account details page. Click the **Edit** button. In the edit form, select the **Portfolio** you want. Click the **Save** button to confirm the procedure. [Update an Account endpoint](/en/reference/midaz/update-an-account) # Adding a Segment to an Account Source: https://docs.lerian.studio/en/midaz/console/adding-segment-to-account Assign a Segment to an existing Account from the Console to classify it by shared characteristics and apply the associated benefits or business rules. Segments group Accounts by shared characteristics and benefits. To learn more about Segments, see the [Segments](/en/midaz/segments) page. To add a Segment to an Account, follow these steps: Open the **Accounts** page. Find the Account you want to add to the Segment. In the **Actions** column, click the three dots (). Select **Details**. Select the **Account Details** tab. Select the Segment from the drop-down list. Click the **Save** button to confirm. [Update an Account endpoint](/en/reference/midaz/update-an-account) # Checking Transaction details Source: https://docs.lerian.studio/en/midaz/console/checking-transaction-details Open the full details of a Transaction in the Console to audit its movements, review the Transaction Data tab, and edit its description when needed. To check the details of a transaction, follow these steps: On the **Transactions** page, find the transaction to check. In its **Actions** column, click the three dots (). Select **See details**. A page opens with the transaction summary. Then select the **Transaction Data** tab to see more information. In the **Transaction Data** tab, edit the description if needed. # Creating a Ledger Source: https://docs.lerian.studio/en/midaz/console/creating-a-new-ledger Create a new Ledger in the Console to isolate Assets, Accounts, Routes, and Transactions, and configure Account Type or Route validation while you set it up. A Ledger isolates Assets, Accounts, Routes, and Transactions in one accounting workspace. Create a new Ledger when you need a separate balance domain, product line, environment, or accounting model. ## Before you start *** You need an existing Organization. If this is the first setup, create the Organization first in [Creating an Organization](/en/midaz/console/creating-an-organization). Decide what the Ledger boundary means before you create it. Do not use one Ledger for unrelated products that need different account structures, routes, or operational controls. ## Create the Ledger *** From the **Ledgers** page, click the **New Ledger** button in the top-right corner. The **New Ledger** form appears. In the **Ledger Details** tab, enter the Ledger name in the **Ledger Name** field. (Optional) To include metadata, select the **Metadata** tab. This tab displays two fields: **Key** and **Value**. * To add more metadata, click the plus (+) icon next to the first row. (Optional) Select the **Settings** tab to configure validation rules for this Ledger. * Enable **Validate Account Type** if Accounts must use registered Account Types. * Enable **Validate Routes** if Transactions must follow pre-configured Accounting Routes. After you complete the form, click **Save**. A success message appears in the top-right corner. The main **Ledgers** page now lists the new Ledger. ## Field guide *** | Field | What to enter | Example | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | Ledger Name | A clear name for the accounting workspace. Use a name that shows the product, country, environment, or business boundary. | `Brazil Payments Ledger` | | Metadata Key | Optional classification or integration field name. | `environment` | | Metadata Value | Value for the metadata key. | `production` | | Validate Account Type | Optional setting. Turn on when every non-external Account in this Ledger must use a previously registered Account Type. Leave off if you will not classify Accounts by type yet. | `Enabled` | | Validate Routes | Optional setting. Turn on only after the Accounting Routes for this Ledger are planned or configured, because Transactions must match a valid route when this is enabled. | `Disabled` | The fields in the Metadata tab are open. The tab has no predefined keys or values. Use consistent key names across your Organization, such as `environment`, `businessLine`, or `region`, so you can filter and audit Ledgers later. ## Ledger settings *** The **Settings** tab controls the validation rules for this Ledger. ### Validate Account Type When you enable this setting, every new non-external Account must use an Account Type registered for this Ledger. Enable it when you want operators to classify Accounts consistently, such as `customer`, `merchant`, `treasury`, or `fee`. After you enable **Validate Account Type**, the Ledger rejects any non-external Account without a valid Account Type. ### Validate Routes When you enable this setting, Transactions must match the Accounting Routes configured for this Ledger. Enable it only after you plan or configure your route model. If you enable **Validate Routes** before routes exist, any Transaction without a matching route fails validation. ## What to do next *** After you create the Ledger: 1. Review whether **Validate Account Type** and **Validate Routes** are correct for this Ledger. 2. Create at least one Asset in [Creating an Asset](/en/midaz/console/creating-an-asset). 3. If you need to change the validation rules later, use the **Settings** tab described in [Managing Ledgers](/en/midaz/console/managing-ledgers-via-console#ledger-settings). [Create a Ledger endpoint](/en/reference/midaz/create-a-ledger) # Creating a Portfolio Source: https://docs.lerian.studio/en/midaz/console/creating-a-portfolio Create a new Portfolio in the Console to group Accounts under a customer, business unit, or reporting boundary, and set an Entity ID for external systems. A Portfolio groups Accounts under a customer, business unit, or reporting boundary. ## Before you start *** You need an existing Organization and Ledger. Create a Portfolio when you want to group Accounts under a customer, business unit, product, or reporting boundary. Entity ID is optional. You can update it later through the Portfolio update API, provided the new value is non-empty. ## Create the Portfolio *** To create a Portfolio, follow these steps: From the **Portfolios** page, click the **New Portfolio** button. The **New Portfolio** form opens on the right side of the screen. Fill in the fields and click the **Save** button. ## Field guide *** | Field | What to enter | Example | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | Portfolio Name | Required name for the customer, business unit, or reporting group. | `Acme Retail Portfolio` | | Entity ID | Optional external entity or system identifier. You can update it later through the Portfolio update API when the new value is non-empty. | `customer_acme` | | Metadata Key | Optional classification or integration field name. | `relationshipManager` | | Metadata Value | Value for the metadata key. | `ops-team-1` | Entity ID is optional. You can update it later through the Portfolio update API, provided the new value is non-empty. ## What to do next *** Now link the Portfolio to Accounts from [Adding Portfolio to Account](/en/midaz/console/adding-portfolio-to-account). You can also create Accounts that belong to it in [Creating an Account](/en/midaz/console/creating-an-account). [Create a Portfolio endpoint](/en/reference/midaz/create-a-portfolio) # Creating a Segment Source: https://docs.lerian.studio/en/midaz/console/creating-a-segment Create a new Segment in the Console to classify Accounts by shared characteristics, business rules, or reporting needs across your Ledger. A Segment classifies Accounts by shared characteristics or business rules. ## Before you start *** You need an existing Organization and Ledger. Create a Segment when a group of Accounts needs a shared classification for reporting, filtering, or fee rules. ## Create the Segment *** To create a Segment, follow these steps: From the **Segments** page, click the **New Segment** button. The **New Segment** form opens on the right side of the screen. In the **Segment Details** tab, enter the **Segment Name**. (Optional) Select the **Metadata** tab to add metadata. When the details are complete, click the **Save** button. The Segment appears in the list of Segments for your Ledger. ## Field guide *** | Field | What to enter | Example | | -------------- | --------------------------------------------------------------------------- | ------------------ | | Segment Name | Required name for a group of Accounts with shared characteristics or rules. | `Retail Customers` | | Metadata Key | Optional classification or integration field name. | `region` | | Metadata Value | Value for the metadata key. | `BR-SP` | The Metadata fields have no predefined keys or values. Use consistent key names across your Organization, such as `region`, `businessLine`, or `channel`. Consistent keys keep Segments useful for filtering and reporting. ## What to do next *** After you create the Segment, link it to Accounts from [Adding Segment to Account](/en/midaz/console/adding-segment-to-account). You can also create Accounts that belong to it in [Creating an Account](/en/midaz/console/creating-an-account). [Create a Segment endpoint](/en/reference/midaz/create-a-segment) # Creating a Transaction Source: https://docs.lerian.studio/en/midaz/console/creating-a-transaction Create a new Transaction in the Console using the 1:1 or n:n wizard to move Asset balances between source and destination Accounts by alias. A Transaction moves value between Accounts in a Ledger. Create one only after the Asset, source account, destination account, and any required Accounting Route are ready. ## Before you start *** Confirm that: * The source and destination Accounts exist in the selected Ledger. * Both Accounts use the Asset selected for the Transaction. * You know each Account alias. A leading `@` is a common convention, not a requirement. * Source accounts allow sending and destination accounts allow receiving. * If **Validate Routes** is on, the Transaction must match an existing Accounting Route. ## Create the Transaction *** From the **Transactions** page, click the **New Transaction** button. The **New Transaction** modal will open. Select the type of transaction you want to create: * **Simple 1:1** - Transactions using one source and one destination * **Complex n:n** - Transactions using more than one source or destination After selecting the transaction type, the **New Transaction** page opens with a step-by-step form: Fill in the basic transaction information: * (Optional) Add a **Description** for the transaction * (Optional) Specify an **Accounting route group** for classification * Enter the **Value** (amount) to transfer * Select the **Asset** for the transaction Click **Next** to proceed to the accounts selection step. Specify the accounts involved: * Search for the **source account** by alias and select it * Search for the **destination account** by alias and select it * For complex transactions, you can add multiple sources and/or destinations You **must** use the account alias to perform transactions. Make sure you save it when you create your account. If you forget your alias, you can find it by [editing account information](/en/midaz/console/editing-account-information). Click **Next** to proceed to the operations step. Review and adjust the operation details: * For simple transactions (1:1), the Console fills the amounts automatically * For complex transactions (n:n), specify the amount for each source (debit) and destination (credit) * Expand each operation to add optional descriptions and metadata For complex transactions, the sum of all source amounts must equal the sum of all destination amounts. When all the information is correct, click the **Go to Review** button. The Console shows a review page with a summary of all the transaction information: * Transaction value and asset * Source and destination accounts * Individual debit and credit operations * Metadata records (if any) Review the information carefully. If everything is correct: * Click **Send Transaction** to submit and view the transaction details * Click **Send and Create another** to submit and immediately start a new transaction On success, the Console opens the transaction details page with the transaction summary, data, and status. ## Field guide *** **Transaction Data** | Field | What to enter | Example | | ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------ | | Transaction description | Optional business description of the movement. | `Customer payment to merchant` | | Accounting route group | Optional route classification when your setup uses route validation or grouping. | `Pix Transfer Group` | | Transaction Route | Select the Transaction Route associated with the transaction. | `Pix Transfer Route` | | Asset | Asset used by the Transaction. It must match the selected Accounts. | `BRL` | | Value | Total amount to move. For complex transactions, source totals must equal destination totals. | `100.00` | **Source/Destination** | Field | What to enter | Example | | ----------- | ------------------------------------ | --------------- | | Source | Account alias that will be debited. | `@customer_123` | | Destination | Account alias that will be credited. | `@merchant_456` | **Operations** | Field | What to enter | Example | | --------------------- | --------------------------------------------------------------------- | ------------------------------------- | | Operation description | Optional description for the operation. | `Debit for the source account` | | Chart of accounts | Optional name of the Chart-of-Accounts that the operation belongs to. | `Pix debit - source` | | Operation Route | Optional operation route for this transaction leg. | `Checking account validation for Pix` | | Metadata Key | Optional classification or integration field name. | `externalReference` | | Metadata Value | Value for the metadata key. | `order_98765` | ## Transaction types explained *** ### Simple Transaction (1:1) Use for straightforward transfers between two accounts: * One source account (debit) * One destination account (credit) * Same amount debited and credited **Example**: Transfer \$100 from Customer A to Merchant B. ### Complex Transaction (n:n) Use for transactions involving multiple parties: * Multiple source accounts (debits) and/or multiple destination accounts (credits) * You can distribute amounts across accounts * Total debits must equal total credits **Examples**: * Payment with fees: * Customer pays \$103 * Merchant receives \$100 * Fee account receives \$3 * Consolidated payment: Multiple customers fund a single vendor payment The fields in the Metadata tab are open. There are no default keys or values. Use consistent key names across your Organization, such as `externalReference`, `orderId`, or `channel`, to link Transactions back to source systems for reconciliation and audit. ## What to do next *** After sending the Transaction, review the details page and confirm the status, source operation, destination operation, and account balances. Create a Transaction programmatically with the JSON endpoint. Inspect the Transaction status and operations via API. # Creating an Account Source: https://docs.lerian.studio/en/midaz/console/creating-an-account Create a new Account in the Console, tie it to an Asset, set its alias and type, and optionally link it to a Portfolio or add metadata during setup. You **must** link an Account to an existing Asset. [Create the Asset](/en/midaz/console/creating-an-asset) before you create the Account. ## Before you start *** You need an existing Organization, Ledger, and Asset. If the Ledger uses **Validate Account Type**, create the required Account Types before you open this form. Decide what the account represents before you choose its type and alias. Common examples include customer, merchant, treasury, fee revenue, settlement, suspense, and expense accounts. ## Create the Account *** To create an Account, follow these steps: From the **Accounts** page, click the **New Account** button. The **New Account** form will open on the right side of the screen with three tabs: **Account Details**, **Portfolio**, and **Metadata**. Fill in the required information in the **Account Details** tab. For more information about the fields in the form, refer to the [Field guide](#field-guide) section. *(Optional)* Switch to the **Portfolio** tab to link this account to a portfolio for reports and management. * Select a portfolio from the dropdown list * If no portfolios exist, you can [create one first](/en/midaz/console/managing-portfolios) * You can link or unlink accounts from portfolios at any time *(Optional)* Switch to the **Metadata** tab to add custom key-value pairs for additional context or integration purposes. Click the **Save** button to create the Account. ## Field guide *** | Field | What to enter | Example | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | Account Name | Optional descriptive name for the account. Midaz accepts up to 256 characters and supplies a default name if no name is submitted. | `John Doe Checking Account` | | Account Alias | Unique human-readable identifier. A leading `@` is a common convention, not a requirement. This cannot be changed after creation. | `@customer_john_checking` | | Type | Account classification. Required. When Account Type validation is enabled, you select from the predefined Account Types; otherwise you type it freely. This cannot be changed after creation. | `customer` | | External account | Switch available when creating an account. Turn it on to create an external account — the type is set to `external` automatically and represents balances held outside this ledger. | — | | Entity ID | Optional external holder, customer, department, or partner identifier. | `customer_123` | | Asset | Asset this account will use. Select an existing Asset. This cannot be changed after creation. | `BRL` | | Segment | Optional grouping category, available when Segments exist. | `Retail` | | Portfolio | Optional reporting or management group for this Account. | `Customer Portfolio` | | Metadata Key | Optional classification or integration field name. | `costCenter` | | Metadata Value | Value for the metadata key. | `BR_11101997` | ## Quick examples *** **Scenario**: Create a checking account for a customer. | Field | Value | | --------------- | ------------------------ | | Account Name | `John Doe Checking` | | Account Alias | `@customer_123_checking` | | Type | `checking` | | Entity ID | `customer_123` | | Asset | `USD` | | Allow Sending | `true` | | Allow Receiving | `true` | **Scenario**: Create a main treasury account for company operations. | Field | Value | | --------------- | ------------------- | | Account Name | `Main Treasury` | | Account Alias | `@treasury_main` | | Type | `treasury` | | Entity ID | `org_treasury_dept` | | Asset | `USD` | | Segment | `Finance` | | Allow Sending | `true` | | Allow Receiving | `true` | **Scenario**: Create a budget account for the engineering department. | Field | Value | | --------------- | ------------------------- | | Account Name | `Engineering Budget 2024` | | Account Alias | `@eng_budget_2024` | | Type | `expense` | | Entity ID | `dept_engineering` | | Asset | `USD` | | Segment | `Engineering` | | Allow Sending | `true` | | Allow Receiving | `true` | ## Important notes *** You cannot change the following fields after you create the account: * **Account Alias** - Choose it carefully, because transactions use the alias * **Type** - You cannot reclassify it after you set it * **Asset Code** - You cannot change the account's currency or commodity To change any of these fields, create a new account. If your ledger uses Account Type validation: * You select the **Type** from your predefined Account Types instead of typing it freely * You must select from predefined account types or create them first * This ensures consistency across your organization's chart of accounts See [Managing Accounts](/en/midaz/console/managing-accounts#account-type-validation) for more information. These flags control transaction permissions: * **Allow Sending = false**: You cannot use the account as a source (debit side) in transactions * **Allow Receiving = false**: You cannot use the account as a destination (credit side) in transactions Both flags default to on. You can change them only after you create the account. Use these flags to: * Freeze an account temporarily * Create a receive-only account (for example, escrow) * Create a send-only account (rare case) After you create an account, you can use it in transactions immediately. Make sure all the information is correct before you save, especially the immutable fields. ## What to do next *** After you create the source and destination Accounts: 1. Create or review Accounting Routes in [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route). 2. Create a Transaction in [Creating a Transaction](/en/midaz/console/creating-a-transaction). [Create an Account endpoint](/en/reference/midaz/create-an-account) # Creating an Account Type Source: https://docs.lerian.studio/en/midaz/console/creating-an-account-type Create a new Account Type in the Console to classify Accounts and enforce validation used by Accounting Routes when transactions are recorded. Account Types classify Accounts. Accounting Routes use these types to define which accounts an operation can use. Create them before Accounts if you enabled **Validate Account Type** for the Ledger. ## Before you start *** You need an existing Organization and Ledger. If you cannot see the Account Types menu, enable **Validate Account Type** in [Ledger Settings](/en/midaz/console/managing-ledgers-via-console#ledger-settings). Define Account Types from the accounting model, not from individual customer names. Examples: `customer`, `merchant`, `treasury`, `fee`, `settlement`, `revenue`, `expense`. ## Create the Account Type *** From the **Account Types** page, click the **New Account Type** button. The **New Account Type** form will open on the right side of the screen. Fill in the information and click the **Save** button. ## Field guide *** | Field | What to enter | Example | | ----------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | | Account Type Name | Clear operator-facing name for this Account Type. | `Customer` | | Description | Optional explanation of when operators should use this type. | `Accounts that represent end customers and can send or receive BRL payments.` | | Key Value | Stable value used to classify Accounts and route rules. Use a short lowercase value that will remain stable. | `customer` | Accounts and Accounting Routes reference the **Key Value**. Keep it short, lowercase, and aligned with your accounting model. You cannot change it after you create the Account Type. To use a different value, create a replacement Account Type, recreate dependent Accounts with the new type, and update dependent routes. ## What to do next *** After creating Account Types, create Accounts and select the correct type in [Creating an Account](/en/midaz/console/creating-an-account). [Create an Account Type endpoint](/en/reference/midaz/create-an-account-type) # Creating an Accounting Route Source: https://docs.lerian.studio/en/midaz/console/creating-an-accounting-route Create a new Accounting Route in the Console using the 3-step wizard to define transaction routes, operation routes, and accounting scenarios. You create an Accounting Route through a guided 3-step wizard. The wizard walks you through the transaction route, the operation routes with their accounting scenarios, and a final review. ## Before you start *** Create the Account Types and Accounts that the route validates. If you have not chosen the operation type, the validation type, and the accounting scenarios yet, read [Accounting rules](/en/midaz/console/mc-accounting) first. If you enable **Validate Routes** for the Ledger, every transaction must match an existing route. Do not enable route validation until the required Accounting Routes are ready. ## Step 1: Basic information *** From the **Accounting Routes** page, click **New Route**. The creation wizard opens on **Step 1 — Basic information**. Fill in the basic information: * **Title** (required): a descriptive name for this transaction route. You must fill in this field to continue. * **Description** (optional): a description of the route's purpose. If you leave it empty, the wizard submits and stores an empty description. Click **Next** to proceed to Step 2. ## Step 2: Operation Routes *** In this step, you add and configure the operation routes. Operation routes define how the ledger validates accounts and records accounting entries. A **Bidirectional** operation route acts as both source and destination. When you later create a transaction, the ledger matches a bidirectional route on **both** the source side and the destination side. A single route can then validate both ends of the flow. Click **Add Operation Route** to create your first operation route card. Configure the operation route: * **Title** (required): a name for this operation route. The operation route has a single required text field and no separate description field. You cannot advance to the next step until you fill it in. * **Operation Type**: `Source`, `Destination`, or `Bidirectional`. * **Validation Type**: Optionally configure `Account Type` or `@Alias` to validate accounts. An Operation Route can omit the account validation rule. * **Account Type or Alias**: the selected Account Type values or the specific account alias. *(Optional)* Configure **Accounting Scenarios** to define how the ledger records entries. Toggle on the scenarios you need: Standard one-step transaction. Define the **Account Code** and **Description** for: * **Debit** — The account code debited on execution * **Credit** — The account code credited on execution For Direct Transactions, which sides appear depends on the operation type. Source routes show debit only, destination routes show credit only, and bidirectional routes show both. Other scenarios may differ — see the Two-Step Transaction tab for details. Transactions with a hold-then-commit cycle. Configure entries for each phase: * **Hold (Value Reserve)** — Entries that record reserved funds * **Commit (Reservation Confirmation)** — Entries that confirm the reservation * **Cancel (Reservation Cancellation)** — Entries that cancel the reservation. Fill these fields too: the ledger validates Hold, Commit, and Cancel as an atomic group and rejects the route when the Cancel mapping is missing. The available entry fields vary by operation type: * **Source** — Hold and Cancel show both debit and credit fields. Commit shows debit only. * **Destination** — Only the Commit phase appears, with credit only. Hold and Cancel are not available. * **Bidirectional** — All phases show both debit and credit fields. Entries that record the reversal of a completed transaction. Define the debit and credit account codes and descriptions for the reversal entries. The Reversal scenario is only available for **Bidirectional** operation routes. Entries that apply when a debit exceeds the account's available funds and draws on an overdraft line. Define the debit and credit account codes and descriptions for the overdraft draw and later repayment. Overdraft is available for **Source**, **Destination**, and **Bidirectional** operation routes. When you enable Overdraft, the wizard also enables the Direct scenario, because an overdraft draw settles through a direct entry. Entries that record blocking and unblocking funds on an account. Configure two independent sections, **Block** and **Unblock**. Each section is all-or-nothing: leave it empty to use the default entries, or fill in both the account code and the description for every visible side. Block is available for all operation types. Source routes show debit entries only, destination routes show credit entries only, and bidirectional routes show both. *(Optional)* Click **Add Operation Route** again to add more operation routes to this Accounting Route. You must add at least one source and one destination operation route, or at least one bidirectional route. The wizard will not proceed without a valid route configuration. Click **Next** to proceed to Step 3. ## Step 3: Review *** Review the complete configuration: * Transaction route title and description * Each operation route with its type, validation rules, and accounting entries * Accounting scenarios with debit and credit entries per transaction type To adjust anything, click **Previous** to go back. Click **Create Transaction Route** to create the Accounting Route. ## Field guide *** ### Basic information | Field | What to enter | Example | | ----------- | --------------------------------------------------------------------- | -------------------------------------------------- | | Route Title | Required route name. Keep it short and specific to the business flow. | `Pix Transfer Route` | | Description | Optional explanation of what the route validates or records. | `Handles Pix transfers between customer accounts.` | ### Operation route | Field | What to enter | Example | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | Title | Required name for this operation route card. Must be filled before advancing; there is no separate description field. | `Checking account validation for Pix` | | Type | Accounting side this operation route applies to: `Source`, `Destination`, or `Bidirectional`. This cannot be changed after creation. | `Bidirectional` | | Validation Type | Optional account-validation rule. Use `Account Type` for account classes or `@Alias` for a specific account. An Operation Route can omit this rule. | `Account Type` | | Account Type or Alias | Account Types or aliases accepted by this operation route. | `customer` | | Accounting Scenarios | Transaction scenarios this route should record: Direct, Two-Step, Reversal, Overdraft, or Block. | `Direct Transaction` | | Account Code | Accounting code used in the debit or credit entry for the selected scenario. | `1.1.1.001` | | Entry Description | Entry-level description shown in the scenario configuration. | `Customer checking - outbound` | ## Quick example *** **Transaction Route** | Field | Value | | ----------- | --------------------------------------------------------- | | Title | `Pix Transfer Route` | | Description | `Handles Pix instant transfers between checking accounts` | **Operation Route 1 — Bidirectional** | Field | Value | | --------------- | ------------------------------------- | | Title | `Checking account validation for Pix` | | Operation Type | `Bidirectional` | | Validation Type | `Account Type` | | Account Types | `checking` | **Direct Transaction scenario** | Side | Account Code | Description | | ------ | ------------ | ------------------------------ | | Debit | `1.1.1.001` | `Customer checking — outbound` | | Credit | `1.1.1.002` | `Customer checking — inbound` | **Two-Step Transaction — Hold** | Side | Account Code | Description | | ------ | ------------ | ----------------------------- | | Debit | `1.1.1.001` | `Customer checking — reserve` | | Credit | `2.1.1.001` | `Pending settlement — hold` | **Two-Step Transaction — Commit** | Side | Account Code | Description | | ------ | ------------ | ------------------------------ | | Debit | `2.1.1.001` | `Pending settlement — release` | | Credit | `1.1.1.002` | `Customer checking — settled` | ## What to do next *** After you create the route, create a transaction that matches the same account types, aliases, and scenario in [Creating a Transaction](/en/midaz/console/creating-a-transaction). Create the transaction-level route via API. Create operation-level routing rules via API. # Creating an Asset Source: https://docs.lerian.studio/en/midaz/console/creating-an-asset Create a new Asset in the Console — currency, crypto, commodity, or custom — and set the code that Accounts and Transactions use. An Asset defines the unit of value that Accounts and Transactions use inside a Ledger. Create Assets before you create Accounts, because every Account uses one Asset. ## Before you start *** You need an existing Organization and Ledger. Decide whether the Asset is a currency, crypto asset, commodity, or another custom unit of value. Choose Asset codes carefully. Accounts and Transactions depend on them. ## Create the Asset *** From the **Assets** page, click the **New Asset** button. The **New Asset** form opens on the right side of the screen. Select the **Type** of Asset. * *Crypto*: for cryptocurrencies such as Bitcoin, Ethereum, and Ripple. * *Commodity*: for tradable raw materials such as gold, natural gas, soybeans, and corn. * *Currency*: for currencies such as Brazilian Real, US Dollar, and Euro. * *Others*: for any other type of Asset. You **cannot** reuse an Asset name or code in the same Ledger. Both the name and the code must be **unique**. Specify the **Asset Name**. Specify the **Code** for the Asset. Use uppercase letters only. * If the Asset is a currency, select its code from the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) list. \[Optional] Select the **Metadata** tab to add custom metadata. After you fill in the fields, click the **Save** button. The Asset appears in the list of available Assets for your Ledger. Saving an Asset also provisions a system-managed External Account when one does not already exist for that Asset code. Its canonical alias is `@external/`. The fields in the Metadata tab are open. There are no predefined keys or values. Use consistent key names across your Organization, such as `country`, `settlementNetwork`, or `regulatoryClass`, so you can filter and audit Assets later. ## Field guide *** | Field | What to enter | Example | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | Type | Asset category. Use `Currency` for fiat money, `Crypto` for crypto assets, `Commodity` for tradable commodities, or `Others` for custom units. | `Currency` | | Asset Name | Human-readable Asset name shown to operators. | `Brazilian Real` | | Code | Asset code in uppercase letters. For currencies, use the ISO 4217 code. | `BRL` | | Metadata Key | Optional classification or integration field name. | `settlementNetwork` | | Metadata Value | Value for the metadata key. | `pix` | ## What to do next *** After creating the Asset, create Accounts that use it in [Creating an Account](/en/midaz/console/creating-an-account). [Create an Asset endpoint](/en/reference/midaz/create-an-asset) # Creating an Organization Source: https://docs.lerian.studio/en/midaz/console/creating-an-organization Create a new Organization in the Midaz Module to set the business boundary that will own your Ledgers, Assets, Accounts, and Transactions. An Organization is a container for Midaz operations. It can be a top-level Organization or a child of another Organization. Create it before you create Ledgers, Assets, Accounts, or Transactions. ## Before you start *** Decide which business boundary this Organization represents. In most setups, it maps to the company, business unit, or regulated entity that owns one or more Ledgers. If you set up Midaz for the first time, follow the [Setup path](/en/midaz/console/midaz-console-setup-path) before you create downstream entities. ## Create the Organization *** Go to the **Organizations** page. Click the **New Organization** button. The **New Organization** page opens. Fill in the required details about the Organization. * **Optional**: To include metadata, expand the **Metadata** section. The section then shows the **Key** and **Value** fields. * To add more metadata entries, click the plus (+) sign next to the first row. See the [Field guide](#field-guide) section for details about each field. After you enter all the details, click **Save**. Your new Organization appears on the Organizations page. You can now manage and configure it. ## Field guide *** | Field | What to enter | Example | | ---------- | --------------------------------------------------------------------------------- | ------------------------ | | Legal Name | Official registered name. Use a name operators will recognize later. | `Lerian Payments Brazil` | | Trade Name | Commercial or customer-facing name. | `Lerian` | | Document | Official tax ID or registration number. Enter the identification document number. | `12345678000190` | | Address | Street name, number, and main address line. | `Avenida Paulista, 1234` | | Complement | Extra address details, such as suite, floor, or apartment. | `CJ 123` | | Country | Two-letter ISO 3166-1 alpha-2 country code where the Organization is registered. | `BR` | | State | State, province, or region. | `São Paulo` | | City | City where the Organization is registered. | `São Paulo` | | ZIP Code | Postal code, up to 20 characters. | `04696040` | ### Metadata Metadata is optional key-value data. Use it for internal classification, integration references, or reporting. | Field | What to enter | Example | | ----- | ---------------------------------- | ------------- | | Key | The metadata field name. | `costCenter` | | Value | The value for that metadata field. | `BR_11101997` | ## What to do next *** After you create the Organization, create its first Ledger in [Creating a new Ledger](/en/midaz/console/creating-a-new-ledger). [Create an Organization endpoint](/en/reference/midaz/create-an-organization) # Creating an Alias from the Holder Source: https://docs.lerian.studio/en/midaz/console/crm-alias-from-holder Create an Alias Account directly from the Holders page in the Console so the Holder ID is preset and the new Alias is linked to its owner in one step. You can also create an Alias Account from the **Holders** page. To do so, follow these steps: On the **Holders** page, find the Holder that you want to link to the Alias Account. In the Actions column, click the three dots () and select **Create Alias Account**. The Console opens the Alias Accounts page and shows the New Alias Account form. Fill in the fields as required, then click **Create Alias Account**. The Console pre-fills the `Holder ID` field. You cannot change it. # Creating a Holder Source: https://docs.lerian.studio/en/midaz/console/crm-creating-a-holder Create a new Holder in the Midaz Module as an Individual or Corporate record to represent the person or company that owns one or more Accounts. A Holder represents the person or company behind one or more Accounts. Create the Holder before you create Alias Accounts. An Alias Account connects customer-facing account information to a ledger Account. ## Before you start *** Decide whether the Holder is an **Individual** or **Corporate** record. The type you choose controls which identity and registration fields the form shows. ## Create the Holder *** Go to the **Holders** page. Click the **New Holder** button. A window opens. Choose the Holder type you want to create. * If you select **Individual** (Natural Person), the **New Holder** page opens for you to enter the individual's information. * If you select **Corporate** (Legal Person), the **New Holder** page opens for you to enter the company's information. Follow the on-screen instructions to enter the required information. Click **Create Holder**. When the creation succeeds, you see a confirmation message. The list refreshes and shows the new Holder. ## Field guide *** ### Individual **Basic Information** | Field | What to enter | Example | | ----------- | ------------------------------------------------------------------------------- | ---------------- | | External ID | Optional ID from your CRM, core banking, KYC, or customer system. | `customer_123` | | Holder type | A read-only field that indicates the type of Holder. | `Natural Person` | | Name | The full name of the Holder. | `John Doe` | | Document | Tax ID, national ID, or company registration number expected by your operation. | `12345678900` | **Contact Information** | Field | What to enter | Example | | --------------- | -------------------------------------------------------------------------- | ----------------------- | | Primary Email | The main email used to contact the Holder. | `john.doe@example.com` | | Secondary Email | An alternative email used to contact the Holder. | `john.doe2@example.com` | | Mobile Phone | Contact phone number, including country or area code when required. | `+5511999999999` | | Other Phone | An alternative phone number, including country or area code when required. | `+551188888888` | **Addresses** | Field | What to enter | Example | | -------------- | -------------------------------------------------------------- | ----------------- | | Address Line 1 | Street name and building number. | `Rua Botucatu 10` | | Address Line 2 | Extra address details, for example, apartment or suite number. | `Casa B` | | Zip Code | Postal or ZIP code. | `04023060` | | City | City where the address is located. | `São Paulo` | | State | State, province, or region. | `SP` | | Country | Two-letter country code (ISO 3166-1 alpha-2). | `BR` | **Personal Details** | Field | What to enter | Example | | ------------- | --------------------------------------------------------- | ------------- | | Favorite Name | Nickname or preferred name. | `Johnny` | | Social Name | Social name, if different from the legal name. | `Johnny Doe` | | Gender | Gender identity of the person. | `Male` | | Birth Date | Date of birth in YYYY-MM-DD format. | `1990-01-01` | | Civil Status | Civil status — for example: Single, Married, or Divorced. | `Married` | | Nationality | Nationality, for example, Brazilian. | `Brazilian` | | Mother's Name | Mother's full name. | `Jane Doe` | | Father's Name | Father's full name. | `Richard Doe` | | Status | Current status of the individual in the system. | `Active` | **Metadata** | Field | What to enter | Example | | ----- | -------------------------------------------------- | ----------- | | Key | Optional classification or integration field name. | `kycStatus` | | Value | Value for the metadata key. | `approved` | ### Corporate **Basic Information** | Field | What to enter | Example | | ----------- | ------------------------------------------------------------------------------- | ---------------- | | External ID | Optional ID from your CRM, core banking, KYC, or customer system. | `company_123` | | Type | A read-only field that indicates the type of Holder. | `Legal Person` | | Name | The name of the company in the system. | `ACME` | | Document | Tax ID, national ID, or company registration number expected by your operation. | `59014899000183` | **Contact Information** | Field | What to enter | Example | | --------------- | -------------------------------------------------------------------------- | ------------------- | | Primary Email | The main email used to contact the company. | `acme@example.com` | | Secondary Email | An alternative email used to contact the Holder. | `acme2@example.com` | | Mobile Phone | Contact phone number, including country or area code when required. | `+5511999999999` | | Other Phone | An alternative phone number, including country or area code when required. | `+551188888888` | **Addresses** | Field | What to enter | Example | | -------------- | -------------------------------------------------------------- | --------------- | | Address Line 1 | Street name and building number. | `Rua Silva 100` | | Address Line 2 | Extra address details, for example, apartment or suite number. | `Cj 12` | | Zip Code | Postal or ZIP code. | `04013060` | | City | City where the address is located. | `São Paulo` | | State | State, province, or region. | `SP` | | Country | Two-letter country code (ISO 3166-1 alpha-2). | `BR` | **Company Details** | Field | What to enter | Example | | ----------------------- | ---------------------------------------------------------------- | -------------------------------- | | Trade Name | The registered business name of the company, if applicable. | `ACME BR` | | Activity | The type of business or activity the company engages in. | `Electronic devices development` | | Legal Type | The legal structure of the company. | `Limited Liability` | | Founding Date | The date when the company was established, in YYYY-MM-DD format. | `2025-01-01` | | Company Size | The size classification of the company. | `Medium` | | Status | Current status of the company in the system. | `Active` | | Representative Name | The legal representative's name. | `Jane Doe` | | Representative Document | The document number of the legal representative. | `43522116992` | | Representative Email | The email address of the legal representative. | `janedoe@corporate.com` | | Role | The role of the legal representative within the company. | `CFO` | **Metadata** | Field | What to enter | Example | | ----- | -------------------------------------------------- | ----------- | | Key | Optional classification or integration field name. | `kycStatus` | | Value | Value for the metadata key. | `approved` | ## What to do next *** After you create the Holder, create an Alias Account to connect the Holder to a Midaz ledger Account. See [Creating an Alias Account](/en/midaz/console/crm-creating-alias-account). [Create Holder endpoint](/en/reference/midaz/crm/create-holder) # Creating an Alias Account Source: https://docs.lerian.studio/en/midaz/console/crm-creating-alias-account Create a new Alias Account in the Midaz Module to link a Holder to a Midaz Account with banking, regulatory, and customer-facing account details. An Alias Account links a Holder to an underlying Midaz Account and stores customer-facing account identifiers separately from ledger balances. ## Before you start *** You need an existing Holder and an existing Midaz Account. Keep the Account ID available because this page connects Holder information to the ledger Account. ## Create the Alias Account *** Go to the **Alias Accounts** page and click the **New Alias Account** button. A window will open where you can specify the information about the account. Fill in all the necessary information and click **Create Alias Account**. If the creation is successful, you will receive a confirmation message, and the list will refresh to show the new Alias Account. ## Field guide *** **Alias Account Details** | Field | What to enter | Example | | ---------- | ------------------------------------------------------------------------------------ | -------------------------------------- | | Holder ID | The unique identifier of the Holder that owns or controls this Alias Account. | `019ed1d9-7694-7bba-a721-d8e53911ab08` | | Account ID | The unique identifier of the Account for which you want to create the Alias Account. | `019ed1eb-21f8-781c-a30f-f5fca470497d` | | Ledger | The name of the Ledger of which the account is part. | `Test Ledger` | **Banking Details** | Field | What to enter | Example | | ------------ | ------------------------------------------------------------------------------------------ | ------------------------ | | IBAN | International Bank Account Number (IBAN) tied to the account. | `US12345678901234567890` | | Bank ID | Bank identifier used to link the account to its institution. | `12345` | | Branch | Branch number or code where the account is held. | `0001` | | Account | Customer-facing account number or external account identifier. | `1234567` | | Type | ISO 20022 account type code (for example, CACC for a current account or SVGS for savings). | `CACC` | | Opening Date | Date when the account was opened. | `2010-01-01` | | Country Code | ISO 3166-1 alpha-2 country code where the bank is based. | `US` | **Regulatory** The related party fields below (Document, Name, Role, Start Date, and End Date) belong to a repeatable **Related Parties** group — click **Add related party** to add each related party entry. | Field | What to enter | Example | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | Participant Document | The document number of the participant institution (e.g., CNPJ for Brazilian financial institutions). Optional field. | `---` | | Document | Identification document of the related party (e.g., CPF or CNPJ). | `98765432100` | | Name | Full legal name of the related party. | `Jane Doe` | | Role | The type of relationship between the related party and the Alias Account. The select shows **Primary Holder**, **Legal Representative**, and **Responsible Party**. | `Legal Representative` | | Start Date | Start date of the relationship with the Alias Account. | `2024-01-01` | | End Date | End date of the relationship with the Alias Account. If not provided, the relationship is considered ongoing. | `2025-01-01` | ## What to do next *** After creating the Alias Account, use it to identify the customer account in operational flows, or return to [Managing Alias Accounts](/en/midaz/console/managing-crm-alias-accounts) to review the record. [Create Alias Account endpoint](/en/reference/midaz/crm/create-alias-account) # Deleting a Holder Source: https://docs.lerian.studio/en/midaz/console/crm-deleting-a-holder Delete a Holder from the Midaz Module only after the Holder has no linked Alias Accounts or Accounts, within your deployment's data-retention and compliance policies. Confirm your data-retention, compliance, and regulatory policies before you delete a Holder. In regulated environments, you may need to retain Holder records to meet legal, regulatory, audit, reporting, or compliance obligations. To delete a Holder, follow these steps: On the **Holders** page, find the Holder you want to delete. In the **Actions** column, click the three dots (). Select **Delete**. A confirmation dialog appears. Click **Confirm** to delete the Holder. ## Data deletion options *** **Regulated deployments** In environments under financial regulation (for example, BACEN, LGPD, or GDPR), mandatory retention periods can apply to Holder records tied to financial transactions. Before you hard delete in production, ask your Legal and Compliance team to review the action. When you delete a Holder in the Console, the Midaz Module performs a **soft delete**: it marks the Holder as deleted and keeps the record in the database. The record stays available for audit. A **hard delete** — permanently removing the record from the database — is available only through the API, using the `?hard_delete=true` query parameter on the delete Holder endpoint. This action is irreversible, so use it only when your retention policy allows it. Deletion fails while the Holder still has linked Alias Accounts or Accounts. Remove or unlink them before you delete the Holder. For more information about data security and deletion, see [CRM Data Security](/en/midaz/crm/crm-data-security). Keep Holder data accurate and current. Accurate Holder records support smooth operations and regulatory compliance. # Deleting an Alias Account Source: https://docs.lerian.studio/en/midaz/console/crm-deleting-alias-account Delete an Alias Account from the Midaz Module. Your deployment's data-retention and compliance policies can keep the underlying record. When you delete an Alias Account, the Midaz Module removes it from the active list. Your deployment's data-retention and compliance policies still apply. In a regulated environment, the underlying record can stay for audit, regulatory, or reporting purposes. Confirm your retention policy before you delete. To delete an Alias Account, follow these steps: From the **Alias Accounts** page, find the alias account to delete. In the Actions column, click the three dots (). Select **Delete**. A confirmation dialog opens. Click **Confirm** to delete the alias account. # Editing a Holder Source: https://docs.lerian.studio/en/midaz/console/crm-editing-a-holder Edit an existing Holder in the Midaz Module to update personal, corporate, contact, or address information and keep customer records accurate. To edit the information of a Holder, follow these steps: From the **Holders** page, find the Holder that you want to edit. Click the three dots () from the Actions column, and select **Edit**. The Holders form will open. Edit the information as needed and click **Update Holder**. The **Document** field is read-only when editing — it cannot be changed after the Holder is created. # Editing an Alias Account Source: https://docs.lerian.studio/en/midaz/console/crm-editing-alias-account Edit an existing Alias Account in the Midaz Module to update banking details, regulatory data, or metadata linked to the underlying Midaz Account. To edit the information of an Alias Account, follow these steps: From the **Alias Accounts** page, find the alias account that you want to edit. Click the three dots () from the Actions column, and select **Edit**. The Alias Account form will open. Edit the information as needed and click **Update Alias Account**. # Deleting a Ledger Source: https://docs.lerian.studio/en/midaz/console/deleting-a-ledger Delete a Ledger from your Organization in the Console when it is no longer used — data retention and compliance policies may keep audit records available. To delete a Ledger, follow these steps: Check your data retention and compliance policy before you delete a Ledger. The Console removes the Ledger from active listings, but keeps the underlying record. Your retention policy controls how long the record stays available for audit, regulatory, or reporting purposes. On the **Ledgers** page, find the Ledger you want to delete. Click the three-dot menu () in the **Actions** column. Select **Delete**. A confirmation dialog will appear. Click **Confirm** to delete the Ledger. Midaz rejects Ledger deletion when `ENV_NAME=production`. After you delete a Ledger, you can no longer use it. Its data may stay available for audit, regulatory, or reporting purposes, based on your retention policy. [Delete a Ledger endpoint](/en/reference/midaz/delete-a-ledger) # Deleting a Portfolio Source: https://docs.lerian.studio/en/midaz/console/deleting-a-portfolio Delete a Portfolio from the Console when its Accounts no longer need to be grouped together — subject to your deployment's data retention rules. Check your data retention policy before you delete a Portfolio. The Console removes it from active listings. Your deployment can keep the underlying record for audit, regulatory, or reporting purposes. To delete a Portfolio, follow these steps: On the **Portfolios** page, find the Portfolio to delete. In the **Actions** column, click the three-dot menu () and select **Delete**. A confirmation dialog opens. Click **Confirm** to delete the Portfolio. [Delete a Portfolio endpoint](/en/reference/midaz/delete-a-portfolio) # Deleting a Segment Source: https://docs.lerian.studio/en/midaz/console/deleting-a-segment Delete a Segment from the Console when it no longer classifies any Accounts — subject to your deployment's data retention and compliance policies. When you delete a Segment, the Console removes it from active listings. Deletion follows your deployment's data retention and compliance policies. In regulated environments, the underlying record remains for audit, regulatory, or reporting purposes. Confirm your retention policy first. To delete a Segment, follow these steps: On the **Segments** page, find the Segment to delete. In the **Actions** column, click the three dots (). Select **Delete**. A confirmation dialog opens. Click **Confirm** to delete the Segment. After you confirm, the Console removes the **Segment** from active listings. [Delete a Segment endpoint](/en/reference/midaz/delete-a-segment) # Deleting an Account Source: https://docs.lerian.studio/en/midaz/console/deleting-an-account Delete an Account from your Ledger in the Console when it is no longer used — External Accounts cannot be removed and retention policies still apply. When you delete an Account, Midaz removes it from active listings. Your deployment's data retention and compliance policies apply. In regulated environments, your deployment can keep the record for audit, regulatory, or reporting purposes. Review your retention policy before you delete an Account. To delete an Account, follow these steps: On the **Accounts** page, find the Account to delete. In the **Actions** column, click the three dots (), then select **Delete**. A confirmation dialog appears. Click **Confirm** to delete the Account. Midaz removes the **Account** from active listings. Your deployment's data retention and compliance policies still apply. Before deleting an Account, ensure every balance has `available` and `onHold` equal to zero and that no transaction is in progress. Otherwise, Midaz rejects the deletion. You **cannot directly** delete or modify External Accounts. Midaz creates them automatically with each Asset. Deleting an Asset soft-deletes active External Accounts with the same Asset code in the same Organization and Ledger. [Delete an Account endpoint](/en/reference/midaz/delete-an-account) # Deleting an Asset Source: https://docs.lerian.studio/en/midaz/console/deleting-an-asset Delete an Asset from your Ledger in the Console when it is no longer used — your deployment's data retention and compliance policies still apply. When you delete an Asset, Midaz removes it from active listings. Your deployment's data retention and compliance policies apply. In regulated environments, your deployment can keep the record for audit, regulatory, or reporting purposes. Review your retention policy before you delete an Asset. To delete an Asset, follow these steps: On the **Assets** page, find the Asset to delete. In the **Actions** column, click the three dots (), then select **Delete**. A confirmation dialog appears. Click **Confirm** to delete the Asset. Midaz removes the **Asset** from active listings. Your deployment's data retention and compliance policies still apply. Deleting an Asset soft-deletes all active External Accounts with the same Asset code in that Organization and Ledger. [Delete an Asset endpoint](/en/reference/midaz/delete-an-asset) # Deleting an Organization Source: https://docs.lerian.studio/en/midaz/console/deleting-an-organization Remove an Organization from the Midaz Module — subject to your deployment's data retention and compliance policies. To delete an Organization in **Midaz Module**, follow these steps: When you delete an Organization, it no longer appears in active listings. Your deployment's data retention and compliance policies govern the record after that. In regulated environments, the record remains for audit, regulatory, or reporting purposes. Confirm your retention policy before you delete. On the **Organizations** page, find the Organization you want to delete. In the **Actions** column, click the three-dot menu ( ). Then select **Delete**. A confirmation dialog will appear. Click **Confirm** to delete the Organization. Midaz rejects Organization deletion when `ENV_NAME=production`. After you delete the Organization, you cannot use it anymore. The record may remain on the platform for audit, regulatory, or reporting purposes, under your deployment's retention policies. [Delete an Organization endpoint](/en/reference/midaz/delete-an-organization) # Editing a Ledger Source: https://docs.lerian.studio/en/midaz/console/editing-a-ledger Edit a Ledger from the Console to update its name, metadata, or the Validate Account Type and Validate Routes settings without any redeployment. To view or edit the details of a Ledger, follow these steps: On the **Ledgers** page, locate the Ledger you want to view or update. Click the three dots () in the **Actions** column and select **Details**. A sidebar will open with multiple tabs: **Ledger Details**, **Metadata**, and **Settings**. Use the **Ledger Details** tab to edit the Ledger name, and the **Metadata** tab to manage custom key-value pairs. To configure validation rules for this Ledger, click the **Settings** tab. From there you can enable **Validate Account Type** and **Validate Routes** for this Ledger. Learn how to enable Validate Account Type and Validate Routes for this Ledger. [Update a Ledger endpoint](/en/reference/midaz/update-a-ledger) [Update Ledger Settings endpoint](/en/reference/midaz/update-ledger-settings) # Editing Account information Source: https://docs.lerian.studio/en/midaz/console/editing-account-information Edit an existing Account in the Console to update its name, metadata, or other details — External Accounts created with an Asset cannot be changed. To edit an Account, follow these steps: On the **Accounts** page, find the Account that you want to edit. In the **Actions** column, click the three dots () and select **Details**. The Console navigates to the account details page. Click the **Edit** button. The edit sheet opens with two tabs: **Account Details** and **Metadata**. Edit the information as needed and click **Save**. You **cannot** edit External Accounts. Midaz creates one automatically for each Asset. [Update an Account endpoint](/en/reference/midaz/update-an-account) # Editing an Account Type Source: https://docs.lerian.studio/en/midaz/console/editing-account-type Edit an existing Account Type from the Console to refine how Accounts are classified and how validation rules are applied across your Ledger. To edit the information of an Account Type, follow these steps: From the **Account Types** page, find the Account Type that you want to edit, click the three dots ( ) from the **Actions** column, and select **Details**. The **Edit** form will open on the right side of the screen. Edit the information as needed and click **Save**. You cannot change the **Key Value** when editing an Account Type — only the name, description, and metadata are editable. [Update an Account Type endpoint](/en/reference/midaz/update-an-account-type) # Editing an Accounting Route Source: https://docs.lerian.studio/en/midaz/console/editing-an-accounting-route Edit an existing Accounting Route in the Console to update its operation routes, validation rules, and accounting scenarios. You can edit an existing Accounting Route. The Console edit flow changes its operation routes. Go to the **Accounting Routes** page. Find the Accounting Route to edit. Click the three dots () in the **Actions** column. Select **Details**. A detail sheet opens on the right side of the screen. It shows the route configuration: all operation routes, their types, validation rules, and accounting entries. Click **Edit** at the bottom of the detail sheet. The edit form opens with the existing configuration already filled in. It uses the same multi-step layout as the creation wizard. Change the information as needed: * Edit existing operation routes (title, validation type, account types or alias, accounting scenarios) * Add new operation routes * Remove an operation route with the delete button on its card You cannot change the transaction route **Title** or **Description** in the Console edit flow — the edit wizard skips the basic-info step and submits operation-route changes only. You set the title and description at creation. To change them later, use the [Update Transaction Route](/en/reference/midaz/update-a-transaction-route) API. You cannot change the **Operation Type** (Source, Destination, or Bidirectional) of an existing operation route. To change the direction, delete the operation route and create a new one. Click **Update Transaction Route** to apply your changes. ## Viewing route details *** To view an Accounting Route without editing it: Go to the **Accounting Routes** page. Click the three dots () in the **Actions** column. Select **Details**. A detail sheet opens on the right side of the screen. It shows the route configuration: all operation routes, their types, validation rules, and accounting entries. If an operation route has a **Code** assigned (set via API), the detail sheet also shows it. You can also expand a route's card in the Accounting Routes list. The card shows a summary of its operation routes in the card-accordion view. ## Removing individual operation routes *** You can remove a single operation route from an Accounting Route without affecting the rest of the configuration: Open the Accounting Route in **Edit** mode (via **Details** → **Edit**). Find the operation route card to remove. Click the delete button. Confirm the deletion in the dialog that appears. When editing, the resulting Accounting Route must contain 2–100 Operation Routes. It must include a Source and a Destination route, or at least one Bidirectional route. If you remove a route and break this rule, you must correct it before you save. Update the transaction-level route via API. Update operation-level routing rules via API. # Editing an Organization Source: https://docs.lerian.studio/en/midaz/console/editing-an-organization Edit an Organization in the Midaz Module through the icon shortcut or the Organizations page to update legal name, address, metadata, or avatar. There are two ways of editing the details of an Organization in **Midaz Module**. ## Editing via icon *** To edit the Organization via the icon, follow these steps: Select your Organization icon from the top of the navigation bar on the left side of the screen. Select **Edit** from the displayed window. You can update the following supported Organization fields: * **Legal and Trade names** * **Address information** * **Parent Organization** * **Metadata** After making your changes, click **Save** to persist the supported changes. ## Editing via the Organization page *** Go to the **Organizations** page and locate the Organization you want to update. Click the three-dot menu ( ) in the **Actions** column, then select **Edit**. The **Organization Details** page will open, allowing you to edit the supported Organization fields. After making your changes, click **Save** to apply them. [Update an Organization endpoint](/en/reference/midaz/update-an-organization) # Editing an Asset Source: https://docs.lerian.studio/en/midaz/console/editing-asset-information Edit an existing Asset from the Console to update its name or metadata without affecting balances — the Asset type itself cannot be changed later. To edit the information of an Asset, follow these steps: From the **Assets** page, find the Asset that you want to edit, click the three dots () from the **Actions** column, and select **Details**. The **Edit Asset** form will open on the right side of the screen. Edit the information as needed and click **Save**. * You cannot edit the type or the **Code** of the Asset after creation — only the name, status, and metadata are editable. [Update an Asset endpoint](/en/reference/midaz/update-an-asset) # Editing Portfolio information Source: https://docs.lerian.studio/en/midaz/console/editing-portfolio-information Edit an existing Portfolio in the Console to update its name, metadata, or details and keep your Account groupings accurate over time. To edit the information of a Portfolio, follow these steps: From the **Portfolio** page, find the Portfolio that you want to edit, click the three dots () from the **Actions** column, and select **Details** . The **Portfolio Details** form will open on the right side of the screen. Edit the information as needed and click **Save**. [Update a Portfolio endpoint](/en/reference/midaz/update-a-portfolio) # Editing Segment information Source: https://docs.lerian.studio/en/midaz/console/editing-segment-information Edit an existing Segment in the Console to update its name or metadata and refine how Accounts are classified for reporting and business rules. To edit the information of a Segment, follow these steps: From the **Segments** page, find the Segment that you want to edit, click the three dots () from the **Actions** column, and select **Details**. The **Segment Details** form will open on the right side of the screen. Edit the information as needed and click **Save**. [Update a Segment endpoint](/en/reference/midaz/update-a-segment) # Managing Account Types Source: https://docs.lerian.studio/en/midaz/console/managing-account-types Manage Account Types from the Midaz Module — enable Validate Account Type on a Ledger, then create and edit the categories that classify Accounts. You use the Midaz Module to manage Account Types. Account Types classify accounts by their role in your Organization’s financial structure. When you enable validation on a Ledger, each new account must use a registered Account Type. For more details, refer to the **Account Types** section on the [Accounts](/en/midaz/accounts#account-types) page. ### Enabling Account Type validation You enable Account Type validation for a specific Ledger from the **Lerian Console**. You do not edit environment configuration files. On the **Ledgers** page, click the three dots () next to the Ledger. Select **Details**. In the sidebar, click the **Settings** tab. Toggle on the **Validate Account Type** switch. The change takes effect immediately for the selected Ledger. Enabling Account Type validation does not make the Type field mandatory — it is always required when you create an account. Instead, validation changes the Type field from free text to a dropdown restricted to the registered Account Types. ## Accessing the Account Types page *** To open the **Account Types** page, select the **Account Types** option in the **Foundation** section of the left sidebar menu. The **Account Types** menu item appears only when **Validate Account Type** is enabled for the active Ledger. If validation is off, the menu item is hidden. The **Account Types** page lists all Account Types you created for the current Ledger. **Account Type validation disabled** — If **Validate Account Type** is off, a banner appears at the top of this page. You can view and edit the registered account types. The Ledger does not validate these rules when you create accounts. To enforce the rules, turn on **Validate Account Type** in the Ledger settings. ### Available actions From the **Account Types** page, you can: # Managing Accounting Routes Source: https://docs.lerian.studio/en/midaz/console/managing-accounting-routes Manage Accounting Routes in the Console — create, edit, or delete Source, Destination, and Bidirectional operation routes and accounting scenarios. Use the **Midaz Module** to manage **Accounting Routes**, the configuration for transaction routing in Midaz. A guided wizard walks you through three steps: define the transaction route, add the operation routes, and review the setup. An Accounting Route defines the full routing logic for a transaction. It has three parts: * The transaction-level metadata — the title and description. * The operation routes — they set which accounts participate and in which direction. * The accounting scenarios — they control how the ledger records entries for each transaction type. Start here if you need to decide whether to use Source, Destination, Bidirectional, Account Type validation, alias validation, Direct, Two-Step, or Reversal scenarios. For technical details on the underlying entities, refer to the [Accounting Routes entities](/en/midaz/transaction-routing-entities) page. ## Key concepts *** ### Operation route types Each operation route within an Accounting Route has one of three types: * **Source** — Defines validation rules for accounts where funds originate (debit side) * **Destination** — Defines validation rules for accounts that receive funds (credit side) * **Bidirectional** — Applies to both sides of the transaction as both source and destination A valid Accounting Route requires at least one source and one destination operation route, or at least one bidirectional route. A bidirectional route works as both source and destination. Midaz matches it on **both** sides when you create a transaction. It appears in both the source and the destination route lists, so a single route can cover both ends of a flow. ### Validation types Each operation route validates accounts by one of two methods: * **Account Type** — Validates against one or more account type categories (e.g., `checking`, `savings`). Use when any account of a certain type is valid. * **@Alias** — Validates against a specific account alias (e.g., `@treasury_main`). Use when you need to target an exact account. ### Accounting scenarios Accounting scenarios define how the ledger records debit and credit entries for each operation route. You can configure entries for five transaction types: * **Direct Transaction** — Standard one-step transaction. Define the debit and credit account codes and descriptions for immediate settlement. * **Two-Step Transaction** — Transactions that go through a hold-then-commit cycle. Configure entries for: * **Hold (Value Reserve)** — Entries recorded when the ledger reserves funds * **Commit (Reservation Confirmation)** — Entries recorded when you confirm the reservation * **Cancel (Reservation Cancellation)** — Entries recorded when you cancel the reservation * **Reversal** — Entries recorded when you reverse a completed transaction * **Overdraft** — Entries recorded when a debit exceeds the account's available funds and draws on an overdraft line. Available for Source and Bidirectional operation routes, and requires both debit and credit entries on every supported route type. When you enable it, Midaz also enables the Direct scenario. * **Block/Unblock** — Entries recorded when you hold and release funds on the account's balance. The scenario tab label is **Block**. Available for all operation route types. Each scenario entry includes an **Account Code** and a **Description** for both the debit and credit sides. The available entry sides depend on the operation route type and the scenario phase. The general rule is: * **Source** — Debit entries in Direct, Hold, Commit, and Cancel. Credit entries only in Hold and Cancel. * **Destination** — Credit entries only, in Direct and Commit. Hold and Cancel are not available. * **Bidirectional** — Both debit and credit entries in all phases, including Reversal. * **Block/Unblock** — Available for all route types. It holds and releases funds on the account's balance. ## Accessing the Accounting Routes page *** The **Accounting Routes** menu appears only when you enable the **Validate Routes** setting for the current Ledger. To enable it, open the Ledger details, go to the **Settings** tab, and toggle the **Validate Routes** switch. **Accounting Routes validation disabled** — If you open this page by URL while **Validate Routes** is off, a banner appears at the top of the screen. The Ledger does not yet process the accounting routes registered here. Entries follow these rules only after you enable **Validate Routes** in the Ledger settings. You can still view and edit routes, but they have no effect until you enable validation. To open the **Accounting Routes** page, select **Accounting Routes** in the **Accounting** section of the left sidebar menu. The **Accounting Routes** page displays your routes for the current Ledger as a **card-accordion list**. Each transaction route is a collapsible card. The card shows its title, transaction route ID, and an operation-count badge. Expand a card to view its operation routes. Each one shows its ID, title, code, and operation type. The Console color-codes operation types: **source** in red, **destination** in green, and **bidirectional** in gray. A route with no operation routes appears as a static (non-expandable) card. ### Available actions From the **Accounting Routes** page, you can: Define a new transaction route with operation routes and accounting scenarios. Modify an existing accounting route and its operation routes. ## Common use cases *** Create an Accounting Route with a bidirectional operation route that validates `checking` and `savings` account types. Enable Direct Transaction scenarios with the appropriate chart of accounts codes for Pix settlement entries. Create an Accounting Route with separate source and destination routes. Enable Two-Step Transaction scenarios to record the hold (reservation), commit (confirmation), and optional cancel (cancellation) entries independently. Create an Accounting Route with a bidirectional operation route that uses alias validation (`@treasury_main`). Enable both Direct Transaction and Reversal scenarios to handle standard movements and their reversals. Reversal entries require a bidirectional route. ## Best practices *** Name your Accounting Routes clearly to indicate the type of transaction they handle (e.g., `Pix Transfer Route` instead of `Route 1`). Use **Bidirectional** when the same validation rules apply to both sides. Use separate **Source** and **Destination** routes when each side has different account requirements. Define your chart of accounts entries during route creation. This ensures that transactions processed through the route produce correct accounting records from the start. The wizard includes a Review step. Use it to verify all operation routes, validation rules, and accounting entries before you create the route. # Managing Accounts Source: https://docs.lerian.studio/en/midaz/console/managing-accounts Manage Accounts from the Lerian Console — create, edit, and delete internal Accounts, view balances, and classify them by type, portfolio, or segment. Use **Midaz Module** to manage **Accounts** associated with your Ledger. Accounts are the fundamental units that hold balances and participate in transactions within your financial system. ## What are Accounts? *** Accounts represent individual balance holders within your ledger structure. Each account: * **Holds balances** in a specific asset (currency or commodity) * **Participates in transactions** as sources or destinations of funds * **Belongs to an entity** such as a customer, business unit, or external party * **Supports portfolios, segments, and account types** for organization and reporting * **Tracks history** with complete audit trails of all movements Accounts can be classified as: * **Internal Accounts** - Accounts you create and manage for your organization's operations * **External Accounts** - Midaz creates these automatically for external assets or liabilities For technical details, refer to the [Accounts](/en/midaz/accounts) page. ## Key concepts *** * **Account Types** - Classify accounts according to your organization's financial structure (e.g., `checking`, `savings`, `revenue`). Enable validation to enforce account type rules. * **Account Alias** - A unique, human-readable identifier (e.g., `@customer_john_doe`) that makes it easier to reference accounts in transactions and queries. * **Portfolios** - Group related accounts together for reporting and management purposes. Useful for tracking customer portfolios or business divisions. * **Segments** - Organize accounts into hierarchical structures for complex organizational needs like cost centers or departments. ## Accessing the Accounts page *** To open the **Accounts** page, select the **Accounts** option in the **Accounts** section of the left sidebar menu. The **Accounts** page displays a table with all Accounts in the current Ledger, including: * **Account Name** - The account's display name * **ID** - The account's unique identifier * **Account Alias** - The unique, human-readable alias (if set) * **Assets** - The asset (currency or commodity) the account holds * **Metadata** - The number of custom key-value pairs on the account * **Portfolio** - The portfolio the account belongs to (if any) * **Actions** - The three-dots menu with the available actions — external accounts show a padlock icon instead The padlock icon marks an account you **cannot** edit or delete. Midaz creates these **external accounts** automatically when you create an Asset. ### Available actions From the **Accounts** page, you can: Add internal accounts for customers, departments, or business operations Modify account details, metadata, and organizational settings Delete accounts you no longer need (internal accounts only) ## Account organization strategies *** Create separate accounts for each customer, organized by account type and asset code. **Example**: * Customer checking account: `@customer_123_checking` (Type: `checking`, Asset: `USD`) * Customer savings account: `@customer_123_savings` (Type: `savings`, Asset: `USD`) * Group both under a Portfolio: `Customer 123 Portfolio` Create dedicated treasury accounts to manage liquidity and operational funds. **Example**: * Main treasury: `@treasury_main` (Type: `treasury`, Asset: `USD`) * Reserve fund: `@treasury_reserve` (Type: `reserve`, Asset: `USD`) * Float account: `@float_operations` (Type: `float`, Asset: `USD`) Create separate accounts for each currency your organization handles. **Example**: * USD operations: `@ops_usd` (Type: `operations`, Asset: `USD`) * EUR operations: `@ops_eur` (Type: `operations`, Asset: `EUR`) * BRL operations: `@ops_brl` (Type: `operations`, Asset: `BRL`) Use segments and account types to track departmental expenses. **Example**: * Engineering expenses: `@eng_expenses` (Type: `expense`, Segment: `Engineering`) * Marketing expenses: `@mkt_expenses` (Type: `expense`, Segment: `Marketing`) * Consolidate with parent segment for organization-wide reporting ## Account type validation *** Account type validation makes each account follow your organization's financial structure. ### Enabling Account type validation Account type validation is a per-ledger setting. To turn it on in the Console: on the **Ledgers** page, open the Ledger's **Details**, click the **Settings** tab, and toggle on **Validate Account Type**. As a programmatic alternative, send a `PATCH` request to `/v1/organizations/{organization_id}/ledgers/{ledger_id}/settings` with this body: ```json theme={null} { "accounting": { "validateAccountType": true } } ``` When you enable account type validation, each new account must have a registered account type. Update existing accounts that have no type. For more information about managing account types, see [Managing Account Types](/en/midaz/console/managing-account-types). ## Best practices *** Name accounts clearly and use aliases for easy reference (e.g., `@customer_john_checking` instead of UUID references). Enable account type validation to enforce your chart-of-accounts structure and prevent misclassification. Group related accounts into portfolios for better reporting and management, especially for customer relationships. Use consistent asset code conventions (ISO 4217 for currencies) to simplify multi-currency operations. Use metadata fields to store additional context like customer IDs, department codes, or integration references. Accounts are the foundation of your ledger. A clear account structure improves your financial operations and reporting. # Managing Assets Source: https://docs.lerian.studio/en/midaz/console/managing-assets Manage the Assets registered in your Ledger from the Midaz Module — create, edit, and delete the currencies, tokens, or commodities you use. You can manage the Assets associated with your Ledger directly from **Midaz Module**. ## Accessing the Asset page *** To open the **Assets** page, select the **Assets** option in the **Foundation** section of the left sidebar menu. The **Assets** page will display a list of **Assets** created for the current Ledger. ### Available actions From the **Assets** page, you can: # Managing Balances Source: https://docs.lerian.studio/en/midaz/console/managing-balances Manage Account Balances from the Lerian Console — list, read, and create balances, and control sending and receiving permissions without leaving the interface. Use the **Midaz Module** to manage the **Balances** in your Accounts. A balance tracks one asset in one account. It records the available amount and the on-hold amount. ## What you can do * **List balances** for an account to see the available and on-hold amounts per asset. * **Read a balance** to inspect its current state and metadata. * **Create an additional balance** for an account under a new balance key. The balance inherits the account's asset. * **Update a balance** to toggle whether it can send or receive funds (**Allow Sending** and **Allow Receiving**). Overdraft limits are managed through the update-balance API, not the Console. The Console UI does not support balance history or balance deletion. This page does not cover them. # Managing Alias Accounts Source: https://docs.lerian.studio/en/midaz/console/managing-crm-alias-accounts Manage Alias Accounts in the Midaz Module — create, edit, and delete customer-facing account records that link Holders to their underlying Accounts. Use **Midaz Module** to manage **Alias Accounts**. Alias Accounts create the link between Holders and Midaz ledger accounts, adding business context like bank account numbers and financial institution details. ## What are Alias Accounts? *** An **Alias Account** connects a Holder (individual or company) to a specific Midaz ledger account. In Midaz Module, each alias account stores: * **Account identification** - Bank ID, branch, account, and type * **Holder reference** - Link to the associated holder (Natural or Legal Person) * **Ledger account link** - Connection to the underlying Midaz account * **Banking details** - Opening date, IBAN, country code, and the Participant Document under Regulatory * **Custom metadata** - Additional fields for integration and business-specific needs Alias Accounts enable you to maintain customer-facing account information separately from the transactional ledger, providing flexibility for multi-bank scenarios and external system integration. For technical details about the underlying CRM capabilities, refer to the [CRM Overview](/en/midaz/crm/crm-overview) page. ## Accessing the Alias Accounts page *** To open the **Alias Accounts** page, select the **Alias** option from the Accounts section of the left-side menu. The **Alias Accounts** page displays a table listing all Alias Accounts in your system, including: * **Holder Name** - The associated holder name * **Holder ID** - The identifier of the associated holder * **Account ID** - The identifier of the linked ledger account * **Ledger** - The ledger the linked account belongs to * **Type** - Classification of the account (checking, savings, etc.) * **Actions** - The actions available for each alias account If you do not have any Alias Accounts, the list will appear empty with a prompt to create your first alias account. ### Available actions From the **Alias Accounts** page, you can: Link a holder to a Midaz ledger account with bank details Update account information and financial details Remove the link between a holder and ledger account ## Common use cases *** When a customer opens an account: 1. Create a Holder with customer information (Natural or Legal Person) 2. Create an Alias Account linking the holder to a Midaz ledger account 3. Add banking details (bank ID, branch, account, type) 4. The customer can now transact using their familiar bank account number When customers have accounts at multiple institutions: * Create multiple Alias Accounts for the same Holder * Each alias account references a different Midaz ledger account * Track different account numbers and bank relationships * Maintain clear separation between institutional relationships When integrating with external banking systems: * Use Alias Accounts to map external account numbers to internal ledger accounts * Store regulatory identifiers such as the Participant Document * Maintain metadata for integration-specific fields * Synchronize status changes between systems ## Best practices *** Always create the Holder record first, then create Alias Accounts to link them to ledger accounts. This ensures proper data relationships. Follow your institution's account numbering standards when creating alias accounts to avoid confusion and integration issues. Ensure Bank IDs and other banking details are correct for proper payment routing and regulatory compliance. Use metadata to store additional context about account relationships, especially in multi-bank or integration scenarios. Update alias account status to reflect changes in the account relationship (active, inactive, closed). # Managing Holders Source: https://docs.lerian.studio/en/midaz/console/managing-crm-holders Manage Holders in the Midaz Module — create, edit, and delete individuals or companies with their identity, contact, and address information. Use the **Midaz Module** to manage **Holders**. Holders represent the people or organizations behind your accounts. In the console, you create, view, edit, and manage their information. ## What are Holders? *** A **Holder** represents an individual (Natural Person) or a company (Legal Person) linked to your Midaz accounts. Each holder stores: * **Identity information** - Name, document number (CPF/CNPJ), and an optional free-text status * **Contact details** - Email addresses and phone numbers for communication * **Address information** - Primary and additional addresses with complete location data * **Person-specific data** - Personal details for individuals or business information for companies * **Custom metadata** - Additional fields for integration and business-specific needs You manage Holders in the CRM area of the Midaz Module. Holders connect to ledger accounts through Alias Accounts. This keeps customer data separate from transactional operations. For technical details about the underlying CRM capabilities, refer to the [CRM Overview](/en/midaz/crm/crm-overview) page. ## Accessing the Holders page *** To open the **Holders** page, select the **Holders** option from the Accounts section of the left-side menu. The **Holders** page shows a table with all Holders in your system. ### Available actions From the **Holders** page, you can: Add new individuals or companies as Holders Update holder information, contact details, and status Remove holders (soft delete). Hard delete is available only through the API (`?hard_delete=true`) ## Understanding Holder types *** When you create a Holder, you select the holder type. The type determines which fields you can manage: ### Natural Person (Individual) For individual customers, the console shows fields for: * **Basic information**: Full name, document (CPF), contact details * **Personal details**: Favorite name, social name, gender, birth date * **Family information**: Civil status, mother's name, father's name * **Demographics**: Nationality ### Legal Person (Company) For corporate customers, the console shows fields for: * **Basic information**: Company name, document (CNPJ), contact details * **Business details**: Trade name, company activity, founding date * **Structure**: Company size, company type, business status * **Legal representation**: Representative name, document, role, contact ## Managing Holder status *** **Status** is an optional free-text field on the holder form. Use it to record any classification your business needs, such as `active` or `under-review`. To change the status, edit the holder. The holders list shows the **Name**, **ID**, **Type**, and **Primary E-mail** columns; open a holder to see its status. ## Common use cases *** To onboard individual customers in the console: 1. Create a Natural Person holder with personal details 2. Add primary address and contact information 3. Link to checking/savings accounts via Alias Accounts 4. Use metadata for customer-specific attributes (loyalty tier, risk profile, etc.) To manage corporate accounts in the console: 1. Create a Legal Person holder with company information 2. Add legal representative details for compliance 3. Include business addresses and contact points 4. Link to corporate treasury and operational accounts 5. Track company size and activity sector for reporting Use the External ID field in the console to keep systems in sync: * Map each holder to your existing CRM using External ID * Keep document numbers consistent across systems * Use metadata for integration-specific fields * Update status in the console to reflect external system changes ## Best practices *** Select Natural Person for individuals and Legal Person for companies. You cannot change the type later. The type sets which fields the console forms show. Document numbers (CPF, CNPJ) must be unique and correctly formatted. The document field is required, but the console does not validate its format — verify the numbers yourself for regulatory compliance. Update email addresses and phone numbers in the console regularly. This keeps customer communication effective. When you create a holder, fill in the External ID field. It maps the holder to your existing customer database. Use the Metadata step in the holder form to store extra information. You do not change the core structure. If you use the optional status field, define a consistent set of values for your team and update it to reflect customer relationship changes before deletion. # Managing Ledgers Source: https://docs.lerian.studio/en/midaz/console/managing-ledgers-via-console Manage Ledgers in your Organization from the Midaz Module — create, edit, and delete them, and enable Validate Account Type or Validate Routes. A Ledger groups the Accounts and Transactions in your Organization. You manage these Ledgers from the **Midaz Module**. ## Current Ledger *** The name of the current Ledger appears in the header of the home page (*Figure 1*). Home page header section displaying the name of the currently selected 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 every Ledger in your Organization. The current Ledger appears selected. ### Available actions From the **Ledgers** page, you can: ## Ledger settings *** Each Ledger has a **Settings** tab where you can enable validation rules for Accounts and Transactions. On the **Ledgers** page, find the Ledger you want to configure. Click the three dots () in the **Actions** column. Select **Details**. In the ledger details sheet, click the **Settings** tab. Enable or disable **Validate Account Type** and **Validate Routes** for this Ledger. ### Available settings | Setting | What it controls | When to enable | | --------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------- | | Validate Account Type | Requires new Accounts to use an Account Type registered for this Ledger. | Enable when Accounts must follow a controlled classification model. | | Validate Routes | Requires Transactions to match configured Accounting Routes. | Enable after your Accounting Routes are planned or configured. | Do not enable **Validate Routes** before your Accounting Routes exist. A Transaction that does not match a route fails validation. # Managing the Metadata Index Source: https://docs.lerian.studio/en/midaz/console/managing-metadata-index Manage the Metadata Index from the Lerian Console — list, create, and remove indexed metadata keys to speed up searches across your Midaz entities. Use the **Midaz Module** to manage the **Metadata Index**. Metadata indexes let you efficiently query entities by the custom key/value metadata you attach to organizations, ledgers, accounts, transactions, and other resources. ## What you can do * **List indexes** to see which metadata keys have an index. * **Create an index** on a metadata key to speed up lookups and filters on that key. * **Remove an index** when you no longer query that metadata key. Create an index for a supported entity and metadata key. `unique` defaults to `false`; `sparse` defaults to `true`. Keep the index focused on the keys you actually query. This keeps search fast and predictable. # Viewing Operations Source: https://docs.lerian.studio/en/midaz/console/managing-operations View the Operations that make up a Transaction in the Lerian Console — inspect debit, credit, hold, and release movements per account and their metadata. Use the **Midaz Module** to review the **Operations** that compose each Transaction. Operations are individual balance movements. Depending on the transaction lifecycle, their type can be `DEBIT`, `CREDIT`, `ON_HOLD`, or `RELEASE`. ## What you can do * **View operation details** to see the source or destination account, asset, amount, and resulting balance impact, along with any metadata. The operation details sheet is read-only. To update operation metadata, use the update-operation API. Operations always belong to a parent Transaction. You open them from the transaction detail view or from the operations list on the account details page. # Managing Organizations Source: https://docs.lerian.studio/en/midaz/console/managing-organizations Manage Organizations in the Midaz Module — create, edit, delete, and switch between the top-level entities that own your Ledgers and Accounts. The Midaz module lets you manage your Organizations directly. You can create, edit, delete, and switch between them. ## Accessing the Organizations page *** To open the **Organizations** page, click the **Organizations** option in the **Foundation** section of the left sidebar menu. The **Organizations** page lists all your Organizations in one place. ### Available actions From the **Organizations** page, you can: ## Multiple Organizations *** If you have more than one Organization, you can also open the **Organizations** page from the navigation bar. Click the Organization icon in the left navigation bar. Then select **Organizations** in the pop-up window (*Figure 1*). Alternative way to open the Organizations page, using the organization icon in the left navigation bar ### Changing Organizations To work on a different Organization, use the same pop-up window. Select the cog icon (). The **Organizations** page opens. Find the Organization you want. Click the three dots () in the **Action** column. Then select **Switch to this organization**. # Managing Portfolios Source: https://docs.lerian.studio/en/midaz/console/managing-portfolios Manage Portfolios from the Lerian Console — create, edit, and delete the containers that group related Accounts under a customer or business unit. Use **Midaz Module** to manage **Portfolios** that group your accounts. A Portfolio gives your accounts a higher-level structure. You manage a set of accounts as a single unit. ## What are Portfolios? *** A **Portfolio** is a container that groups related accounts within a ledger. In Midaz Module, portfolios enable you to: * **Group related accounts** - Organize accounts that belong together logically or operationally * **Associate with external entities** - Link portfolios to external system identifiers via Entity ID * **Simplify account management** - Manage multiple accounts as a cohesive unit * **Add custom context** - Store additional information through metadata for integration and reporting Portfolios are ledger-specific. Each ledger has its own set of portfolios for your needs. For technical details about portfolios in Midaz, refer to the [Portfolios documentation](/en/midaz/portfolios). ## Accessing the Portfolios page *** To open the **Portfolios** page, select the **Portfolios** option from the Accounts section of the left-side menu. The **Portfolios** page displays a table of all Portfolios in your Ledger. If you have no Portfolios, the list is empty and shows a prompt to create your first Portfolio. ### Available actions From the **Portfolios** page, you can: Add a new portfolio to group your accounts Update portfolio name and metadata Remove a portfolio from your ledger ## Common use cases *** Create portfolios to group all accounts belonging to a single customer: * Portfolio: `Customer-12345` with Entity ID linking to your CRM * Contains: Checking account, savings account, investment accounts * Use metadata for customer tier, relationship manager, onboarding date Organize accounts by financial product: * `Checking Accounts Portfolio` - All customer checking accounts * `Savings Accounts Portfolio` - All customer savings accounts * `Business Accounts Portfolio` - All commercial/business accounts * `Investment Accounts Portfolio` - All investment-related accounts Group accounts by institution or partner: * `Partner Bank A` - Accounts related to Partner Bank A operations * `Partner Bank B` - Accounts for Partner Bank B integration * Use Entity ID to link to partner identifiers in your system Create portfolios for operational purposes: * `Treasury Operations` - Internal treasury and liquidity accounts * `Settlement Accounts` - Accounts used for settlement operations * `Suspense Accounts` - Temporary holding accounts for reconciliation * `Fee Collection` - Accounts for collecting and managing fees ## Best practices *** Design your portfolio structure before you create accounts. Decide how to group the accounts and which Entity IDs to use for external systems. Set the Entity ID during creation when a portfolio links to an external system (CRM, core banking). External systems can then match the portfolio to their own records. Use portfolio names that clearly indicate the purpose or contents of the portfolio. This helps team members quickly understand the account organization. Use the Metadata tab to store extra details. Add business unit codes, cost centers, or integration references that do not fit in the standard fields. Each portfolio should have a clear purpose. Do not create catch-all portfolios. Instead, create a specific portfolio for each use case. ## Portfolios vs Segments *** Both Portfolios and Segments help organize accounts, but they serve different purposes: | Feature | Portfolio | Segment | | ---------------- | ----------------------------------------- | ----------------------------------------- | | **Purpose** | Group related accounts as a unit | Classify accounts by category | | **Entity ID** | Yes - links to external entities | No | | **Use Case** | Customer accounts, product grouping | Departmental, regional classification | | **Relationship** | One-to-many (portfolio contains accounts) | One-to-many (segment classifies accounts) | You can use portfolios and segments together to organize accounts. For example, accounts in a customer portfolio can also belong to a segment (checking, savings, investment). # Managing Segments Source: https://docs.lerian.studio/en/midaz/console/managing-segments Manage Segments from the Console — create, edit, and delete the classifications used to group Accounts by department, region, or business function. Use **Midaz Module** to manage **Segments** that help you organize and categorize your accounts. Segments provide a way to group accounts by business purpose, department, region, or any other classification relevant to your organization. ## What are Segments? *** A **Segment** is an organizational unit that helps you classify and group accounts within a ledger. In Midaz Module, segments enable you to: * **Organize accounts by purpose** - Group accounts by business function, department, or product line * **Create reporting hierarchies** - Build structures for financial reporting and analysis * **Apply business classifications** - Use segments to filter and analyze accounts by classification * **Maintain flexibility** - Add custom metadata for integration and business-specific needs Segments are ledger-specific, meaning each ledger can have its own set of segments tailored to its organizational structure. For technical details about segments in Midaz, refer to the [Segments documentation](/en/midaz/segments). ## Accessing the Segments page *** To open the **Segments** page, select the **Segments** option on the Accounts section of the left-side menu. The **Segments** page displays a table listing all Segments in your Ledger. If you do not have any Segments, the list will appear empty with a prompt to create your first segment. ### Available actions From the **Segments** page, you can: Add a new segment to organize your accounts Update segment name and metadata Remove a segment from your ledger ## Common use cases *** Create segments to represent different departments or cost centers: * `Finance` - For treasury and financial operations accounts * `Operations` - For operational and transactional accounts * `HR` - For payroll and employee-related accounts * `Sales` - For revenue and customer-related accounts Organize accounts by product or service offering: * `Checking Products` - All checking account-related segments * `Savings Products` - Savings and investment account segments * `Loans` - Credit and lending product segments * `Cards` - Credit and debit card operation segments Group accounts by geographic region: * `North Region` - Accounts for northern operations * `South Region` - Accounts for southern operations * `International` - Cross-border and international accounts Create segments for regulatory and reporting purposes: * `Regulatory Reserve` - Accounts subject to reserve requirements * `Audit Trail` - Accounts requiring special audit tracking * `Tax Reporting` - Accounts with specific tax reporting needs ## Best practices *** Design your segment hierarchy before creating accounts. A well-planned structure makes account management and reporting easier. Choose segment names that clearly indicate their purpose. This helps team members understand account organization at a glance. Use the Metadata tab to store additional information like cost center codes, department IDs, or integration references. If you have multiple ledgers, consider using consistent segment naming conventions for easier cross-ledger reporting. Periodically review your segment structure to ensure it still aligns with your organizational needs. ## Segments and transaction rules *** Segments classify accounts for organization, filtering, and reporting — they do not control transaction flows. Route validation in [Accounting Routes](/en/midaz/console/managing-accounting-routes) supports only account types and aliases; there is no segment-based routing. Use segments to keep your account structure organized and to slice reports by department, region, or product line. # Managing Transactions Source: https://docs.lerian.studio/en/midaz/console/managing-transactions Manage Transactions from the Midaz Module — create new Transactions between Accounts and inspect approved or canceled records in your Ledger. Use the **Midaz Module** to view, create, and monitor **Transactions** in a visual interface. The Transactions page shows every financial movement in your Ledger. You can track balances, review operations, and create transactions without writing code. See the [Transactions](/en/midaz/transactions) reference documentation for transaction structures, double-entry accounting, N:N transactions, and API integration. ## Accessing the Transactions page *** Select the **Transactions** option in the left-side menu to open the **Transactions** page. The **Transactions** page shows a table of all Transactions in your Ledger. If your Ledger has no Transactions, the list is empty and shows a prompt to create your first transaction. ### Available actions From the **Transactions** page, you can: Record a new asset movement between accounts See complete transaction information and operations ## Transaction status *** Transactions in Midaz have the following statuses: * **CREATED** - The initial state of a transaction * **PENDING** - A two-step transaction holding funds, awaiting commit or cancel * **APPROVED** - Midaz posted the transaction * **CANCELED** - Midaz canceled the transaction After Midaz approves a transaction, you cannot edit its operations or amounts, and you cannot delete it. To reverse it, create a reversal transaction. This protects the integrity of your financial records. ## Common use cases *** A basic transaction moving funds from one account to another: * **Source**: Customer checking account (debit \$100) * **Destination**: Merchant account (credit \$100) * Single source, single destination, equal amounts A transaction that splits funds between multiple destinations: * **Source**: Customer account (debit \$103) * **Destinations**: * Merchant account (credit \$100) * Fee collection account (credit \$3) * Single source, multiple destinations Multiple sources funding a single destination: * **Sources**: * Primary account (debit \$500) * Secondary account (debit \$300) * **Destination**: Vendor account (credit \$800) * Multiple sources, single destination Multiple sources and destinations in a single transaction: * **Sources**: * Customer A (debit \$200) * Customer B (debit \$300) * **Destinations**: * Vendor account (credit \$450) * Tax account (credit \$50) * Multiple sources, multiple destinations ## Best practices *** Include a clear description of the purpose of each transaction. This helps auditing and troubleshooting. Make sure the source account has enough balance before you create a transaction. This avoids rejections. Store external reference IDs, order numbers, or other system identifiers in the transaction metadata for traceability. When possible, combine related transfers into one transaction instead of several separate transactions. Double-check the source accounts, destination accounts, and amounts before you submit. You cannot edit a transaction after you create it. ## Accounting Routes integration *** Midaz can validate Transactions against [Accounting Routes](/en/midaz/console/managing-accounting-routes) before it processes them. Accounting Routes define which accounts can transact with each other. They add a layer of control over your financial operations. # Accounting rules Source: https://docs.lerian.studio/en/midaz/console/mc-accounting Learn how Accounting Routes decide which Accounts join a transaction and how debit and credit entries are posted for each transaction type in Midaz. The **Accounting** section is where you define optional rules for transactions. You do this through **Accounting Routes**. An Accounting Route is a reusable rule for one kind of transaction, such as a *Pix transfer*, a *card purchase*, or a *fee charge*. Each route answers three questions: * Which accounts can act on the source side? * Which accounts can act on the destination side? * Which debit and credit entries does the route post when the transaction runs? **Example.** A `Pix transfer` route can require a `customer` account on the source side and a `merchant` account on the destination side. The route then defines direct debit-and-credit entries between them. When route validation is enabled, a Pix transaction that references the route validates the accounts and applies the configured rules. Accounting Routes provide optional route validation. Midaz enforces them only when the Ledger setting `accounting.validateRoutes` is enabled (default `false`). When enabled, transactions must reference valid routes and operations must match configured rules. Routes are easier to model when you already know which accounts represent customers, treasury, fees, settlement, revenue, and expenses. ## How the pieces fit together *** How accounts, account types, and accounting routes fit together so a transaction request is validated and posted as debit-and-credit entries | Piece | What it controls | Example | | ------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- | | Accounting Route | The transaction-level route that groups the operation rules. | `Pix transfer route` | | Operation Route | The account side and validation rule. | `Source must be customer`, `Destination must be merchant` | | Validation rule | How Midaz decides whether an account can be used. | Account Type `customer` or alias `@treasury_main` | | Accounting scenario | Which debit and credit entries are posted for the transaction lifecycle. | Direct, Two-Step, Reversal, Overdraft, Block/Unblock | ## Choosing the operation type *** Use **Source** for accounts where value originates. Example: a customer account can send funds in a payment flow. Use **Destination** for accounts where value lands. Example: a merchant account can receive funds in a payment flow. Use **Bidirectional** when the same account class can send and receive. Example: checking accounts can transfer value to other checking accounts. A route must include a Source and a Destination operation route, or at least one Bidirectional operation route. ## Choosing the validation rule *** | Validation type | Use when | Example | | --------------- | --------------------------------------- | -------------------------------- | | Account Type | Any account in a class should be valid. | Any `customer` account can send. | | Alias | Only one exact account should be valid. | Only `@treasury_main` can send. | An Operation Route can optionally include an account rule—registered Account Types or an `@Alias`. Midaz enforces a present rule when route validation is enabled. Use Account Type validation for scalable flows. Use alias validation for fixed operational accounts, such as treasury, fee, settlement, or suspense accounts. ## Common route patterns *** ### Customer to merchant Use separate Source and Destination routes when each side has a different role. | Operation route | Validation | | --------------- | ----------------------- | | Source | Account Type `customer` | | Destination | Account Type `merchant` | ### Peer-to-peer transfer Use a Bidirectional route when the same account type can be both source and destination. | Operation route | Validation | | --------------- | ----------------------- | | Bidirectional | Account Type `customer` | ### Fee collection Use a Destination route with alias validation when fees must always land in one operational account. | Operation route | Validation | | --------------- | ----------------------- | | Source | Account Type `customer` | | Destination | Alias `@fee_revenue` | ## Accounting scenarios *** | Scenario | Use when | What the user configures | | -------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Direct Transaction | The movement is executed in one step. | Debit and credit entries for immediate posting. | | Two-Step Transaction | The movement has hold, commit, and cancel phases. | Entries for reservation, confirmation, and cancellation. | | Reversal | A completed transaction may need to be reversed. | Debit and credit entries for the reversal event. | | Overdraft | A debit may exceed the account's available funds, drawing on an overdraft line. | Debit and credit entries for the overdraft draw and later repayment. | | Block/Unblock | Funds need to be held and later released on the account's balance. Available for all route types (tab label "Block"). | Entries for the block (hold) and unblock (release) events. | Do not enable route validation in Ledger settings until the required routes exist. If you enable it without matching routes, transactions fail validation. ## Available pages *** A step-by-step, Console-first guide to building your accounting model — from chart of accounts to a working Pix payment, no API calls required. Configure accounting routes with operation rules and accounting scenarios in a unified wizard. # Account management Source: https://docs.lerian.studio/en/midaz/console/mc-accounts Explore how Accounts, Holders, Aliases, Segments, and Portfolios work together in the Midaz Module to organize your financial structure and ownership. The **Accounts** section is where you open the accounts that hold balances, identify who owns them, and group them for reporting and control. Accounts can be linked to a Holder, Segment, and Portfolio. These persisted relationships support ownership, grouping, fee resolution, and scoped rules; they are not merely tags. You will work with the following here: * **Account** — The record that holds a balance of one asset and can send or receive that asset through transactions (for example, a customer's BRL account). * **Holder** — The individual or entity that owns one or more accounts (for example, a customer or a merchant registered in CRM). * **Alias Account** — An optional CRM Instrument linked to a Holder and an existing ledger Account. It stores banking details. This is different from the account's own **alias** field, a friendly identifier on the account itself that lets you refer to it by name instead of by ID (for example, `@treasury_main`). * **Segment** — An optional ledger-scoped grouping for Accounts that can scope fee selection and Tracer limits. * **Portfolio** — A ledger-scoped grouping of Accounts with an optional external `entityId`; it is not a direct Holder relationship. ## Available pages ***
Create and configure the accounts that hold and move assets within your ledgers. Register the individuals or entities that own accounts in your system. Link holders to ledger accounts with banking details. Organize accounts into logical groupings for reporting and control. Group accounts into portfolios for consolidated management.
# Foundation elements Source: https://docs.lerian.studio/en/midaz/console/mc-foundation Set up the Foundation of the Midaz Module — Organizations, Ledgers, Assets, and Account Types that every downstream Account and transaction depends on. The **Foundation** section is the structure that the rest of Midaz builds on. Set up an Organization, Ledger, and Asset before opening an Account. Define Account Types when your configuration needs them; you can create and update Foundation resources as the environment evolves. You will work with four elements here: * **Organization** — The top-level entity that owns one or more ledgers. Usually represents your company or business unit (for example, *Acme Bank*). * **Ledger** — The isolated book of records where accounts, balances, and transactions live. Use one or more ledgers according to your product and operational needs. * **Asset** — A currency or instrument that accounts can hold and move, identified by a code (for example, `BRL`, `USD`, or `USDC`). Each account holds one asset. * **Account Type** — An optional label that classifies accounts when your configuration needs it (for example, `customer`, `treasury`, `fee`, or `revenue`). ## Available pages *** Create and configure the top-level entities that group your ledgers and assets. Set up ledgers to organize and track your financial operations. Register the currencies and instruments used across your ledgers. Define the categories that classify your accounts within each ledger. # Transaction basics Source: https://docs.lerian.studio/en/midaz/console/mc-transactions Use the Midaz Module to record Transactions that move Assets between Accounts with double-entry accounting, N:N support, and a full audit trail. Use **Midaz Module** to manage **Transactions** that record the movement of assets between accounts. Transactions are the core of your ledger operations. They record every financial movement in your system. ## What are Transactions? *** A **Transaction** represents the transfer of assets from one or more source accounts to one or more destination accounts. In Midaz Module, transactions: * **Move assets between accounts** - Record value movements through one or more Operations * **Support multiple sources and destinations** - Handle complex transactions with multiple parties * **Track status** - Monitor the transaction lifecycle (`CREATED`, `APPROVED`, `PENDING`, `CANCELED`, or `REVERTED`) * **Maintain audit trail** - Record complete history with timestamps and metadata * **Support accounting controls** - When enabled, Accounting Routes validate configured rules for the Transaction A transaction is immutable. Once Midaz records the movement, you cannot change or delete it. You can still update the transaction description and metadata, cancel a pending transaction, or view it at any time. For technical details about transactions in Midaz, refer to the [Transactions documentation](/en/midaz/transactions). ## Available pages *** Create Transactions and inspect their details after processing. # Concepts map Source: https://docs.lerian.studio/en/midaz/console/midaz-console-concepts-map Understand how Organizations, Ledgers, Assets, Accounts, Routes, and Transactions depend on each other before you start configuring the Midaz Module. The Midaz Module uses a small set of core objects with explicit dependencies. Organization, Ledger, Assets, and Accounts are the core setup; Account Types, CRM records, Portfolios, Segments, and Accounting Routes are configured when the workflow needs them. ## Core hierarchy *** Core Midaz Module hierarchy, from Organization and Ledger down through Assets, Accounts, Routes, and Transactions ### What each object means *** | Object | What it represents | Why it exists first or later | | ------------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `Organization` | The legal or operational owner of the setup | It is the top-level boundary. Nothing below exists without it. | | `Ledger` | The accounting environment where balances and movements are recorded | It groups the structure that a team will operate day to day. | | `Asset` | The unit of value moved in the ledger, such as `BRL` or `USD` | Accounts and transactions depend on it. | | `Account` | A balance container that can send or receive value | Transactions happen between accounts. | | `Account Type` | An optional classification such as `checking`, `treasury`, or `expense` | Use it when configured account or route rules need it. | | `Portfolio` | A grouping of accounts, often by customer or wallet structure | Useful for reporting and organization, but not a prerequisite for every account. | | `Segment` | A ledger-scoped persisted grouping of accounts, such as by department, region, or tier | Useful when the business needs slicing or policy grouping. | | `Holder` | The person or entity that owns accounts in CRM flows | Needed when account ownership matters operationally. | | `Alias` | A human-readable identifier used to find or refer to an account | Especially important in transaction creation. | | `Accounting Route` | An optional rule set for transaction participants and posting behavior | Midaz applies configured route rules when route validation is enabled. | | `Transaction` | The actual balance movement recorded in the ledger | It is the output of the whole setup. | ## How a transaction actually depends on prior setup *** How creating a transaction depends on the prior setup of organization, ledger, assets, accounts, and accounting routes The required prerequisites depend on the Transaction and Ledger settings. Assets and Accounts are required; Accounting Routes are required only when route validation is enabled. ## Practical reading of the UI *** Use this interpretation when you guide users: • `Foundation` defines where the system lives: Organization, Ledger, Asset. • `Accounting` defines optional participation and posting rules: Account Types and Accounting Routes. • `Accounts` defines who holds balances and how you classify them: Accounts, Holders, Aliases, Segments, Portfolios. • `Transactions` exercises the configured structure. • `Plugins` extend the model after the core behavior already works. ## When users usually get lost *** The Account is the balance container. The Holder is the CRM identity. An Alias Account is an optional CRM Instrument linked to a Holder and an existing Ledger Account; the Account's own `alias` is its human-friendly identifier. Account Type is an optional classification. Segment and Portfolio are ledger-scoped persisted groupings of Accounts; they are not merely labels or direct Holder relationships. The Route is an optional reusable rule. The Transaction is the individual movement; it is checked against configured route rules only when route validation is enabled. Almost every object below it is ledger-scoped. Users who treat the ledger as a cosmetic container usually misconfigure accounts and routes later. Pair this page with [Setup path](/en/midaz/console/midaz-console-setup-path) when you onboard new users. One page explains the model. The other explains the order. # Onboarding Source: https://docs.lerian.studio/en/midaz/console/midaz-console-onboarding Follow the Midaz Module guided onboarding to create your first Organization and Ledger in a few minutes, with fields explained at every step. The Midaz Module guides you through a step-by-step onboarding flow. It helps you set up your Organization and create your first Ledger. ## Onboarding process *** The onboarding starts automatically when you have no Organization. This happens when you never created one, or when you deleted all your Organizations. This flow guides you through the initial setup. At each step, you see where you are and why the step matters. In a few minutes, your Organization and your first Ledger are ready. You need both to access the full platform. ### Starting the process Click the **Let's go** button on the first-steps window. ## Part 1: Creating your Organization *** The first part of onboarding creates your Organization in three steps. ### Step 1 – Set up your Organization details This step is the foundation. You enter the key details of your Organization. **Required field:** * **Legal Name** – Your company's official registered name. You can also provide a **Trade Name** (DBA) and a **Document** such as a CNPJ, EIN, or other registration number. The **Document** is permanent. After you submit it, you cannot change it. You can still edit the Legal Name and Trade Name later in the Organization settings. Click **Next** at the bottom of the screen. ### Step 2 – Add your Organization's address This step adds the registered address of your Organization. **Required fields:** * **Address** – Street address (line 1) * **Complement** – Additional address information (optional) * **Country** – Select from the dropdown * **State** – Select from the dropdown (based on country) * **City** – Your city name * **ZIP Code** – Postal code You can update your address at any time in the Organization settings. Click **Next** at the bottom of the screen. ### Step 3 – Add a theme (optional) Upload your Organization's **Icon** to personalize the Midaz Module. **Icon format:** * SVG or PNG file * 256x256 px When you submit this step, Midaz creates your Organization and returns a unique `organizationId`. Your icon becomes part of the Organization metadata. You can skip this step and add the icon later. A confirmation dialog asks if you want to finish without a theme. Click **Finish** to continue. ### Organization created Your Organization is ready. You see a confirmation dialog. Click **Continue** to create your first Ledger. ## Part 2: Creating your first Ledger *** After Midaz creates your Organization, the flow guides you to create your first Ledger. The Ledger is a core part of Midaz. It is where you manage Accounts, balances, and Transactions. Create Assets separately for the Organization, then use them when you create Accounts. **Required field:** * **Ledger Name** – A unique name for your Ledger. You can configure additional metadata later. An alert on the page reminds you of this option. Enter a name for your Ledger. Click **Finish** to complete the setup. ## What's next? *** With onboarding complete, you can explore everything the Midaz Module offers: Edit your Organization settings and create additional Organizations. Configure your Ledger and create new ones. Define the currencies and tokens for your Organization. Set up accounts to start managing balances. # Setup path Source: https://docs.lerian.studio/en/midaz/console/midaz-console-setup-path Follow the recommended setup order for the Midaz Module so each Console screen has the Organization, Ledger, Assets, and Accounts it depends on. The Midaz Module is easier to configure when you follow the dependency order the product expects. Most user confusion comes from trying to create Accounts, Routes, or Transactions before the structural pieces already exist. ## Recommended order *** Start with the legal and operational owner of the environment. Use [Midaz Module onboarding](/en/midaz/console/midaz-console-onboarding) if this is your first setup. If the organization already exists and you want to add another, go directly to [Creating an Organization](/en/midaz/console/creating-an-organization). The Ledger is the workspace where balances, Accounts, and Transactions are scoped. Go to [Creating a new Ledger](/en/midaz/console/creating-a-new-ledger). Decide whether to enable Accounting Route validation before users start creating Transactions. This setting is optional and defaults to `false`. You can configure these settings while creating the Ledger in [Creating a new Ledger](/en/midaz/console/creating-a-new-ledger#ledger-settings), or update them later from [Managing Ledgers](/en/midaz/console/managing-ledgers-via-console#ledger-settings). Assets define a unit of value for the Organization, such as `BRL`, `USD`, or a custom token. Accounts reference an Asset when you create them. Go to [Creating an Asset](/en/midaz/console/creating-an-asset). Define Account Types when your configured Account or Route rules need those classifications. Go to [Creating an Account Type](/en/midaz/console/creating-an-account-type). Accounts hold balances and are the source or destination of transactions. Go to [Creating an Account](/en/midaz/console/creating-an-account). These are persisted support structures. Configure them when your workflow needs ownership context, grouping, fee resolution, or scoped rules. Use: * [Creating a Portfolio](/en/midaz/console/creating-a-portfolio) * [Creating a Segment](/en/midaz/console/creating-a-segment) * [Creating a Holder](/en/midaz/console/crm-creating-a-holder) * [Creating an Alias Account](/en/midaz/console/crm-creating-alias-account) Accounting Routes define optional participant and posting rules. Midaz applies them when route validation is enabled. Go to [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route). Create Transactions after the Organization has an Asset and the Ledger has the required Accounts. A matching Accounting Route is required only when route validation is enabled. Go to [Creating a Transaction](/en/midaz/console/creating-a-transaction). Capabilities such as Fees Engine extend behavior. Turn them on after the base ledger model is already clear. Go to [Midaz module settings](/en/platform/console/plugin-settings). ## Fast path by goal *** Use onboarding, then create at least one Asset and one Account. Start with Ledger settings, Account Types, Accounts, and Segments. Create the accounts first, then define Accounting Routes and test with a transaction. Enable Fees Engine only after the core ledger structure is already in place. ## Common sequencing mistakes *** You must link every Account to an Asset. If the Asset does not exist yet, account creation stalls immediately. This makes the type field mandatory, but the dropdown remains empty. Create the Account Types first or disable validation until they are ready. Users often try to model routes too early. In practice, route rules are easier to define after the treasury, customer, fee, and settlement accounts already exist. Transaction creation depends on accounts being searchable and structurally valid. If operators cannot identify the source and destination accounts quickly, stop and fix the account model first. When you document the setup or train a client team, teach this order first. The CRUD pages become much easier once the dependency chain is explicit. # Troubleshooting Source: https://docs.lerian.studio/en/midaz/console/midaz-console-troubleshooting Fix common Midaz Module issues in Lerian Console — missing menus, empty dropdowns, blocked transactions, and Ledger validation setting mismatches. Use this page when the Console does not show an option you expected, or when you cannot complete a form. Also use it when a transaction does not behave as you expect. Most issues come from missing prerequisites, or from Ledger settings you enabled before the supporting configuration existed. ## First checks *** Before you troubleshoot a specific screen, confirm these basics: * You selected the correct **Organization** and **Ledger**. * The Ledger already has the required **Assets**. * The Ledger settings match the workflow you want to use. * The required **Account Types**, **Accounts**, and **Accounting Routes** exist before you create transactions. * You use each account alias exactly as registered, with the `@` prefix. Review the recommended setup order before you change individual screens. ## Common issues *** The **Account Types** menu appears when you enable **Validate Account Type** in the current Ledger settings. To fix it: 1. Open **Foundation > Ledgers**. 2. Open the target Ledger details. 3. Go to the **Settings** tab. 4. Enable **Validate Account Type**. 5. Return to the sidebar and open **Accounting > Account Types**. After you enable this setting, new Accounts must use a valid Account Type. Create the required types before you ask operators to create accounts. Each Account must link to an existing Asset. If no Asset exists in the selected Ledger, you cannot complete the Account form. Create the Asset first in [Creating an Asset](/en/midaz/console/creating-an-asset), then return to [Creating an Account](/en/midaz/console/creating-an-account). This usually means you enabled **Validate Account Type** but created no Account Types for the selected Ledger. Create at least one Account Type in [Creating an Account Type](/en/midaz/console/creating-an-account-type). If your workflow does not need type validation, disable **Validate Account Type** in Ledger settings. Transaction creation searches accounts by alias. Confirm that: * The account exists in the selected Ledger. * The alias includes the `@` prefix. * The account allows the expected direction: source accounts can send, destination accounts can receive. * The account Asset matches the Asset you selected for the Transaction. If the alias is wrong or does not exist, open the account details from [Managing Accounts](/en/midaz/console/managing-accounts). The **Validate Routes** setting controls route validation. When you enable it, transactions must match an existing Accounting Route. Check that: * The Accounting Route exists in the selected Ledger. * The route has at least one valid source and one valid destination operation route, or one bidirectional route. * Account Type or alias validation matches the accounts used in the transaction. * The transaction type matches the accounting scenario configured in the route. Start with [Managing Accounting Routes](/en/midaz/console/managing-accounting-routes) and then review [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route). For complex transactions, the total you debit from source accounts must equal the total you credit to destination accounts. Review every operation before you send the transaction. To model fees, include the fee account as an additional destination so the total credits match the total debits. You cannot delete some entities after downstream records use them. For example, treat an Asset that Accounts use, or an Account that Transactions use, as part of the ledger history. Instead, edit optional metadata, disable future use where the Console supports it, or create a replacement entity for new operations. Start from the transaction details page and check: * The source and destination accounts. * The Asset used in the transaction. * Each debit and credit operation. * The transaction state: direct, pending, committed, cancelled, or reversed. Then open the account balance and operation history for the accounts involved. ## API references *** When a Console issue needs deeper inspection, use the corresponding API references: Confirm whether the account exists in the selected Ledger. Inspect account details such as type, asset, and metadata. Review transaction history for a Ledger. Inspect transaction status and operation details. # About Midaz module Source: https://docs.lerian.studio/en/midaz/console/midaz-module Discover the Midaz Module in Lerian Console — manage Organizations, Ledgers, Accounts, Assets, and Transactions from a single visual interface. **Midaz** is a module inside the [Lerian Console](/en/platform/console/about-lerian-console) that provides a visual interface for managing ledger infrastructure. From a single screen, you create organizations and ledgers, register assets (such as BRL or USD), and open accounts. You define the rules that govern how money moves, and record transactions — all without writing API calls. Use the Midaz Module for day-to-day operations: open an account, run a transaction, or check balances. You can also configure how the ledger validates and posts transactions. **Important transition**: Midaz Console is now part of Lerian Console as the **Midaz Module**. All existing functionality remains available, along with new integrations and platform features. ## Start here first *** New to Midaz? Start with these pages. They show what to create, in which order, and how the main components work together. Follow the recommended setup order for Organizations, Ledgers, Assets, Accounts, Routes, and Transactions. Learn how Midaz components connect and how they support financial operations. Resolve common issues related to configuration, routes, permissions, and transactions. ## What you can do *** From the Midaz Module, you can: * **Manage organizations and ledgers** — Create the operational structure used by your financial environment * **Configure assets** — Define currencies, tokens, and other asset types used across ledgers * **Set up accounts** — Create accounts with account types, portfolios, and segments * **Configure accounting routes** — Define how operations and transactions move assets between accounts * **Monitor transactions** — Track financial movements across organizations and ledgers * **Manage Holders and Alias Accounts** — Use the integrated CRM to register account owners and link them to ledger accounts * **Access plugins** — Enable and configure capabilities such as Fees Engine from the [Settings](/en/platform/console/plugin-settings) page ## Recommended setup order *** For a new environment, we recommend configuring Midaz in the following order: Define the top-level structure that groups your ledgers and operations. Create the ledger that will store balances, accounts, and transactions. Adjust operational and accounting settings for the ledger. Define the currencies or asset types that will circulate in the ledger. Define how accounts behave within the financial structure. Create the accounts that will hold balances and participate in transactions. Organize accounts based on business or operational needs. Define how transactions move assets between accounts. Start recording and monitoring financial operations. Activate optional platform capabilities such as Fees Engine. For a more detailed explanation of each step, see [Setup path](/en/midaz/console/midaz-console-setup-path). ## Accessing Midaz *** To open Midaz, select the **Midaz Module** from the Lerian Console product launcher (*Figure 1*). The Midaz Module card in the Lerian Console product launcher, used to open Midaz ## Available features *** The sidebar follows the order in which most teams build their environment. You start with **Foundation**, then move to **Accounting**, **Accounts**, **Transactions**, and finally **Plugins**. Configure Organizations, Ledgers, Assets, and Account Types that support your financial structure. Configure Accounting Routes that control how assets move through the system. Manage Accounts, Holders, Aliases, Segments, and Portfolios. Create and monitor transactions that transfer assets between accounts. Extend Midaz with integrated capabilities such as Fees Engine. ## Migrating from Midaz Console *** If you're currently using the standalone **Midaz Console**, here's what changes when moving to Lerian Console. The standalone Midaz Console is now in maintenance mode. It no longer receives feature updates. Only critical security fixes still apply. We recommend migrating to Lerian Console to access the latest platform capabilities and integrations. ### Why migrate to Lerian Console? * **Unified platform** — Access Midaz and other Lerian products from a single interface * **New platform features** — Lerian releases new improvements and capabilities only in Lerian Console * **Integrated workflows** — Connect Midaz with plugins and other platform modules * **Simplified navigation** — Work with a consistent operational experience across products ### What stays the same? * All core Midaz capabilities remain available * Existing data and configurations continue to work * Operational workflows remain familiar for current users # Alias Accounts Source: https://docs.lerian.studio/en/midaz/crm/alias-accounts Link Holders to Midaz Ledger Accounts with Alias Accounts, adding banking, regulatory, and related-party context for CRM-driven features. An **Alias Account** adds business context to a [Ledger Account](/en/midaz/accounts) in Midaz. It links a [Holder](/en/midaz/crm/holders) to a specific account in the ledger. It adds banking details, regulatory information, and related-party data to that account. Without this link, CRM features that rely on account context do not work as expected. For a step-by-step walkthrough of linking holders to accounts, see [Getting started with CRM](/en/midaz/crm/crm-getting-started). ## How it works *** The Alias Account is a CRM-level representation of a Midaz Ledger Account. When you create an Alias Account, you provide the `ledgerId` and `accountId` that identify the target account in the ledger. The Alias Account inherits the `document` and `type` from its parent Holder automatically. This design keeps customer-facing account details separate from the transactional ledger. Bank numbers, branch codes, and regulatory identifiers stay in the CRM, not in the ledger. The separation supports multi-bank setups and integration with external systems. Always link an Alias Account to an existing Holder. Create the Holder first, then create the Alias Account. See [Using CRM](/en/midaz/crm/crm-using-overview) for the correct integration flow. ## Alias Account fields *** ### Core fields | Field | Type | Required | Description | | :------------ | :--------- | :--------------- | :---------------------------------------------------------------------------------------------------------------- | | **id** | `uuid` | System-generated | Unique identifier of the Alias Account. | | **holderId** | `uuid` | System-generated | The ID of the associated Holder (derived from the URL path). | | **ledgerId** | `string` | Yes | The UUID of the Midaz Ledger. | | **accountId** | `string` | Yes | The UUID of the Midaz Ledger Account. | | **document** | `string` | System-generated | Inherited from the parent Holder. | | **type** | `string` | System-generated | Inherited from the parent Holder (`NATURAL_PERSON` or `LEGAL_PERSON`). | | **metadata** | `object` | No | Key-value pairs for custom, non-sensitive data. Keys are limited to 100 characters and values to 2000 characters. | | **createdAt** | `datetime` | System-generated | Timestamp of creation (RFC 3339). | | **updatedAt** | `datetime` | System-generated | Timestamp of last update (RFC 3339). | | **deletedAt** | `datetime` | System-generated | Timestamp of soft deletion, if applicable (RFC 3339). | ### Banking details The `bankingDetails` object stores information about the financial institution for the alias: | Field | Type | Required | Description | | :-------------- | :------- | :------- | :------------------------------------------------------------------ | | **branch** | `string` | No | Bank branch code (e.g., `0001`). | | **account** | `string` | No | Bank account number (e.g., `123450`). | | **type** | `string` | No | Account type code (e.g., `CACC` for current account). | | **openingDate** | `string` | No | Date the account was opened in `YYYY-MM-DD` format. | | **closingDate** | `string` | No | Date the account was closed, if applicable, in `YYYY-MM-DD` format. | | **iban** | `string` | No | International Bank Account Number. | | **countryCode** | `string` | No | Country code of the financial institution (e.g., `US`, `BR`). | | **bankId** | `string` | No | Identifier of the bank or financial institution. | ### Regulatory fields The `regulatoryFields` object stores data that financial regulators require: | Field | Type | Required | Description | | :---------------------- | :------- | :------- | :--------------------------------------------------------------------------------- | | **participantDocument** | `string` | No | Document number identifying the financial-group entity that owns the relationship. | ### Related parties A related party is a person or entity linked to an Alias Account. Each related party has a defined role and a time-bounded relationship. Related parties represent the real-world people or organizations connected to the account, for ownership, legal authority, or operational accountability. Compliance, regulatory reporting, and CRM-driven workflows use them. #### Roles Each related party must have one of the following roles: | Role | Description | | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `PRIMARY_HOLDER` | The main individual or entity who owns or holds the account. This is typically the customer themselves when account ownership differs from the Holder record. | | `LEGAL_REPRESENTATIVE` | Someone with legal authority to act on behalf of the holder — for example, a legal guardian, attorney, or authorized representative. | | `RESPONSIBLE_PARTY` | An entity responsible for the account in an operational or regulatory capacity, such as a compliance officer or a parent organization. | #### Time-bounded relationships Every related party relationship has a defined active period: * **`startDate`** — Required. The date the relationship became active (`YYYY-MM-DD`). * **`endDate`** — Optional. The date the relationship ended (`YYYY-MM-DD`). If you omit it, the relationship stays active. When you set it, `endDate` must be after `startDate`. This design records who held a relationship with the account, and in what capacity. It keeps past relationships in the record. #### Managing related parties You manage related parties through the Alias Account endpoints. There are no standalone create or list endpoints: * **Add on creation** — Include a `relatedParties` array in the [Create Alias Account](/en/reference/midaz/crm/create-alias-account) request body. * **Add to existing** — Include a `relatedParties` array in the [Update Alias Account](/en/reference/midaz/crm/update-alias-account) request body. Midaz **appends** new entries to the existing list. It does not replace existing related parties. * **Remove** — Use the [Delete Related Party](/en/reference/midaz/crm/delete-related-party) endpoint with the specific `related_party_id`. * **List** — The Alias Account response returns related parties in the `relatedParties` array. #### Fields | Field | Type | Required | Description | | :------------ | :------- | :--------------- | :----------------------------------------------------------------------------------------- | | **id** | `uuid` | System-generated | Unique identifier of the related party. | | **document** | `string` | Yes | Document number of the related party. Cannot be empty or whitespace. | | **name** | `string` | Yes | Full name of the related party. Cannot be empty or whitespace. | | **role** | `enum` | Yes | `PRIMARY_HOLDER`, `LEGAL_REPRESENTATIVE`, or `RESPONSIBLE_PARTY`. | | **startDate** | `string` | Yes | Date the party relationship started in `YYYY-MM-DD` format. | | **endDate** | `string` | No | Date the party relationship ended, if applicable. Must be after `startDate` when provided. | Validation errors for related party fields return specific error codes: **CRM-0025** (invalid role), **CRM-0026** (document required), **CRM-0027** (name required), **CRM-0028** (start date required), **CRM-0029** (end date invalid — must be after start date). See the [CRM error reference](/en/reference/midaz/crm/crm-error-list) for details. ## Data security *** Midaz **encrypts several Alias Account fields at rest**, including the inherited `document` field and banking details such as `account` and `iban`. Encryption protects sensitive financial data even if an attacker reaches the underlying storage. Never store sensitive information in the `metadata` object. Midaz does **not** encrypt metadata, and stores it in plain text. For the full list of protected fields and encryption strategies, see [CRM data security](/en/midaz/crm/crm-data-security). ## Managing Alias Accounts *** ### Via API Use the CRM API to manage Alias Accounts programmatically: * [Create an Alias Account](/en/reference/midaz/crm/create-alias-account) — Link a Holder to a Midaz Ledger Account. * [List Alias Accounts](/en/reference/midaz/crm/list-alias-accounts) — View all Alias Accounts with pagination and filters. * [Retrieve an Alias Account](/en/reference/midaz/crm/retrieve-alias-account) — Get the details of a specific Alias Account. * [Update an Alias Account](/en/reference/midaz/crm/update-alias-account) — Edit the details of an existing Alias Account. * [Delete an Alias Account](/en/reference/midaz/crm/delete-alias-account) — Soft-delete or permanently remove an Alias Account. The organization ID is a URL path parameter for every Alias Account operation. The Holder ID is a path parameter for most of them. If [Access Manager](/en/platform/access-manager/access-manager) is enabled, add an `Authorization` header with a Bearer token. ### Via Lerian Console You can manage Alias Accounts through the **Alias Accounts** page in the Midaz Module of [Lerian Console](/en/platform/console/about-lerian-console). The console gives you a visual interface to create, view, edit, and delete Alias Accounts without writing code. [**Learn more in the Managing Alias Accounts guide.**](/en/midaz/console/managing-crm-alias-accounts) ## Next steps *** Learn about the Holder entity that an Alias Account links to. Review operational and data management best practices for CRM. # Best practices Source: https://docs.lerian.studio/en/midaz/crm/crm-best-practices Follow CRM production best practices covering integration flow, encryption key management, metadata safeguards, and safe operational workflows. CRM manages sensitive, identity-related data. These practices help you integrate CRM correctly, protect that data, and run it safely in production. These recommendations complement the [CRM data security](/en/midaz/crm/crm-data-security) guide, which covers encryption, hashing, and key management in detail. ## 1. Follow the correct integration flow *** CRM relies on a specific order to create entities. If you skip steps or create entities out of order, you can break integrations and lose data downstream. The expected flow is: 1. **Create the holder** — the individual or the organization. 2. **Link the holder to a Midaz ledger account** — associate the holder with an account that already exists in the ledger. 3. **Make sure both exist** — before you start any downstream flow. Without a holder linked to a ledger account, most CRM-driven features (fees, notifications, billing, identity verification) do not work as expected. Make sure your identifiers are correct before you create or link records. The CRM API does **not** validate the accuracy of the data you submit. A mismatched `ledgerId` or `accountId` can cause integration failures with the ledger or other components. ## 2. Protect your encryption keys *** CRM encrypts and hashes sensitive fields before it stores them. The security of this data depends entirely on how you manage your keys. * **Generate unique keys** for `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY` with `openssl rand -hex 32`. * **Store keys in a secret manager** (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, or equivalent). Never hardcode them in configuration files, source code, or version control. * **Plan for key rotation.** If a key is compromised, generate a new one and re-encrypt all affected data. This is a manual process, so design your operational runbooks for it. * **Use Kubernetes Secrets** in production. Reference existing secrets via `useExistingSecret` and `existingSecretName` in your Helm values instead of storing keys inline. For the full list of protected fields and encryption strategies, see [CRM data security](/en/midaz/crm/crm-data-security). ## 3. Never store sensitive data in metadata *** The `metadata` object in CRM entities is **not encrypted**. CRM stores it in plain text for non-sensitive, auxiliary information only. Do not use metadata for: * Personal identification numbers (CPF, SSN, passport) * Financial account details * Contact information (email, phone) * Any data subject to LGPD, GDPR, or similar regulations If you need to store a sensitive attribute that is not in the [protected fields list](/en/midaz/crm/crm-data-security#protected-fields), contact your Lerian representative to discuss options. ## 4. Do not expose CRM directly on edge layers *** CRM runs inside the Midaz ledger and exposes an internal API. If you expose it directly through API gateways, load balancers, or frontend applications, you increase your attack surface. You also bypass application-level access controls. Instead: * Route CRM traffic through your **backend services** or an internal API layer. * Use [Access Manager](/en/platform/access-manager/access-manager) to enforce authentication and authorization if you need fine-grained control. * Restrict network access to the ledger pods with Kubernetes NetworkPolicies or your cloud provider's security groups. ## 5. Use soft delete as the default *** CRM supports both **soft delete** and **hard delete**: * **Soft delete** (default): CRM marks the record with a `deletedAt` timestamp. The record drops out of standard queries but stays in the database for audit and recovery. * **Hard delete**: CRM requests removal of the record, subject to your deployment's retention and compliance rules. Where legal, regulatory, audit, or record-keeping obligations require retention, CRM does not guarantee physical removal. For most cases, soft delete is the safer choice. It preserves audit trails and lets you recover data you delete by mistake. Reserve hard delete for cases where regulations demand complete removal (for example, GDPR right-to-erasure requests). Confirm your retention and legal-hold policy before you run a hard delete. The GDPR right to erasure (Article 17) is not absolute. Where legal, regulatory, audit, or record-keeping obligations require data retention, they can take precedence and restrict erasure. ## 6. Validate data before sending it to CRM *** CRM acts as a **neutral, persistent data layer**. It does not enforce business rules, validate document formats, or check KYC compliance. Data integrity is your responsibility. Before you create or update a record: * Validate document formats (CPF, CNPJ, passport numbers) on your application layer. * Make sure the `ledgerId` and `accountId` values point to real entities in Midaz. * Sanitize inputs so you do not store malformed or inconsistent data. ## 7. Keep CRM and Midaz versions aligned *** CRM ships inside the Midaz ledger binary, so it shares the Midaz version. Before you upgrade: * Check the [version compatibility table](/en/platform/plugins/midaz-version-compatibility) to confirm your target version. * Test the upgrade in a staging environment before you apply it to production. * Back up your MongoDB data and Helm values before any major upgrade. For upgrade procedures, see the [Helm upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide). ## 8. Monitor database health and performance *** CRM uses MongoDB for data storage. In production: * **Monitor connection pool usage.** The default `MONGO_CRM_MAX_POOL_SIZE` is 1000. Adjust it for your traffic patterns and replica count. * **Set up alerts** for MongoDB disk usage, replication lag, and connection saturation. * **Enable backups.** Whether you use the bundled Bitnami MongoDB or an external instance, run automated backups and test them regularly. * **Enable OpenTelemetry** (`ENABLE_TELEMETRY: true`) to collect traces and metrics from CRM. Integrate with your observability stack for end-to-end visibility. ## 9. Review the security recommendations *** CRM handles personal and sensitive data. Beyond CRM-specific practices, make sure your deployment follows the platform-wide [Security recommendations](/en/midaz/security-recommendations), which cover: * Network segmentation and Zero Trust Architecture * TLS 1.2+ enforcement for all communications * IAM and RBAC configuration * Incident response planning * Patch management and vulnerability scanning # Core Concepts Source: https://docs.lerian.studio/en/midaz/crm/crm-core-concepts-overview Meet the two CRM entities behind every Midaz account: Holders that carry customer identity and Alias Accounts that link them to Ledger accounts. The CRM core concepts define the entities that connect real-world identities to your Midaz ledger accounts. They provide the business context that the ledger's transactional layer doesn't manage. ## Content *** Here, you'll find: * [**Holders**](/en/midaz/crm/holders): The core entity for an individual (`NATURAL_PERSON`) or an organization (`LEGAL_PERSON`) behind a ledger account. A Holder stores identity attributes such as name, document number, contact details, and addresses. * [**Alias Accounts**](/en/midaz/crm/alias-accounts): The link between a Holder and a specific ledger account. Alias Accounts let one Holder own accounts in different ledgers. Each one keeps banking details and metadata in one place. # CRM data security Source: https://docs.lerian.studio/en/midaz/crm/crm-data-security Secure CRM personal data with HTTPS/TLS in transit, field-level encryption and hashing at rest, and controls aligned with GDPR and LGPD requirements. ### Why does this matter? Every regulation, from GDPR to LGPD, shares one principle: you use personal data only for the purpose the user agreed to. The **CRM** is part of the Midaz ledger. It handles transactional and non-transactional flows, so it must protect the sensitive data these flows carry. Although Lerian doesn't offer cybersecurity services, we follow strict information security practices in all software we ship. ## Security responsibilities *** As an on-premise technology provider, Lerian does not oversee or enforce our clients' cybersecurity policies. We respect the trust-based relationship each institution has with its end users. We assume that each client follows the LGPD and any other data protection regulation that applies to its region or industry. Still, Lerian delivers technology that follows strong, market-aligned security practices. These practices protect sensitive CRM data as follows: ### In transit **TLS over HTTPS** protects all data that you exchange with the CRM. The CRM encrypts sensitive fields *before* it writes them to the database. This applies to every request that creates or updates a holder or an instrument. ### At rest The CRM runs on-premise, so disk and volume encryption is the client's responsibility. We strongly recommend it. It adds a robust layer of protection. ## How we protect data *** The CRM uses different protection methods. The method depends on the type of data and its intended use. * **Encryption** protects values that the system might need to read again, such as names or contact information. * **Hashing** protects values that the system never shows again but must still match, such as identifiers for filtering or lookup. The CRM protects sensitive data as soon as the data reaches it. The CRM never stores or processes sensitive data in raw form. ### Encryption and hashing strategies The CRM combines several cryptographic techniques that follow industry best practices. These techniques protect sensitive data and still support the application. #### Encryption The system must read or show some fields again, such as personal names or emails. The CRM encrypts each of these values with strong, symmetric encryption. Even if someone reads the database directly, the original values stay unreadable without proper authorization. Encryption also adds a random component, so identical values never produce the same encrypted output. #### Hashing The CRM hashes certain fields to allow secure filtering without exposure of the original value. It stores these hashes in a separate internal structure for fast, secure lookups. In some cases, the CRM hashes a field only to meet an internal database constraint, even when the field is not searchable. ### Key management The CRM uses encryption keys and hashing keys. The deployment team must generate, store, and manage these keys securely. Keys must follow strong cryptographic standards. Never hardcode a key or expose it in source code or version control. Protect your keys with a dedicated secret manager or secure storage. If an attacker compromises a key, rotate it and re-encrypt or re-hash the affected data. ### Protected fields The CRM protects the fields listed below. You do not need to know how this protection works. Do not enter sensitive data in any field that this list does not include. Never store sensitive information in the `metadata` object. #### Holder * `name` * `document` * `contact.primaryEmail` * `contact.secondaryEmail` * `contact.mobilePhone` * `contact.otherPhone` * `naturalPerson.motherName` * `naturalPerson.fatherName` * `legalPerson.representative.name` * `legalPerson.representative.document` * `legalPerson.representative.email` #### Instrument * `document` * `bankingDetails.account` * `bankingDetails.iban` * `regulatoryFields.participantDocument` * `relatedParties.document` ## Best practices & recommendations *** Data encryption in the **CRM** adds a strong security layer. Even if an attacker compromises the database, the sensitive data stays inaccessible in its original form. The CRM decrypts data only with a valid key, or through authenticated and authorized service calls. Key security is therefore critical. If a key leaks, the application cannot protect your data on its own. To keep your environment safe, we strongly recommend: * **Do not expose the Midaz API** directly on edge layers such as gateways or frontend apps. * **Never store sensitive data in metadata fields.** The CRM does not encrypt them. * **Enforce governance** over access to the production environment and related tools. * **Use secure key managers** with strict access controls to safeguard your secrets. * **Encrypt disks or volumes** to protect data at rest. If an attacker compromises a key, or if you must rotate it, **you must generate a new key and re-encrypt the affected data**. Use the CRM's available services for this. Data security is a shared responsibility. The **CRM** gives you the building blocks. Make sure your deployment uses them well. # Getting started with CRM Source: https://docs.lerian.studio/en/midaz/crm/crm-getting-started Follow this guide to register Holders in CRM and link them to ledger accounts with Instruments through the Midaz REST API. This guide shows how to create and manage **Holders** — the customers or companies behind your accounts. It also links each holder to a ledger account with an **Instrument**. By the end, you have a holder registered in CRM and linked to a ledger account. ## Prerequisites *** Before you begin, make sure you meet the following requirements: * You completed the [Midaz setup](/en/midaz/midaz-setup) guide and all services run. * At least one **Organization**, **Ledger**, and **Account** already exist, as created in the [Midaz Getting Started](/en/midaz/midaz-getting-started) guide. * The Midaz ledger serves the holder and instrument endpoints. In Midaz v4, CRM is part of the ledger binary, so it needs no separate service or port. Replace the placeholder IDs in the examples below with the real IDs from your environment. ## CRM components *** The CRM (Customer Relationship Management) component lets you register the people and companies behind your ledger accounts. It manages two core entities: * **Holders**: Individuals (`NATURAL_PERSON`) or companies (`LEGAL_PERSON`) that own accounts. * **Instruments**: The link between a holder and a specific ledger account, with optional banking details. This model lets one holder own many accounts across different ledgers. It keeps identity and contact information in one place. ## Step 1 — Create a holder *** A **Holder** represents a person or company in your system. Create holders for individuals (`NATURAL_PERSON`) or companies (`LEGAL_PERSON`). Send a `POST` request with the holder's type, name, document, contact, and address. For the full request and response schema, see [Create a holder](/en/reference/midaz/create-a-holder). ```bash theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/holders \ -H "Content-Type: application/json" \ -d '{ "type": "NATURAL_PERSON", "name": "Jane Smith", "document": "12345678900", "contact": { "primaryEmail": "jane.smith@example.com", "mobilePhone": "+15551234567" }, "addresses": { "primary": { "line1": "123 Main Street", "line2": "Apt 4B", "city": "New York", "state": "NY", "zipCode": "10001", "country": "US", "description": "Home address" } }, "naturalPerson": { "favoriteName": "Jane", "birthDate": "1990-05-15", "nationality": "American" }, "metadata": { "segment": "premium", "source": "onboarding" } }' ``` To register a company instead of an individual, set the type to `LEGAL_PERSON`: ```bash theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/holders \ -H "Content-Type: application/json" \ -d '{ "type": "LEGAL_PERSON", "name": "Acme Corp Ltd", "document": "12345678000199", "contact": { "primaryEmail": "finance@acmecorp.com", "mobilePhone": "+15559876543" }, "legalPerson": { "tradeName": "Acme Corp", "activity": "Financial services", "type": "Limited Liability", "foundingDate": "2015-03-20", "size": "Medium", "status": "Active", "representative": { "name": "Bob Johnson", "document": "98765432100", "email": "bob@acmecorp.com", "role": "CFO" } } }' ``` Save the `holderId` from the response. You use it when you create instruments. ## Step 2 — Link a holder to an account *** Once the holder exists, link it to a ledger account with an **Instrument**. An instrument connects a holder to one account inside a ledger, with optional banking details. For the full request and response schema, see [Create an instrument](/en/reference/midaz/create-an-instrument). ```bash theme={null} curl -X POST http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id}/instruments \ -H "Content-Type: application/json" \ -d '{ "ledgerId": "{ledger_id}", "accountId": "{account_id}", "bankingDetails": { "branch": "0001", "account": "123450", "type": "CACC", "openingDate": "2025-01-15", "countryCode": "US", "bankId": "12345" }, "metadata": { "isPrimary": "true" } }' ``` ## Step 3 — Query and update your data *** Once holders and instruments exist, you can retrieve, list, and update them. | Operation | Endpoint | API reference | | ------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Retrieve a holder | `GET /v1/organizations/{organization_id}/holders/{holder_id}` | [Retrieve a holder](/en/reference/midaz/retrieve-a-holder) | | List all holders | `GET /v1/organizations/{organization_id}/holders?limit=10&page=1` | [List holders](/en/reference/midaz/list-holders) | | List instruments for a holder | `GET /v1/organizations/{organization_id}/instruments?holder_id={holder_id}` | [List instruments](/en/reference/midaz/list-instruments) | | Retrieve a specific instrument | `GET /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}` | [Retrieve an instrument](/en/reference/midaz/retrieve-an-instrument) | | Update a holder | `PATCH /v1/organizations/{organization_id}/holders/{holder_id}` | [Update a holder](/en/reference/midaz/update-a-holder) | ```bash theme={null} curl -X PATCH http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id} \ -H "Content-Type: application/json" \ -d '{ "contact": { "primaryEmail": "jane.new-email@example.com", "mobilePhone": "+15559999999" }, "metadata": { "segment": "vip", "source": "onboarding" } }' ``` The update changes only the fields you send. All other fields stay unchanged. ## Step 4 — Clean up *** To remove resources, delete instruments first, then holders. | Operation | Endpoint | API reference | | -------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Delete an instrument | `DELETE /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}` | [Delete an instrument](/en/reference/midaz/delete-an-instrument) | | Delete a holder | `DELETE /v1/organizations/{organization_id}/holders/{holder_id}` | [Delete a holder](/en/reference/midaz/delete-a-holder) | Delete an instrument: ```bash theme={null} curl -X DELETE http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} ``` Delete a holder: ```bash theme={null} curl -X DELETE http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id} ``` ## Summary *** In this guide, you: 1. Created a **Holder** to register an individual or company in CRM. 2. Created an **Instrument** to link the holder to a ledger account. 3. Queried and updated CRM data. 4. Removed instruments and holders when no longer needed. ## Next steps *** Explore advanced filtering, metadata queries, and all available endpoints. Manage holders through a graphical interface. # Migrating from standalone CRM Source: https://docs.lerian.studio/en/midaz/crm/crm-migration-guide Migrate from the standalone plugin-crm chart to the CRM component embedded in Midaz v3.5.0+ and Helm v5.x, with architecture and configuration steps. Starting with **Midaz v3.5.0** and **Helm Chart v5.x**, CRM is no longer deployed as a standalone plugin. It is now integrated directly into the Midaz monorepo and Helm chart as an embedded component. This guide explains how to migrate from the standalone `plugin-crm` deployment to the integrated CRM. If you are starting a **new Midaz deployment** (v5.x+), you do not need this guide. Simply enable CRM in your Helm values as described in [Deploy Midaz using Helm](/en/platform/helm/midaz/midaz-installation). The integrated CRM is now the only supported deployment model. ## What changed *** The CRM plugin was originally maintained as a **separate codebase** with its own release cycle and deployed independently through a dedicated Helm chart (`plugin-crm`) in the `midaz-plugins` namespace. Starting with **Midaz v3.5.0-beta.12** (December 2025), CRM was incorporated into the Midaz monorepo under `components/crm/`. Its deployment was then consolidated into the main Midaz Helm chart beginning in **v5.x**. ### Architecture comparison | Aspect | Standalone (v4.x and earlier) | Integrated (v5.x+) | | :--------------- | :---------------------------------- | :--------------------------------------- | | **Source code** | Separate repository | `components/crm/` in the Midaz monorepo | | **Helm chart** | `plugin-crm` (dedicated chart) | Part of the `midaz` chart | | **Namespace** | `midaz-plugins` | `midaz` | | **Versioning** | Independent release cycle | Matches Midaz core version | | **MongoDB** | Own connection configuration | Shared MongoDB with other Midaz services | | **Installation** | `helm install plugin-crm oci://...` | `crm.enabled: true` in Midaz values | | **Port** | 4003 | 4003 (unchanged) | ### API changes The CRM API remains **fully backward-compatible**. All endpoints available in the standalone version continue to work the same way in the integrated deployment. | Resource | Endpoints | Status | | :------------------- | :----------------------------------------------------- | :-------------------------- | | **Holders** | POST, GET (list), GET (by ID), PATCH, DELETE | Unchanged | | **Aliases** | POST, GET (list by holder), GET (by ID), PATCH, DELETE | Unchanged | | **Aliases (global)** | GET `/v1/aliases` (list across all holders) | Unchanged | | **Related Parties** | DELETE | Added in integrated version | The `GET /v1/aliases` endpoint allows you to list aliases across all holders with advanced filtering. Filters include `holder_id`, `account_id`, `ledger_id`, `document`, banking details, regulatory fields, and related party attributes. This endpoint complements the holder-scoped endpoints under `/v1/holders/{holder_id}/aliases`. The endpoint `DELETE /v1/holders/{holder_id}/aliases/{alias_id}/related-parties/{related_party_id}` was introduced with the integrated CRM. If you previously needed to remove related parties individually, this operation is now supported directly. In earlier versions, this required updating the alias payload without the related party. ### What stays the same * **API contract**: All existing endpoints, request and response schemas, and behaviors remain unchanged. * **Database**: MongoDB continues to be the storage backend. * **Authentication**: Access Manager integration works the same way (`PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_HOST`). * **Encryption keys**: `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY` are still required. * **Default port**: CRM continues to run on port 4003. ## Pre-migration checklist *** Before starting the migration, confirm the following: **Identify your current standalone version** ```bash Shell theme={null} helm list -n midaz-plugins ``` Record the `plugin-crm` chart version and app version. **Backup your current Helm values** ```bash Shell theme={null} helm get values plugin-crm -n midaz-plugins > plugin-crm-values-backup.yaml ``` **Backup your MongoDB data** ```bash Shell theme={null} # Find the MongoDB pod in the midaz-plugins namespace kubectl get pods -n midaz-plugins -l app=mongodb # Export the CRM database kubectl exec -n midaz-plugins -- \ mongodump --db crm --archive=/tmp/crm-backup.archive # Copy the backup locally kubectl cp midaz-plugins/:/tmp/crm-backup.archive ./crm-backup.archive ``` **Verify your Midaz chart is v5.x or later** ```bash Shell theme={null} helm list -n midaz ``` If you are running v4.x or earlier, upgrade Midaz first using the [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrade-guide) guide. **Schedule a maintenance window** CRM will be temporarily unavailable during migration. Plan for a short downtime window. ## Migration steps *** ### Step 1 — Enable CRM in the Midaz chart Add the CRM configuration to your Midaz Helm values: ```yaml values.yaml theme={null} crm: enabled: true configmap: MONGO_HOST: "midaz-mongodb.midaz.svc.cluster.local." MONGO_NAME: "crm" MONGO_USER: "midaz" MONGO_PORT: "27017" secrets: MONGO_PASSWORD: "" LCRYPTO_HASH_SECRET_KEY: "" LCRYPTO_ENCRYPT_SECRET_KEY: "" ``` Use the **same encryption keys** (`LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY`) used in the standalone deployment. Different keys will make existing encrypted data unreadable. If you use external secrets: ```yaml values.yaml theme={null} crm: enabled: true useExistingSecret: true existingSecretName: "crm-secrets" ``` ### Step 2 — Migrate your MongoDB data If your standalone CRM used its own MongoDB instance, restore the data into the Midaz-managed MongoDB. ```bash Shell theme={null} # Copy the backup to the Midaz MongoDB pod kubectl cp ./crm-backup.archive midaz/:/tmp/crm-backup.archive # Restore the CRM database kubectl exec -n midaz -- \ mongorestore --archive=/tmp/crm-backup.archive --db crm --drop ``` If the standalone and integrated CRM already use the **same MongoDB instance**, you can skip this step. Just confirm that `MONGO_HOST` and `MONGO_NAME` match. ### Step 3 — Deploy the integrated CRM ```bash Shell theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version 5.x.x \ -n midaz \ -f your-values.yaml ``` ### Step 4 — Verify the integrated CRM is running ```bash Shell theme={null} # Check the pod status kubectl get pods -n midaz -l app=crm # Check the logs kubectl logs -n midaz deployment/midaz-crm # Test the health endpoint (run port-forward in a separate terminal) kubectl port-forward -n midaz svc/midaz-crm 4003:4003 & curl http://localhost:4003/health ``` ### Step 5 — Validate your data Run a quick validation to confirm that your data migrated correctly. ```bash Shell theme={null} # List holders through the integrated CRM curl -H "X-Organization-Id: " \ http://localhost:4003/v1/holders # Verify a specific holder curl -H "X-Organization-Id: " \ http://localhost:4003/v1/holders/ ``` Compare the results with the standalone deployment. ### Step 6 — Update DNS and ingress Update your DNS records or ingress rules to point to the CRM service in the `midaz` namespace. ```yaml ingress-example.yaml theme={null} # The CRM service name changes from the standalone chart # Old: plugin-crm.midaz-plugins.svc.cluster.local # New: midaz-crm.midaz.svc.cluster.local ``` ### Step 7 — Remove the standalone CRM After confirming that everything works as expected, remove the standalone deployment. ```bash Shell theme={null} helm uninstall plugin-crm -n midaz-plugins ``` Only uninstall the standalone CRM **after validating the integrated deployment**. This operation removes the standalone deployment and its resources. If the `midaz-plugins` namespace is no longer needed, you can optionally remove it. ```bash Shell theme={null} kubectl delete namespace midaz-plugins ``` ## Access Manager permissions *** Access Manager permissions remain unchanged after the migration. The application name continues to be `plugin-crm`, and permissions apply to the `holders` and `aliases` resources. | Permission | Description | Resources | Allowed Methods | | :---------------------------------- | :------------- | :--------------- | :----------------------- | | `plugin-crm-editor-permission` | Full access | holders, aliases | post, get, patch, delete | | `plugin-crm-contributor-permission` | Read and write | holders, aliases | post, get, patch | | `plugin-crm-viewer-permission` | Read only | holders, aliases | get | No updates are required in your Access Manager configuration. ## Rollback procedure *** If you need to revert to the standalone CRM: Disable CRM in the Midaz chart: ```yaml values.yaml theme={null} crm: enabled: false ``` Re-deploy the Midaz chart: ```bash Shell theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version 5.x.x -n midaz -f your-values.yaml ``` Re-install the standalone `plugin-crm`: ```bash Shell theme={null} helm install plugin-crm oci://registry-1.docker.io/lerianstudio/plugin-crm \ --version \ -n midaz-plugins \ -f plugin-crm-values-backup.yaml ``` Restore DNS or ingress to point back to the standalone service. ## Troubleshooting *** **CRM pod fails to start with encryption errors** * Confirm that `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY` exactly match the values used in the standalone deployment. **Data appears empty after migration** * Verify that `MONGO_HOST` and `MONGO_NAME` point to the correct MongoDB instance and database. * If you ran `mongorestore`, confirm the restore completed successfully. **Access Manager rejects requests** * The application name in Access Manager should still be `plugin-crm`. No change is required. **Port conflict on 4003** * Running both the standalone and integrated CRM simultaneously will create a conflict on port 4003. If you need to run both during testing, temporarily change the port in the Midaz values: ```yaml values.yaml theme={null} crm: configmap: SERVER_PORT: "4013" ``` ## Next steps *** Learn about CRM features and design principles. Start working with holders and alias accounts. Complete Helm upgrade guide covering all migration paths. Full deployment reference for Midaz v5.x. # What is CRM? Source: https://docs.lerian.studio/en/midaz/crm/crm-overview Discover how the optional CRM layer enriches Midaz Ledger accounts with customer identity, contact details, and business context outside the ledger. CRM is **not part of the ledger's transactional domain**. It adds business attributes to ledger accounts. It does not change the ledger's logic, consistency, or performance. CRM is where user metadata lives. It does not hold balances, transfers, or fees. It records who is behind an account and what you need to run your business. ## Versioned with Midaz *** CRM ships inside the Midaz ledger and needs **no separate license**. Midaz is source-available. CRM shares the Midaz version. For example, Midaz `v3.5.0` uses CRM `v3.5.0`. CRM runs in the same binary as the ledger. It is always available. You choose whether to store holder data and call its endpoints. **Test CRM locally** You can run CRM with the Midaz stack locally, without Kubernetes, using our [plugins-docker-compose repository](https://github.com/LerianStudio/plugins-docker-compose). ## Why use the CRM? *** Midaz ledger accounts are generic by design. They are transaction-first and domain-agnostic. CRM adds meaning to those accounts and links each one to a user profile and a business context. You do not have to use CRM. You can adopt it later, only when your use case needs customer or business metadata beyond the ledger. ### Example use cases * **Banking (individuals and businesses)**: CRM stores identifiers like CPF or CNPJ, an address, contact details, or account numbers. Midaz runs the same transaction logic for both customer types. * **Supply chains (supplier, factory, retailer)**: CRM tracks metadata like supplier IDs, warehouse locations, or contract details. This data does not belong in the transaction, but it drives business decisions. CRM ships inside the Midaz ledger, so there is no separate service to deploy. Use it only if your product or regulatory context needs customer or business metadata. ## Entities *** CRM has two core entities: * **[Holder](/en/midaz/crm/holders)** – The main entity in CRM. It represents the persona for a Midaz ledger account. It stores identity attributes and individual traits. * **[Alias Account](/en/midaz/crm/alias-accounts)** – The business context for a [Ledger Account](/en/midaz/accounts) in **Midaz Ledger**. It holds financial details such as bank account information. *Figure 1* shows how the CRM entities connect to Midaz. How CRM entities, the Holder and the Alias Account, connect to accounts in the Midaz ledger ## Design principles *** CRM focuses on: * **Separation of concerns**: It keeps user profile data out of transaction logic. * **System performance**: It adds no user-level orchestration inside Midaz core services. * **Simpler deployment**: CRM ships inside the ledger binary, with no separate service to run. * **Clear domain boundaries**: It evolves with Midaz without coupling to ledger internals. CRM does not validate ledger-level rules or compliance processes such as KYC. It is a neutral, persistent data layer. ## API behavior *** CRM runs inside the Midaz ledger binary. It exposes a registry API for non-transactional data linked to Midaz accounts. * It does **not** enforce validations such as user status, risk profile, or document verification. * It is **agnostic to the asset, ledger, or grouping logic** used in Midaz accounts. * It gives consistent access to holder metadata without loading the transaction layer. ## Security & compliance *** Lerian ships Midaz and its components for on-premise deployment. **You keep control** of your infrastructure, compliance policies, and user-data lifecycle. CRM ships with Midaz and follows the same security standards as the rest of the platform: * **Data in transit**: Midaz encrypts it with industry-standard protocols. * **Data at rest**: Midaz protects it with encryption and fine-grained access control. You own encryption key management. Use a secret-management service to store, rotate, and control access to keys. You define and enforce your own policies for LGPD, GDPR, and other regional data-protection rules. CRM provides the structure. Review the [Security recommendations](/en/midaz/security-recommendations) before you deploy any component into your environment. Apply security best practices to every component — **Access Manager**, **Fees Engine**, **Pix**, and **CRM**. Secure network boundaries, manage secrets, apply patches, and enforce strict access control. ## Data deletion strategies *** CRM gives you full control over how it removes data. It offers two options: * **Soft delete** (default): CRM flags the record as deleted but keeps it in the database. Use it to keep an audit trail or to recover data later. * **Hard delete**: Where your policy permits removal, you can request deletion of the data. Physical removal depends on your retention, compliance, and legal-hold configuration. Your policies define the behavior. CRM adapts to your compliance requirements. ## Access control *** To restrict who can access your CRM data, enable our [Access Manager](/en/platform/access-manager/access-manager). ### Enabling Access Manager First, install **Access Manager**. Then open the Midaz ledger `.env` file and set these environment variables: ```text Text theme={null} ## AUTH --- PLUGIN_AUTH_HOST=http://plugin-auth:4000 PLUGIN_AUTH_ENABLED=true ``` After you enable it, CRM enforces access permissions from your authentication setup. Access Manager is available under the Enterprise model. To evaluate it, [contact our team](https://lerian.studio/contact). Ready to start? See [Getting started with CRM](/en/midaz/crm/crm-getting-started) for a step-by-step guide. ## Next steps *** Understand the key entities managed by CRM: Holders and Alias Accounts. Browse endpoints for holders, alias accounts, and related parties. Learn how to register holders and link them to ledger accounts. # Using CRM Source: https://docs.lerian.studio/en/midaz/crm/crm-using-overview Manage Holders and Instruments in CRM day to day through the REST API or the visual Midaz Console interface. This section covers CRM configuration and day-to-day operations through the REST API or the Midaz Console. ## Content *** Here, you'll find: * [**Using CRM**](/en/midaz/crm/crm-getting-started): Manage Holders and Instruments through the REST API. * [**Using CRM with Midaz Console**](/en/midaz/crm/using-crm-with-midaz-console): Manage Holders and Instruments through the Midaz Console graphical interface. # Holders Source: https://docs.lerian.studio/en/midaz/crm/holders Model the individuals or organizations behind Midaz accounts as Holders, storing names, documents, contacts, and compliance details as Natural or Legal Person. A **Holder** is the core entity in CRM. It represents a real-world individual or organization behind a Midaz ledger account. A Holder stores identity attributes such as the name, document number, contact details, and addresses. [CRM](/en/midaz/crm/crm-overview) manages Holders. Holders do not belong to the ledger's transactional domain. They enrich ledger accounts with business data. They do not affect the ledger's logic, consistency, or performance. Each Holder is one of two types: **Natural Person** (individual) or **Legal Person** (company/organization). To create and manage holders step by step, see [Getting started with CRM](/en/midaz/crm/crm-getting-started). ## Holder types *** The holder type controls which fields are available. It also sets how the platform treats the entity. You choose the type at creation. You **cannot change it later**. ### Natural Person The Natural Person type represents an individual customer. It supports personal attributes such as the name, gender, birth date, civil status, nationality, and family information. Use this type for: * Individual bank account holders * Personal wallet owners * Freelancers or sole proprietors ### Legal Person The Legal Person type represents a company or organization. It supports business attributes such as the trade name, activity type, founding date, company size, and legal representative details. Use this type for: * Corporate treasury accounts * Business partners and suppliers * Institutional clients Choose the holder type carefully at creation. You cannot change it later. To use a different type, create a new Holder. ## Holder fields *** ### Core fields | Field | Type | Required | Description | | :------------- | :--------- | :--------------- | :---------------------------------------------------------------------------------------------------------------- | | **id** | `uuid` | System-generated | Unique identifier of the Holder. | | **type** | `enum` | Yes | `NATURAL_PERSON` or `LEGAL_PERSON`. | | **name** | `string` | Yes | Full name of the individual or legal name of the company. | | **document** | `string` | Yes | Identification document number (e.g., CPF, CNPJ, passport). | | **externalId** | `string` | No | Optional identifier to map the Holder to an external system. | | **metadata** | `object` | No | Key-value pairs for custom, non-sensitive data. Keys are limited to 100 characters and values to 2000 characters. | | **createdAt** | `datetime` | System-generated | Timestamp of creation (RFC 3339). | | **updatedAt** | `datetime` | System-generated | Timestamp of last update (RFC 3339). | | **deletedAt** | `datetime` | System-generated | Timestamp of soft deletion, if applicable (RFC 3339). | ### Address fields The `addresses` object supports up to three addresses: `primary`, `additional1`, and `additional2`. Each address contains: | Field | Type | Required | Description | | :-------------- | :------- | :------- | :----------------------------------------------------------------------------------------------- | | **line1** | `string` | Yes | Primary address line (street, number). Max 256 characters. | | **line2** | `string` | No | Secondary address line (apartment, suite). Max 256 characters. | | **zipCode** | `string` | Yes | Postal or ZIP code. Max 20 characters. | | **city** | `string` | Yes | City name. Max 100 characters. | | **state** | `string` | Yes | State or province code. Max 100 characters. | | **country** | `string` | Yes | ISO 3166-1 alpha-2 country code (e.g., `US`, `BR`). | | **description** | `string` | No | A descriptive label for the address (for example, Home, Office, or Billing). Max 100 characters. | ### Contact fields | Field | Type | Required | Description | | :----------------- | :------- | :------- | :-------------------------------------------------------------- | | **primaryEmail** | `string` | No | Primary email address. | | **secondaryEmail** | `string` | No | Secondary email address. | | **mobilePhone** | `string` | No | Mobile phone number with country code (e.g., `+5511999999999`). | | **otherPhone** | `string` | No | Alternative phone number. | ### Natural Person fields Available only when `type` is `NATURAL_PERSON`. | Field | Type | Required | Description | | :--------------- | :------- | :------- | :-------------------------------------------------------------------- | | **favoriteName** | `string` | No | Preferred name or nickname. | | **socialName** | `string` | No | Social name (name the person identifies with). | | **gender** | `string` | No | Gender identity. | | **birthDate** | `string` | No | Date of birth in `YYYY-MM-DD` format. | | **civilStatus** | `string` | No | Marital status (e.g., Single, Married, Divorced). | | **nationality** | `string` | No | Nationality (e.g., Brazilian, American). | | **motherName** | `string` | No | Mother's full name. | | **fatherName** | `string` | No | Father's full name. | | **status** | `string` | No | Lifecycle status (e.g., `Active`, `Inactive`, `Suspended`, `Closed`). | ### Legal Person fields Available only when `type` is `LEGAL_PERSON`. | Field | Type | Required | Description | | :--------------- | :------- | :------- | :-------------------------------------------------------------------- | | **tradeName** | `string` | No | Trade or doing-business-as name. | | **activity** | `string` | No | Primary business activity. | | **type** | `string` | No | Legal structure (e.g., LLC, Corporation). | | **foundingDate** | `string` | No | Date the company was founded in `YYYY-MM-DD` format. | | **size** | `string` | No | Company size classification (e.g., Small, Medium, Large). | | **status** | `string` | No | Lifecycle status (e.g., `Active`, `Inactive`, `Suspended`, `Closed`). | #### Representative The `representative` object within `legalPerson` stores the details of the company's legal representative: | Field | Type | Required | Description | | :----------- | :------- | :------- | :---------------------------------------- | | **name** | `string` | No | Full name of the legal representative. | | **document** | `string` | No | Document number of the representative. | | **email** | `string` | No | Email address of the representative. | | **role** | `string` | No | Role within the company (e.g., CEO, CFO). | ## Data security *** Midaz encrypts several Holder fields at rest, including the `name`, the `document`, and the contact fields. This protects the data even if someone gains access to the storage. Never store sensitive information in the `metadata` object. Midaz does not encrypt metadata and stores it in plain text. For the full list of protected fields and encryption strategies, see [CRM data security](/en/midaz/crm/crm-data-security). ## Managing Holders *** ### Via API Use the CRM API to manage Holders programmatically: * [Create a Holder](/en/reference/midaz/crm/create-holder) — Register a new Holder in the system. * [List Holders](/en/reference/midaz/crm/list-holders) — View all Holders with pagination and filters. * [Retrieve a Holder](/en/reference/midaz/crm/retrieve-holder) — Get the details of a specific Holder. * [Update a Holder](/en/reference/midaz/crm/update-holder) — Edit an existing Holder's data. * [Delete a Holder](/en/reference/midaz/crm/delete-holder) — Soft-delete or permanently remove a Holder. Every CRM API request includes the organization ID in the URL path, for example `/organizations/{organization_id}/holders`. If you enable [Access Manager](/en/platform/access-manager/access-manager), add an `Authorization` header with a Bearer token. ### Via Lerian Console You can manage Holders through the **Holders** page in the Midaz Module of [Lerian Console](/en/platform/console/about-lerian-console). The console provides a visual interface to create, view, edit, and delete Holders without code. [**Learn more in the Managing Holders guide.**](/en/midaz/console/managing-crm-holders) ## Next steps *** Learn how Alias Accounts link Holders to Midaz ledger accounts. Follow the step-by-step guide to register Holders and create Alias Accounts. # CRM in Lerian Console Source: https://docs.lerian.studio/en/midaz/crm/using-crm-with-midaz-console Manage CRM Holders and Alias Accounts visually inside the Midaz Module of Lerian Console, without writing API calls or curl requests to the REST endpoints. The **CRM** interface is part of the **Midaz Module** in [Lerian Console](/en/platform/console/about-lerian-console). Use it to manage core entities such as Holders and Alias Accounts. It gives you a clear view of your CRM data. ## Accessing CRM features *** CRM features are in the **Midaz Module** of Lerian Console. To open them: 1. Open **Lerian Console** and go to the **Midaz Module** 2. In the left-side menu, find the CRM entities in the **Accounts** section: * **Holders** - Manage individuals and companies * **Alias Accounts** - Link holders to ledger accounts ### Available actions From the Midaz Module, you can: Create and manage individuals or companies associated with your accounts Link holders to Midaz ledger accounts with business context # Database migrations Source: https://docs.lerian.studio/en/midaz/database-migrations Run Midaz Ledger and Tracer PostgreSQL schema migrations with dedicated runner images so applications boot against an already-migrated database. Midaz applies PostgreSQL schema migrations through **dedicated migration-runner images** — one for the Ledger, one for Tracer — decoupled from the application binaries. The apps no longer migrate at startup: they boot against a schema that has already been migrated by the runner. This model is what runs under `make up` in local development and under the Helm chart in Kubernetes. If you use `make up`, no manual step is required — the compose stack gates the app on the migration runner. ## Why a dedicated runner *** The old in-process migrator ran on every app boot from the working directory. Under a hardened pod security context (`distroless:nonroot`, `runAsUser: 1000`, `readOnlyRootFilesystem: true`, `capabilities: drop [ALL]`), that path failed with `permission denied` and crash-looped the pod. The dedicated runner sidesteps that entirely: * Migrations run **once, as a separate step**, before the app starts. * The app image no longer bundles migration SQL and never writes to the schema. * The runner image itself runs cleanly under the same hardened security context (see [Security posture](#security-posture)). ## Runner images *** Two images are published per release: | Image | Applies | Databases | | ------------------------- | ------------------------------------------ | -------------------------------- | | `midaz-ledger-migrations` | Ledger onboarding + transaction migrations | Two: `onboarding`, `transaction` | | `midaz-tracer-migrations` | Tracer schema | One | Both images share the same shape: * Base: [`migrate/migrate`](https://github.com/golang-migrate/migrate) `v4.19.1` (pinned). * Runs as `USER 65532:65532` — non-root, UID-agnostic. * POSIX shell entrypoint that assembles a DSN from environment variables, runs `migrate ... up`, and exits. * Writes nothing to disk — migration progress is tracked in the `schema_migrations` table in Postgres, so `readOnlyRootFilesystem: true` is safe. The Ledger runner applies **onboarding first, then transaction**, in a single invocation. The Tracer runner applies its single database in one pass. ## Environment contract *** Each entrypoint accepts either a prebuilt URL override or the individual `DB_*` variables. **The URL override takes precedence when set.** ### Ledger runner | Database | URL override | Assembled from | | ----------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | onboarding | `ONBOARDING_DATABASE_URL` | `DB_ONBOARDING_HOST`, `DB_ONBOARDING_PORT` (default `5432`), `DB_ONBOARDING_USER`, `DB_ONBOARDING_PASSWORD`, `DB_ONBOARDING_NAME`, `DB_ONBOARDING_SSLMODE` (default `disable`) | | transaction | `TRANSACTION_DATABASE_URL` | `DB_TRANSACTION_*` (same shape) | ### Tracer runner | URL override | Assembled from | | -------------- | ------------------------------------------------------------------------------------------------------------- | | `DATABASE_URL` | `DB_HOST`, `DB_PORT` (default `5432`), `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `DB_SSL_MODE` (default `disable`) | ### DSN shape When the entrypoint assembles the DSN from `DB_*` variables, it produces: ```text theme={null} postgres://:@:/?sslmode= ``` The password is percent-encoded so URI-reserved characters (`% @ : / ? # & + space [ ]`) do not break the DSN. `%` is encoded first so already-inserted escapes are not double-encoded. The entrypoint logs only phase markers (`applying onboarding migrations`, `applying transaction migrations`, `applying tracer migrations`, `migrations complete`). It never prints credentials or the assembled DSN. ## Local development *** ### `make up` From the repository root, `make up`: 1. Starts infra and waits until Postgres is healthy. 2. Starts the one-shot `ledger-migrate` and `tracer-migrate` compose services. 3. Starts each app service, which `depends_on` its migrate service with `condition: service_completed_successfully`. The app never starts against an un-migrated database. ### Host `make` targets If you want to run migrations directly against a local database — for example when developing a new migration — each component exposes host-side Make targets that use the pinned `golang-migrate` CLI and read connection settings from `.env`. Ledger (`components/ledger`): | Target | Effect | | -------------------------- | ----------------------------------------------- | | `make migrate` | Applies onboarding then transaction (aggregate) | | `make migrate-onboarding` | Applies onboarding only | | `make migrate-transaction` | Applies transaction only | | `make migrate-down` | Rolls back both databases | Tracer (`components/tracer`): | Target | Effect | | ---------------------- | ------------------------------------------------------- | | `make migrate` | Applies the tracer schema | | `make migrate-down` | Rolls back the tracer schema | | `make migrate-version` | Prints the current schema version | | `make migrate-force` | Forces the schema version (recovery from a dirty state) | ### Example: run the Ledger runner manually You rarely need to invoke the runner image by hand — the compose gate does it for you. When you do, point the runner at your databases via `DB_*` (or `*_DATABASE_URL`) and run it once: ```bash theme={null} docker run --rm \ --network=infra-network \ -e DB_ONBOARDING_HOST=postgres \ -e DB_ONBOARDING_PORT=5432 \ -e DB_ONBOARDING_USER=midaz \ -e DB_ONBOARDING_PASSWORD='s3cret!' \ -e DB_ONBOARDING_NAME=onboarding \ -e DB_ONBOARDING_SSLMODE=disable \ -e DB_TRANSACTION_HOST=postgres \ -e DB_TRANSACTION_PORT=5432 \ -e DB_TRANSACTION_USER=midaz \ -e DB_TRANSACTION_PASSWORD='s3cret!' \ -e DB_TRANSACTION_NAME=transaction \ -e DB_TRANSACTION_SSLMODE=disable \ lerianstudio/midaz-ledger-migrations: ``` The runner exits `0` on success (including the idempotent no-op case) and non-zero on failure. ## Kubernetes deployment *** In production, run each migration runner as a Kubernetes **Job** (typically an Argo CD PreSync Job or a Helm hook) that completes **before** the app rollout. The Helm chart wires this up for you; the sections below cover the properties you should keep in mind if you author your own manifests. For multi-tenant deployments, the runner is intentionally **tenant-agnostic**: it migrates the databases pointed at by its environment, once. Per-tenant fan-out is a deploy concern — run the Job once per tenant database, passing that tenant's `DB_*` (or URL override) values. ### Security posture The runner is designed for a hardened pod security context: * **Non-root** — the image sets `USER 65532:65532`. * **UID-agnostic** — migration files are root-owned and world-readable, so any UID can read them. * **`readOnlyRootFilesystem`-safe** — `migrate` writes no filesystem state. The same image runs cleanly under `runAsNonRoot: true`, `readOnlyRootFilesystem: true`, and `capabilities: drop [ALL]`. ### TLS The entrypoints honor `sslmode` from the environment (`DB_ONBOARDING_SSLMODE`, `DB_TRANSACTION_SSLMODE`, or `DB_SSL_MODE`), defaulting to `disable`. **Production deployments should set `require`** (or stricter). The shell runner does not reproduce the in-code TLS enforcement, error classification, or telemetry that the apps use for their own connections — TLS for migrations is controlled entirely through `sslmode`, in exchange for a minimal, dependency-free runner. ## Conventions and idempotency *** * **Paired up/down** — every migration has a `.up.sql` and a `.down.sql` file. * **Idempotent** — running `migrate up` when the schema is already at the latest version is a no-op. Re-running the runner after a partial or complete apply is safe. * **Pinned CLI** — the runner images and host `make` targets all pin `golang-migrate` `v4.19.1`. ## Upgrading from earlier versions *** If you were running a Midaz release where the app migrated on startup, no manual data migration is required — the on-disk schema is the same. To adopt the new model: 1. Pull the release that ships the runner images. 2. In Kubernetes, add the `midaz-ledger-migrations` and `midaz-tracer-migrations` Jobs to your deploy pipeline so they run before the app rollout. If you use the official Helm chart, this is done for you. 3. In local development, `make up` continues to work as before — the compose stack now gates the apps on the new one-shot migrate services automatically. ## Related pages *** * [Installing Midaz](/en/midaz/midaz-setup) — full local setup with `make up`. * [Updating Midaz](/en/midaz/updating-midaz) — upgrade flows for local and Helm. * [Deployment strategies](/en/midaz/deployment) — BYOC deployment overview. * [Tracer environment variables](/en/tracer/tracer-environment-variables) — full Tracer configuration reference. # Deployment strategies Source: https://docs.lerian.studio/en/midaz/deployment Choose the right deployment strategy for Midaz under BYOC — Kubernetes with Helm, Terraform foundations, and infrastructure prerequisites. This section covers deployment strategies for running Midaz in your own infrastructure under the **BYOC (Bring Your Own Cloud)** model. If you're using the **SaaS** model, Lerian manages deployment for you — skip ahead to [Getting started](/en/getting-started) to begin configuring your platform. Not sure which model fits? See [Deployment models](/en/deployment-models) for a full comparison of SaaS and BYOC. Set up your environment, meet the prerequisites, and follow our best practices to ensure a secure, scalable, and reliable setup. Before you deploy, check out the [Midaz Architecture Overview](/en/midaz/architecture). Understanding how the components work together will help you make informed decisions during setup. ### Content In this section, you will find: * [Midaz Terraform Foundation](/en/midaz/midaz-terraform-foundation): Ready-made Terraform examples for provisioning the cloud infrastructure Midaz runs on — AWS, GCP, or Azure. For Kubernetes deployment, configuration, and lifecycle management via Helm, see the dedicated [Helm documentation](/en/platform/helm/helm-overview) in the Platform section. ## Deployment strategies *** You manage the infrastructure in both Midaz modules, Community and Enterprise. We strongly recommend using Kubernetes with our Helm Charts. This approach simplifies deployment and scaling, especially if using the Enterprise edition, which includes support and modular microservices. That said, you can choose the deployment strategy that fits your needs. ### Deploying with Kubernetes We recommend our official Helm Charts. They offer a fast and flexible way to install Midaz in a Kubernetes environment. You can either plug Midaz into your existing databases and services or use the default components provided in the Helm chart. This setup adapts to different infrastructure needs and supports production-grade deployments. For a step-by-step guide, check the [Deploying using Helm](/en/platform/helm/midaz/midaz-installation) documentation. ### Self-managed deployment If you prefer full control, you can deploy Midaz manually. Midaz includes a Docker configuration, which makes local or small-scale setups straightforward. You can also adapt it to your own infrastructure, whether you're using a different orchestrator or running directly on virtual machines or bare metal. For instructions, check the [Installing Midaz](/en/midaz/midaz-setup) page. ## Platform requirements *** The default Helm configuration meets these minimums, except for RabbitMQ. Its bundled subchart installs server 3.13.6. Midaz targets RabbitMQ 4.x for development and tests, so that subchart runs below the target. If you bring your own infrastructure components, make sure you meet the minimum supported versions: | Service | Minimum Supported Version | Notes | | :------------- | :------------------------ | :-------------------------------------------------------------------- | | Kubernetes | 1.20+ | 1.30+ recommended | | Helm CLI | 3.8+ | Required for OCI registry | | PostgreSQL | 16.3 | Primary database | | MongoDB | 8.x | Document storage (Helm chart: bitnami/mongodb 16.4.0+) | | RabbitMQ | 4.x | Message broker (Helm chart: rabbitmq subchart 2.1.11 → server 3.13.6) | | Valkey | 8.x | In-memory cache (Redis fork) | | OTEL Collector | 2.2.1 | Optional, for observability | Midaz needs these minimum versions for a stable, performant platform. For the complete compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Best practices *** ### Access to the infrastructure * Use **least privilege** policies when connecting to databases. * Block direct access to databases by default. Only allow it in exceptional cases with approval. * Store secrets securely using tools like **HashiCorp Vault**. * Use dedicated tools for safe access to production data. ### Redundancy and high availability * Enable **replication and backups** for both Midaz and your databases. * Use **load balancers** to route traffic across services evenly. ### Network security * Run your services inside **VPCs** to isolate critical components. * Use **NAT gateways or proxies** for outbound connections. * Apply **security groups or firewall rules** to restrict access to required ports and IPs. ### Data persistence and storage * Use **replicated databases** to support high availability and recovery. * Choose **scalable storage solutions** that match your data growth. * Use **Valkey** (Redis fork) for caching and background message management. ### Observability * Integrate tools that monitor logs, traces, and metrics. * Midaz supports observability through **OpenTelemetry**, making detecting and resolving issues easier. ## Shared responsibilities *** Under the BYOC model, Lerian and your team share responsibility for running Midaz. For a detailed breakdown of who secures what — including how responsibilities differ between BYOC and SaaS — see the [Security](/en/midaz/security) page. For actionable guidance on securing your environment before deployment, check the [Security Recommendations](/en/midaz/security-recommendations) documentation. # FAQ Source: https://docs.lerian.studio/en/midaz/faq Answers to common Lerian and Midaz questions on API pagination limits, SaaS data isolation, tenant JWT scoping, and multi-organization platform configuration. ## Lerian APIs *** This section answers common questions about Lerian's APIs. It covers general behavior, configuration, and best practices across all services. Yes. The default maximum is **100** records per page. This limit keeps performance consistent and controls the data volume in each request. To raise it, set the `MAX_PAGINATION_LIMIT` environment variable in your deployment configuration. The API accepts larger page sizes after you restart the application. **Important**: A larger page size can slow response times, especially with large datasets. Test in staging before you change production. ## Multi-tenancy and SaaS *** These questions cover data isolation, tenant scoping, and how multi-tenancy works in Lerian deployments. Yes. Every tenant operates on a separate database. The platform resolves your tenant from the JWT on every request and routes it to your isolated database. There is no way to access another tenant's data through the API. Learn more about [multi-tenancy](/en/multi-tenancy). No. The JWT access token you receive during authentication carries your tenant context. The platform resolves it automatically. You do not need to include a tenant identifier in headers or request bodies. Yes. A tenant can contain multiple Organizations. Each Organization has its own Ledgers, accounts, and transactions. The platform scopes all of them to your tenant automatically. No. The API surface is identical: same endpoints, same payloads, same responses. Only one thing differs. SaaS requires authentication on every request, and your token scopes all operations to your tenant. ## Midaz *** These questions cover Organizations, Ledgers, Accounts, Transactions, and more in Midaz. ### Organizations No. Each Organization operates independently and does not communicate with others. No. Each license binds to one Organization. To support multiple Organizations, acquire a separate license for each. The same rule applies to Plugins. Yes. An Organization can have more than one Plugin. Yes. An Organization can manage multiple Ledgers. You can create a **Parent Organization** and a **Child Organization**. Each **Organization** keeps its own Ledger and operates independently. Transactions cannot move value directly between ledgers. You orchestrate the transfer with these steps: In the source ledger, create a transaction from the original account (**source**) to the asset's **external account** (distribute). This removes the value from the source ledger. In the destination ledger, **create a second transaction**. The **source** is now the asset's **external account**, and the destination is the receiving account (**distribute**). This pattern moves value between ledgers in different Organizations in a controlled way. ### Ledgers No. Ledgers do not communicate directly. Transfers between Ledgers require orchestration. You must orchestrate the process and move the amount through an External Account. This involves two steps: Ledger A -> External Account. External Account -> Ledger B. No. A single Ledger can support multiple Plugins. For example, one Ledger can handle both Exchange and Pix Plugins. ### Assets No. Each Asset links to a single Account. Each Asset also links to an External Account. Midaz creates that External Account automatically when you create the Asset. Midaz supports several Asset types: * *currency*: Traditional fiat currencies like BRL, USD, and EUR. * *fiat*: An alternative type for fiat currencies; like `currency`, the Asset code must follow ISO 4217. * *crypto*: Digital assets such as BTC, ETH, and other cryptocurrencies. * *commodities*: Tangible goods like gold, soybean, and oil. * *others*: Custom Assets, including loyalty points and tokenized securities. ### Portfolios A **Portfolio** groups accounts that belong to the same entity (**CPF/CNPJ**). For example, one CPF with two different `segment_id` values has two matching `account_id` values. You create a Portfolio for that CPF to link both accounts under one structure. This makes related accounts easier to find and manage. ### Accounts No. Each Account links to a single Asset. You cannot change this link. An External Account receives funds from outside the Ledger. It brings money into the system. Midaz creates an **External Account** automatically when you create an Asset. This External Account backs all transactions that move in and out of the Ledger. No. Each account (`account_id`) links to only one Segment (`segment_id`). No. You can create as many Accounts as you need. Midaz sets no limit on the number of Accounts. The balance top-up process works as follows: 1. When you create an Asset (for example, BRL) in the Midaz Ledger, Midaz also creates an External Account for that Asset. 2. This External Account mirrors the balances the institution holds outside Midaz. Those balances can sit in a PI account, a settlement account, a reserve account, or a traditional bank or payment account. 3. To deposit funds from outside the Midaz Ledger into a user account, follow these steps: * Create a transaction with the External Account as the source and the target accounts as the destination. * Midaz debits the External Account by the amount (so it becomes negative) and credits the destination accounts by the values in the transaction payload. ### Transactions A Transaction must have at least two Operations. For example, a transfer of R\$ 100 from Account A to Account B has two operations: * **Operation 1:** Debit R\$ 100 from Account A. * **Operation 2:** Credit R\$ 100 to Account B. Lerian gives clients several ways to access transaction receipts: 1. **Via APIs** — Retrieve transaction data through the APIs, then generate a visual receipt in any format you choose. 2. **With the Reporter** — Extract transaction data and create custom visual receipts. 3. **Through the Console** — Access transaction data directly in the Lerian Console. ### Entities The Entity (`entity_id`) accepts external IDs. Midaz enforces no validation on this field. You can use the IDs that already exist in your database and integrate them into your system. ### Idempotency Midaz treats the request as new every time. Retries can then create duplicate operations. No. Scope each key to a single operation and endpoint. Midaz uses only the TTL from the first request. A later change has no effect. Yes. For a completed request, Midaz returns the same result it stored from the first request. It also sets the `X-Idempotency-Replayed` header to `true`. The default TTL is **300 seconds** (5 minutes). Send the `X-TTL` header to set a custom value in seconds. ### Accounting in Midaz Midaz lets you mirror your organization's official **Chart of Accounts** in the platform. You configure two core features: * [Account Types](/en/midaz/accounts) — Create the logical categories from your chart, such as Assets, Liabilities, Revenue, and Expenses. Assign them to accounts in your ledger. When you enable the Account Types feature, the `type` field in the Accounts API becomes mandatory and must match a registered value. * [Accounting Routes](/en/midaz/transaction-routing-entities) — Use Operation Routes to validate each leg of a transaction. For example, a debit must come from an account of type `user_wallet`. Use Accounting Routes (the `transactionRoute` resource in the API) to define complete transaction patterns that match your accounting logic. Account Types and Accounting Routes together enforce your accounting rules at the ledger level. Midaz validates and categorizes every transaction against your Chart of Accounts. You do not hardcode rules in your business logic. ## Plugins *** Plugins extend Midaz with integration and process orchestration. They provide abstractions so you can focus on your business model instead of system logic outside your domain. The questions below cover how plugins work, how you deploy them, and the options available. Plugins are technologies that integrate into the Midaz ledger. They simplify process integration and orchestration. They provide abstractions so clients can focus on their business model. Clients do not build or manage system logic outside their domain. No. Plugins operate only with Midaz. They provide specific abstractions and orchestrate transactions based on the ledger structure. After you contract a plugin, Lerian provides and installs it in your infrastructure (on-premise model), next to your Midaz instance. Applications connect to each plugin according to its function. Lerian provides two types of plugins, grouped by origin: * **Native Plugins:** Lerian develops and integrates these plugins into the Midaz ledger. Lerian provides full support for them. * **Marketplace Plugins:** Lerian's partners create these plugins for specific market niches. Lerian helps integrate them into Midaz. The partners provide and support them directly. ## Fees Engine *** These questions cover Fees Engine. Fees Engine is a licensed Midaz capability that runs inside the unified ledger process. ### General Concepts Fees Engine is part of **Midaz**. It runs in the Midaz ledger process to calculate fees for financial transactions. Configure and deploy it with Midaz. Learn more in the [Fees Engine overview](/en/midaz/fees/fees-engine-overview). It works across three main domains: * **Fee Packages (`/v1/packages`):** defines charging rules per transaction (flat fee, percentage, or whichever is greater). * **Billing Packages (`/v1/billing-packages`):** defines periodic charges based on transaction volume or account maintenance. * **Calculation and Estimation (`/v1/fees` and `/v1/estimates`):** endpoints for calculating fees in real time or simulating before confirming. Fees Engine runs inside the Midaz ledger process. When a configured fee package applies, Midaz incorporates its fee calculations into the transaction. It uses ledger query use cases rather than an external HTTP connection to Midaz. Every request requires the `X-Organization-Id` header with your Organization ID in Midaz. This header scopes the request to an Organization. It is specific to Fees Engine, not a tenant identifier. The platform still resolves your tenant context automatically from the JWT. When the authentication plugin is active, you also send a Bearer token in the `Authorization` header. ``` X-Organization-Id: Authorization: Bearer ``` Fees Engine uses **MongoDB** for storage. Deletions follow the **soft-delete** pattern. Fees Engine does not remove records physically. It marks them with `deletedAt` instead. A deleted record does not appear in listings, but you can still audit it. You need **Midaz v3.6.0** or higher. Fees Engine depends on Transaction module APIs that Midaz added in v3.6.0. Earlier Midaz versions do not work with Fees Engine. ### Fee Packages A Fee Package (`Package`) is a set of charging rules under one `feeGroupLabel`. Each package links to an **Organization + Ledger**, and optionally to a **Segment**. A package can contain several fees (`Fee` objects), each with its own calculation logic. Learn more about [Fee Packages](/en/midaz/fees/using-fee-engine). Send a `POST /v1/packages` with the following body. See the [Create Package API reference](/en/reference/midaz/plugins/fees-engine/create-package) for full details. ```json theme={null} { "feeGroupLabel": "Digital Account Fees", "ledgerId": "ldg_abc123", "segmentId": "seg_xyz456", "minimumAmount": "100.00", "maximumAmount": "50000.00", "transactionRoute": "PIX", "enable": true, "waivedAccounts": ["exempt-account-1", "exempt-account-2"], "fees": { "admin_fee": { "feeLabel": "Administrative Fee", "calculationModel": { "applicationRule": "percentual", "calculations": [ { "type": "percentage", "value": "1.50" } ] }, "referenceAmount": "originalAmount", "priority": 1, "isDeductibleFrom": true, "creditAccount": "fee-revenue-account" } } } ``` Yes. Set the `enable` field to `false` when you create or update the package. Fees Engine skips a disabled package during fee calculation, even when the transaction context matches its scope. Fees Engine applies the package only to transactions whose value falls within the `[minimumAmount, maximumAmount]` range. If the transaction value falls outside this range, Fees Engine ignores the package. **Example:** A package with `minimumAmount: 100` and `maximumAmount: 5000` charges fees only on transactions between 100 and 5,000. If you do not set `maximumAmount`, the package can apply without an upper limit. Check the validation rules for your version. Yes. Set the `transactionRoute` field in the package. Fees Engine then considers the package only for transactions with that route, such as `"PIX"`, `"TED"`, or `"BOLETO"`. These are account aliases that the package **exempts** from fees. If the sender or recipient of a transaction is an account in `waivedAccounts`, Fees Engine does not apply the package fees to it. ```json theme={null} "waivedAccounts": ["vip-account", "employee-account"] ``` This package does not charge any transaction that comes from or goes to these accounts. Yes. The listing endpoints (`GET /v1/packages`, `GET /v1/billing-packages`) support the `limit` and `page` query parameters for pagination. ``` GET /v1/packages?limit=20&page=2 ``` ### Calculation Models The `applicationRule` field inside `calculationModel` defines how Fees Engine calculates the fee. See [Calculation Models](/en/midaz/fees/fee-engine-calculation) for full details. There are three options: | Rule | Description | | ----------------- | ------------------------------------------------------------------- | | `flatFee` | Fixed-amount fee | | `percentual` | Percentage fee based on the reference amount | | `maxBetweenTypes` | Calculates both flat and percentage; applies the **greater** result | Use exactly **1 calculation** of type `flat`: ```json theme={null} "calculationModel": { "applicationRule": "flatFee", "calculations": [ { "type": "flat", "value": "5.00" } ] } ``` This charges a fixed 5.00 regardless of the transaction amount. Use exactly **1 calculation** of type `percentage`: ```json theme={null} "calculationModel": { "applicationRule": "percentual", "calculations": [ { "type": "percentage", "value": "2.50" } ] } ``` This charges 2.5% of the transaction's reference amount. `maxBetweenTypes` requires **2 or more calculations** that combine `flat` and `percentage`. Fees Engine calculates both and applies the **greater result**. **Example:** Minimum fee of 3.00 or 1% of the value — whichever is greater: ```json theme={null} "calculationModel": { "applicationRule": "maxBetweenTypes", "calculations": [ { "type": "flat", "value": "3.00" }, { "type": "percentage", "value": "1.00" } ] } ``` For a 200 transaction: 1% = 2.00 vs. 3.00 flat → charges **3.00**. For a 500 transaction: 1% = 5.00 vs. 3.00 flat → charges **5.00**. Yes. You can include any combination of `flat` and `percentage`. Fees Engine evaluates all of them and applies the greatest. Note that `flatFee` and `percentual` require exactly 1 calculation. Only `maxBetweenTypes` accepts 2 or more. ### Important Fields The `referenceAmount` defines **which value** Fees Engine calculates the fee on: * `originalAmount`: the original transaction value, **before** any fees. * `afterFeesAmount`: the transaction value **after** higher-priority fees apply. The fee with `priority: 1` runs first, so it **must** use `originalAmount`. No prior fees exist to consider. When `isDeductibleFrom: true`, Fees Engine deducts the fee from the amount the sender sends. The recipient receives the discounted amount, and the sender pays extra to cover the charge. When `false`, Fees Engine charges the fee **separately**. The sender sends the full amount, and Fees Engine debits the fee apart. **Constraints:** * `isDeductibleFrom: true` requires `referenceAmount: originalAmount` * If the type is `percentage`: the value cannot exceed 100 * If the type is `flat`: the value cannot exceed the package's `minimumAmount` The `priority` defines the **execution order** of fees within a package. Fees Engine runs lower values first. * `priority: 1` → executed first (must use `originalAmount`) * `priority: 2` → executed after, can use `afterFeesAmount` Use priorities to chain fees. For example, run an administrative fee on the original amount. Then run an IOF fee on the amount after the administrative fee. It is the alias of the ledger account that receives the fee revenue. Each fee can have a different `creditAccount`. This helps when different fees belong to different cost centers. ```json theme={null} "creditAccount": "admin-fee-revenue-account" ``` These fields define the routes of the **accounting legs** that the fee charge generates. They are optional. They let you track the origin and destination of fee movements in the ledger. ### Billing Packages Billing Packages are **periodic** charge packages, independent of per-transaction fee calculation. See [Billing Package examples](/en/midaz/fees/billing-package-examples) for use cases. There are two types: * **`volume`:** charges based on the **number of transactions** in a period, with tiered pricing. * **`maintenance`:** charges a **fixed fee per account** in a given scope. Use volume billing to charge clients by the **number of processed transactions**. This is a common model for payment platforms with volume-based pricing. You define price tiers that apply as volume grows. ```json theme={null} { "type": "volume", "eventFilter": { "transactionRoute": "PIX", "status": "approved" }, "pricingModel": "tiered", "tiers": [ { "minQuantity": 0, "maxQuantity": 1000, "unitPrice": "0.50" }, { "minQuantity": 1001, "unitPrice": "0.30" } ], "assetCode": "BRL", "debitAccountAlias": "client-account", "creditAccountAlias": "volume-revenue-account" } ``` The last tier must be **unbounded** (no `maxQuantity`). There must be no gaps or overlaps between tiers. Use maintenance billing to charge a **fixed periodic fee per account**. For example, charge a monthly fee per active account. You specify the scope (`segmentId`, `portfolioId`, or `aliases`) and the fee amount. ```json theme={null} { "type": "maintenance", "feeAmount": "15.00", "assetCode": "BRL", "maintenanceCreditAccount": "maintenance-revenue-account", "accountTarget": { "segmentId": "seg_premium_clients" } } ``` `accountTarget` must have exactly **one** of the three fields: `segmentId`, `portfolioId`, or `aliases` (maximum 100 aliases). Tiers define the **unit price per tier** as volume increases. The rules are: 1. Must be **contiguous** — no gaps between tiers (`minQuantity` of the next = `maxQuantity` of the previous + 1). 2. Must not **overlap**. 3. The **last tier must be unbounded** (no `maxQuantity`). **Example of correct tiers:** ```json theme={null} "tiers": [ { "minQuantity": 0, "maxQuantity": 500, "unitPrice": "0.80" }, { "minQuantity": 501, "maxQuantity": 2000, "unitPrice": "0.60" }, { "minQuantity": 2001, "unitPrice": "0.40" } ] ``` It is a **free allowance**. Fees Engine does not charge a set number of transactions before tiers apply. This helps pricing models with a minimum included volume. **Example:** `freeQuota: 100` means Fees Engine does not charge the first 100 transactions in the period. These are discount tiers for volume billing. They reduce the charged amount based on extra criteria. They complement the logic of the main `tiers`. It defines **how Fees Engine counts transactions**: * `perRoute`: counts transactions by route (e.g., total approved Pix). * `perAccount`: counts transactions per individual account. ### Fee Calculation and Estimation | Endpoint | When to use | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /v1/fees` | Calculate the **actual** fee of an ongoing transaction. See the [Calculate Fees API reference](/en/reference/midaz/plugins/fees-engine/calculate-fees). The system automatically finds applicable packages based on ledger, segment, route, and amount. | | `POST /v1/estimates` | **Simulate** the fee of a specific package before confirming the transaction. Useful for showing end users the cost before execution. See the [Simulate Fees API reference](/en/reference/midaz/plugins/fees-engine/simulate-fees). | The endpoint receives the transaction data. The system **automatically finds** the applicable packages. It considers: * `ledgerId` — required * `segmentId` — optional * `transactionRoute` — optional * Transaction value — compared with the package's `minimumAmount`/`maximumAmount` Fees Engine calculates and returns fees from all matching packages. The `/v1/estimates` endpoint simulates the fee of a **specific package** (`packageId`). You do not need an actual transaction. It works well for: * Show the estimated cost to the user before confirmation. * Test configurations of newly created packages. * Build fee simulators in your product. Yes. `/v1/estimates` is a read-only endpoint. It does not alter state or register transactions. It is safe to use in UX flows to show the cost before confirmation. After you configure Billing Packages, call this endpoint: ``` POST /v1/billing/calculate ``` This endpoint processes the configured rules and generates the charges for the period. See the [Calculate Billing API reference](/en/reference/midaz/plugins/fees-engine/calculate-billing). ### Common Errors The fee with `priority: 1` must have `referenceAmount: "originalAmount"`. It is the first fee to run, so no prior fees exist to base the calculation on. **Fix:** ```json theme={null} { "priority": 1, "referenceAmount": "originalAmount" } ``` Fees with `isDeductibleFrom: true` can only use `referenceAmount: "originalAmount"`. Update the field: ```json theme={null} { "isDeductibleFrom": true, "referenceAmount": "originalAmount" } ``` When `isDeductibleFrom: true` and the type is `flat`, the fee value cannot exceed the package's `minimumAmount`. This prevents a fee that is larger than the minimum transaction value. **Example:** If `minimumAmount: 100`, the flat fee cannot exceed 100. This error occurs when `isDeductibleFrom: true`, the type is `percentage`, and the value is above 100. A deductible percentage fee of 100% would zero out the transaction. Values above 100 are invalid. Volume billing tiers must cover all ranges without gaps. Check that the `minQuantity` of each tier is exactly `maxQuantity + 1` of the previous tier. ```json theme={null} // ❌ Wrong — gap between 500 and 600 { "minQuantity": 0, "maxQuantity": 500, "unitPrice": "0.80" }, { "minQuantity": 600, "unitPrice": "0.40" } // ✅ Correct { "minQuantity": 0, "maxQuantity": 500, "unitPrice": "0.80" }, { "minQuantity": 501, "unitPrice": "0.40" } ``` The last tier in volume billing must have **no upper limit** (no `maxQuantity`). This keeps a price on transactions above the highest defined range. In maintenance billing, the `accountTarget` field accepts only **one** of the three options. Do not combine fields: ```json theme={null} // ❌ Wrong "accountTarget": { "segmentId": "seg_abc", "portfolioId": "port_xyz" } // ✅ Correct "accountTarget": { "segmentId": "seg_abc" } ``` Yes. The `aliases` field accepts a maximum of **100 aliases** per maintenance Billing Package. The `applicationRule: "flatFee"` accepts exactly 1 calculation, and it must be of type `flat`. Do not use `percentage` with `flatFee`. ```json theme={null} // ✅ Correct "calculationModel": { "applicationRule": "flatFee", "calculations": [{ "type": "flat", "value": "10.00" }] } ``` Like `flatFee`, the `applicationRule: "percentual"` accepts exactly 1 calculation of type `percentage`. `maxBetweenTypes` requires at least 2 calculations to work — it needs values to compare. Provide at least one `flat` and one `percentage`. Diagnostic checklist — see also [Best Practices](/en/midaz/fees/fees-engine-best-practices): * **`enable`:** is the package active (`enable: true`)? * **`ledgerId`:** is the package linked to the correct ledger? * **`minimumAmount` / `maximumAmount`:** is the transaction value within the range? * **`transactionRoute`:** if the package has `transactionRoute`, does the transaction use the same route? * **`segmentId`:** if the package is scoped to a segment, does the account belong to it? * **`waivedAccounts`:** is the account listed as exempt? Fees Engine soft-deletes records. It marks them with `deletedAt` and does not remove them from the database. The API does not expose restoration endpoints by default. Contact the Lerian team if you need to recover a deleted record. For a full list of error codes, see the [Error Codes reference](/en/reference/midaz/plugins/fees-engine/fee-engine-error-list). # Fees Engine billing package examples Source: https://docs.lerian.studio/en/midaz/fees/billing-package-examples Walk through end-to-end billing package examples for boleto tiered pricing, volume discounts, and account maintenance charges with full JSON configuration. This page is business-oriented — it focuses on *what* each billing model solves and *how* to configure it. For field-level details, see the [Billing Packages overview](/en/midaz/fees/fees-engine-overview#billing-packages) and the [API reference](/en/reference/midaz/plugins/fees-engine/create-billing-package). ## Volume billing: boleto issuance with tiered pricing *** ### The business need A fintech offers boleto issuance to its business clients. Pricing is volume-based: the more boletos a client issues each month, the lower the unit cost. The first 50 boletos each month are free. Clients that issue 1,000 or more boletos receive an extra 5% discount. At 3,000 boletos or more, the discount increases to 10%. ### Pricing structure | Range | Unit price | | --------- | ---------- | | 1–500 | R\$ 1.20 | | 501–2,000 | R\$ 0.80 | | 2,001+ | R\$ 0.45 | ### Package configuration ```json theme={null} POST /v1/billing-packages Headers: X-Organization-Id: org_01HZ... Body: { "label": "Boleto Issuance — Tiered", "description": "Monthly volume billing for boleto issuance with progressive tiers", "ledgerId": "ldg_01HZ...", "type": "volume", "enable": true, "eventFilter": { "transactionRoute": "boleto-issuance", "status": "APPROVED" }, "pricingModel": "tiered", "tiers": [ { "minQuantity": 1, "maxQuantity": 500, "unitPrice": "1.20" }, { "minQuantity": 501, "maxQuantity": 2000, "unitPrice": "0.80" }, { "minQuantity": 2001, "maxQuantity": null, "unitPrice": "0.45" } ], "freeQuota": 50, "discountTiers": [ { "minQuantity": 1000, "discountPercentage": "5.00" }, { "minQuantity": 3000, "discountPercentage": "10.00" } ], "countMode": "perRoute", "assetCode": "BRL", "debitAccountAlias": "client-operating", "creditAccountAlias": "fees-boleto-revenue" } ``` ### How the calculation works At month-end, the orchestrator calls `POST /v1/billing/calculate` with the billing period. The engine counts approved boleto transactions on the route, subtracts the free quota, applies tiered pricing, and applies the matching discount. Tiered pricing is **volume pricing**: the engine picks the single tier the billable count lands in and charges every billable unit at that tier's price. It does not bill each tier's slice separately. **Example result** — a client that issued 1,800 boletos in March: | Step | Detail | Amount | | ------------- | ------------------------ | ---------------- | | Total issued | 1,800 boletos | — | | Free quota | 50 exempt | — | | Billable | 1,750 boletos | — | | Matching tier | 501–2,000 → R\$ 0.80 | — | | Gross | 1,750 × R\$ 0.80 | R\$ 1,400.00 | | Discount | 5% (total count ≥ 1,000) | −R\$ 70.00 | | **Net total** | — | **R\$ 1,330.00** | The engine returns a transaction payload that debits `client-operating` by R\$ 1,330.00 and credits `fees-boleto-revenue`. ## Maintenance billing: monthly account fee (*PF*) *** ### The business need A digital bank charges a fixed monthly maintenance fee for active personal (PF) accounts. The fee is R\$ 9.90 per account. The engine keeps only accounts whose status code is `active` and excludes every other status. You do not filter them by hand. Midaz organizes accounts by segment. The segment `seg_pf` groups all personal accounts. ### Package configuration ```json theme={null} POST /v1/billing-packages Headers: X-Organization-Id: org_01HZ... Body: { "label": "PF Account Maintenance", "description": "Monthly maintenance fee for active personal accounts", "ledgerId": "ldg_01HZ...", "type": "maintenance", "enable": true, "feeAmount": "9.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pf", "accountTarget": { "segmentId": "seg_pf_01HZ..." } } ``` ### How the calculation works The engine resolves all accounts in the PF segment and filters for active status. It generates one N:1 transaction: it debits R\$ 9.90 from each active account and sends the full total to `fees-maintenance-pf`. **Example result** — 12,000 active PF accounts: | Detail | Value | | :-------------------------------- | :------------------- | | Active accounts | 12,000 | | Fee per account | R\$ 9.90 | | Transaction entries (source) | 12,000 debit entries | | Transaction entries (destination) | 1 credit entry | | **Total revenue** | **R\$ 118,800.00** | ## Volume billing: fixed-price Pix with segment exemption *** ### The business need A fintech charges R\$ 0.10 per Pix sent — flat rate, no tiers. Premium-tier customers pay no fee. Instead of listing each premium account, you configure the exemption at the segment level. ### Package configuration ```json theme={null} POST /v1/billing-packages Headers: X-Organization-Id: org_01HZ... Body: { "label": "Pix Send — Standard", "description": "Flat-rate billing per Pix sent", "ledgerId": "ldg_01HZ...", "type": "volume", "enable": true, "eventFilter": { "transactionRoute": "pix-send", "status": "APPROVED" }, "pricingModel": "fixed", "tiers": [ { "minQuantity": 1, "maxQuantity": null, "unitPrice": "0.10" } ], "freeQuota": 0, "discountTiers": [], "countMode": "perRoute", "assetCode": "BRL", "debitAccountAlias": "client-wallet", "creditAccountAlias": "fees-pix-revenue" } ``` ### Segment exemption To exempt premium accounts, configure the fee package for the `pix-send` route. Add a segment reference to `waivedAccounts`: ```json theme={null} { "waivedAccounts": [ "segment:seg_premium_01HZ..." ] } ``` All accounts in the premium segment are exempt automatically. When accounts join or leave the segment in Midaz, the change takes effect on the next calculation. You do not update the package. ### How the calculation works **Example result** — 5,000 Pix transactions from standard accounts: | Detail | Value | | :------------- | :------------- | | Total Pix sent | 5,000 | | Unit price | R\$ 0.10 | | **Total** | **R\$ 500.00** | Premium accounts show zero charges in the billing results. ## Maintenance billing: PJ portfolios with different rates *** ### The business need A financial institution manages multiple business (PJ) account portfolios: PME (small and medium enterprises) and Corporate. Each portfolio has a different monthly maintenance fee. The institution wants to calculate both in a single billing run. ### Package configuration **PME portfolio — R\$ 29.90/month:** ```json theme={null} POST /v1/billing-packages Headers: X-Organization-Id: org_01HZ... Body: { "label": "PJ Maintenance — PME", "description": "Monthly maintenance for PME business accounts", "ledgerId": "ldg_01HZ...", "type": "maintenance", "enable": true, "feeAmount": "29.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pj", "accountTarget": { "portfolioId": "port_pme_01HZ..." } } ``` **Corporate portfolio — R\$ 89.90/month:** ```json theme={null} POST /v1/billing-packages Headers: X-Organization-Id: org_01HZ... Body: { "label": "PJ Maintenance — Corporate", "description": "Monthly maintenance for Corporate business accounts", "ledgerId": "ldg_01HZ...", "type": "maintenance", "enable": true, "feeAmount": "89.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pj", "accountTarget": { "portfolioId": "port_corp_01HZ..." } } ``` ### How the calculation works A single call to `POST /v1/billing/calculate` with `"type": "maintenance"` processes both packages. The response includes one result per package and a consolidated summary. **Example result** — 500 PME accounts + 50 Corporate accounts: | Portfolio | Active accounts | Fee | Total | | ---------------- | --------------- | --------- | ----------------- | | PME | 500 | R\$ 29.90 | R\$ 14,950.00 | | Corporate | 50 | R\$ 89.90 | R\$ 4,495.00 | | **Consolidated** | **550** | — | **R\$ 19,445.00** | Both results credit the same `fees-maintenance-pj` account, keeping revenue consolidated. The orchestrator sends each transaction payload to Midaz independently. ## Next steps *** API reference for creating volume and maintenance billing packages. Trigger billing calculations for a period and retrieve transaction payloads. Detailed mechanics of tiered pricing, free quotas, and maintenance billing. Operational guidance for billing packages in production. # Connect the Fees Engine to Midaz Source: https://docs.lerian.studio/en/midaz/fees/connecting-fees-to-midaz Configure the embedded Fees Engine with the Midaz ledger. Fees runs inside the unified Midaz ledger process. Configure it in the ledger deployment; it is neither a separate service nor a plugin. ## Configuration *** Configure its MongoDB storage with `MONGO_FEES_*` settings in the Midaz deployment. `DEFAULT_CURRENCY` is optional and defaults to `USD` when unset. See [Midaz environment variables](/en/midaz/midaz-environment-variables) for ledger configuration. ## See it in Lerian *** * The full integration journey across the pieces: [Building a complete core banking](/en/building-a-complete-core-banking) * What the Fees Engine does: [What is Fees Engine?](/en/midaz/fees/fees-engine-overview) * Deploy the stack: [Helm charts](/en/platform/helm/helm-overview) # Billing Runs Source: https://docs.lerian.studio/en/midaz/fees/console/billing-runs Use Billing Runs in the Midaz Console to preview volume and maintenance fees for a daily, weekly, or monthly period before any charges are posted to accounts. A **Billing Run** previews the fees for a given period, based on your configured packages. It does not charge anything — it only calculates a preview. Use it to check amounts, reconcile expected revenue, and review the results before you apply any charge. Enable the Fees Engine in [Midaz module settings](/en/platform/console/plugin-settings) before you use this page. ## Key concepts *** * **Preview, not a charge** — A Billing Run shows an extract of what it *would* bill for the selected period. It posts nothing until you act on the result. * **Period type** — The length of the period: **daily**, **weekly**, or **monthly**. With a reference date, it sets the date range that the run evaluates. * **Reference date** — The date that sets the period range. A monthly run covers the full month, a weekly run the ISO week, a daily run that day. You can also choose a **custom range**. * **Package type** — Which packages the run includes: * **Volume** — fees on the number of transactions. * **Maintenance** — a fixed fee per account. * **All** — both Volume and Maintenance packages. * **Scope to specific packages** — Limit the run to selected packages instead of every package of the chosen type. * **Extract** — The result of the run. It has a summary and one preview line per package. The summary shows the total results, the Volume and Maintenance totals, and a net total. ## Accessing Billing Runs *** In the Midaz Module sidebar, expand **Fees Engine**. Choose **Billing Runs** to open the run page. Pick the **period type** and **reference date**, or a custom range. Choose the **package type**. To narrow the run, select specific packages. Run the calculation to see the extract. Review the summary and the per-package results. If the period has no fees to bill, the preview shows an empty state. ## Where to go next *** Create, edit, and delete the packages that Billing Runs evaluate. Understand how the Fees Engine, packages, and rules fit together. # Creating a Fee Package Source: https://docs.lerian.studio/en/midaz/fees/console/fe-creating-a-package Build a new Fee Package in the Midaz Module by configuring basic info, amount ranges, flat or percentage fee rules, waivers, and priorities from the Console UI. A Fee Package groups fee rules and defines when they apply to Transactions. ## Before you start *** * Enable the **Fees Engine** in [Midaz module settings](/en/platform/console/plugin-settings). * You need an existing Ledger and at least one Account that can receive fee amounts. * If the package should apply only to a specific route or segment, create those records first. ## Create the Fee Package *** Go to the **Fee Packages** page and click the **New Package** button. The **New Fee Package** form will open with multiple sections to configure. Check the field guide for more information about the fields in the form. Fill in the **Basic Information** section Configure the **Amount Range** section Transactions outside this range will not have fees applied from this package. Click the **Next** button. Select one of the options in the **Add Fee** section: * `Flat Fee` - Fixed amount regardless of transaction value. * `Percentage` - Percentage of the transaction amount. * `Max Between Types` - Higher value between flat and percentage. Add Fee information. (Optional) You can add another fee rule if needed. To do so, select a new fee type and fill the information. Click the **Next** button. (Optional) Configure **Account Waivers** section: * Add account aliases that should be exempt from fees in this package * Click **Add** to add each waived account Click the **Next** button. Review the information and confirm that everything is correct. Click the **Create Package** button. If successful, the Console shows a confirmation message and opens the Fee Packages list. ## Field guide *** ### Basic Information | Field | Description | Example | | ----------------- | ------------------------------------------------------------------ | ---------------------------------- | | Fee Package Name | Required package name. Use a name that describes the charge model. | `Standard Transfer Fee` | | Description | An explanation of when and why the package applies. | `Fixed fee for standard transfers` | | Transaction Route | Optional route filter for the package. | `Pix Transfer Route` | | Segment | Optional segment filter for targeted fees. | `Retail Customers` | | Minimum Amount | Lowest transaction amount eligible for the package. | `10.00` | | Maximum Amount | Highest transaction amount eligible for the package. | `500.00` | ### Fee Rules Every fee inside a package has a **Priority** (1 = applied first). The fee with priority `1` must use `Original Amount` as Reference Amount. #### Flat Fee A fixed amount applied to the Transaction regardless of its value. | Field | Description | Example | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | Amount | The fixed amount charged for this fee. Must be a positive number. | `5.00` | | Fee Name | Unique identifier for this fee, used as the JSON object key. **Must start with a letter or underscore** and contain only letters, numbers, or `_`. | `taxaAdm` | | Reference Amount | Base used by the calculation: `Original Amount` (transaction value as sent) or `After Fees Amount` (value after previous fees in the package). | `Original Amount` | | Credit Account ID | Alias of the account that receives the fee amount. | `@fees_transfers` | | Route From | Optional. Name or UUID of the source route for this fee operation. | `payments_in` | | Route To | Optional. Name or UUID of the destination route for this fee operation. | `fees_revenue` | | Deductible from transaction? | When enabled, the fee is deducted from the transaction amount (recipient pays); when disabled, the fee is added on top (sender pays). | Off | When you enable **Deductible from transaction?**, the form forces **Reference Amount** to `Original Amount` and disables the `After Fees Amount` option. #### Percentage Fee A percentage of the Transaction amount. | Field | Description | Example | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | Percentage | Percentage rate applied to the reference amount. Must be greater than `0` and less than or equal to `100`. | `2.5` | | Fee Name | Unique identifier for this fee, used as the JSON object key. Must start with a letter or underscore and contain only letters, numbers, or `_`. | `processingFee` | | Reference Amount | `Original Amount` to charge on the transaction value as sent, or `After Fees Amount` to charge on the value after previous fees in the package. | `Original Amount` | | Credit Account ID | Alias of the account that receives the fee amount. | `@fees_revenue` | | Route From | Optional. Name or UUID of the source route for this fee operation. | `payments_in` | | Route To | Optional. Name or UUID of the destination route for this fee operation. | `fees_revenue` | | Deductible from transaction? | Enable so the fee is deducted from the transaction amount; disable to add the fee on top of the transaction. | `Disabled` | #### Max Between Types Calculates a flat amount **and** a percentage, then applies the highest result. The form requires both calculations. | Field | Description | Example | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | Flat Fee (Calculations) | First calculation row. Fixed amount candidate. Must be a positive number. | `1.00` | | Percentage Fee (Calculations) | Second calculation row. Percentage candidate; must be greater than `0` and less than or equal to `100`. | `2.0` | | Fee Name | Unique identifier for this fee, used as the JSON object key. Must start with a letter or underscore and contain only letters, numbers, or `_`. | `guaranteeFee` | | Reference Amount | Base used to calculate the percentage candidate before the comparison. | `Original Amount` | | Credit Account | Alias of the account that receives the resulting (highest) fee amount. | `@fees_guarantee` | | Route From | Optional. Name or UUID of the source route for this fee operation. | `payments_in` | | Route To | Optional. Name or UUID of the destination route for this fee operation. | `fees_revenue` | | Deductible from transaction? | When enabled, the higher of the two results is deducted from the transaction amount; when disabled, it is added on top. | `Disabled` | The **Max Between Types** form always requires exactly two calculations: one of type `Flat Fee` and one of type `Percentage Fee`. The Console computes both and applies the highest. ### Account Waivers | Field | Description | Example | | ------------- | --------------------------------------------- | ------------------------- | | Account Alias | The alias of the account that will be waived. | `@customer_john_checking` | ## Important notes *** ### Fee priority ordering When a package contains multiple fees, the Console applies them in priority order (lowest number first). This matters when using `After Fees Amount` as a reference, since earlier fees affect the calculation base for later fees. **Example**: * Fee A (Priority 1): 1% on original amount * Fee B (Priority 2): 0.5% on after fees amount On a \$100 transaction: * Fee A = \$1.00 (1% of \$100) * Fee B = \$0.495 (0.5% of \$99) ### Amount range considerations * Ranges should not overlap between packages for the same transaction route * Use `0.01` as minimum for packages that should apply to all transactions * Use a high maximum (e.g., `999999999.99`) for no upper limit * The range includes Transactions exactly at the min or max values ### Waived accounts behavior * Waived accounts bypass all fees in the package, not just specific fees * Use account aliases (not IDs) when adding waived accounts * Waivers apply when the waived account is the **source** of the transaction ## What to do next *** After creating the Fee Package, run a test calculation in [Running a Fee Estimation](/en/midaz/fees/console/fe-fee-simulation) before using it with live Transactions. [Create Package endpoint](/en/reference/midaz/plugins/fees-engine/create-package) # Deleting a Fee Package Source: https://docs.lerian.studio/en/midaz/fees/console/fe-deleting-a-package Delete a Fee Package from the Midaz Module when it is no longer needed, keeping in mind data retention, audit, and compliance rules for regulated deployments. If you only need to stop a package temporarily, disable it instead of deleting it. A deleted Fee Package no longer appears in active listings. Your deployment's data retention and compliance policies still apply to the record. In regulated environments, your deployment can keep the record for audit, regulatory, or reporting purposes. ## Delete a package On the **Fee Packages** page, click the three dots () in the **Actions** column for the package you want to delete. Select **Delete**. Review the confirmation dialog. Click **Confirm** to finalize the deletion. # Editing a Fee Package Source: https://docs.lerian.studio/en/midaz/fees/console/fe-editing-a-package Edit a saved Fee Package in the Midaz Console to adjust its name, amount range, fee rules, or waived accounts without recalculating fees already applied to past Transactions. Edit a Fee Package to change its name, amount range, fee rules, or waived accounts. Your changes apply to new fee calculations. Midaz does not recalculate fees already applied to past Transactions. ## Edit a package On the **Fee Packages** page, find the package you want to edit. Click the three dots () in the **Actions** column and select **Edit**. The creation wizard opens with the current values. Make your changes and click **Save**. A confirmation toast appears and the updated package returns to the list. To test the changes before you save, use the [Fee Calculator](/en/midaz/fees/console/managing-fee-calculations) on sample amounts first. # Running a Fee Estimation Source: https://docs.lerian.studio/en/midaz/fees/console/fe-fee-simulation Run a fee estimation with the Console Fee Calculator to preview charges on sample amounts, accounts, and currencies before creating any real Ledger Transaction. Use the **Fee Calculator** in the Midaz Module to estimate fees before you process a real Transaction. The Fee Calculator is read-only. It does not create a Transaction or change any Ledger balance. ## Before you start *** * Enable the **Fees Engine** in [Midaz module settings](/en/platform/console/plugin-settings). * You need at least one [Fee Package](/en/midaz/fees/console/fe-creating-a-package) saved. * You need the source and destination Account aliases you want to test against. ## Running an estimation *** In the Midaz Module sidebar, expand **Fees Engine** and click **Calculator**. Fill in the estimated transaction. See the [Field guide](#field-guide) below for each input. Click **Calculate Fees** to run the estimation. Review the results displayed below the form. ## Field guide *** | Field | What to enter | Example | | ------------------ | ----------------------------------------- | ----------------------- | | Transaction Amount | Total value to estimate fees against. | `100.00` | | Currency | Asset code for the estimated transaction. | `BRL` | | Fee Package | Fee Package to test. | `Standard Transfer Fee` | | From Account | Account alias that would be debited. | `@customer_123` | | To Account | Account alias that would be credited. | `@merchant_456` | | Description | Optional description for the estimation. | `Pix transfer test` | The Fee Calculator is a read-only tool. It does not affect the ledger or create any transactions. You can run as many estimations as needed safely. ## Understanding the results *** After you run an estimation, the Fee Calculator shows a detailed breakdown of the calculation: ### Summary * **Total Fee** - The sum of all fees for the transaction * **Net Amount** - The amount the destination would receive after deductible fees ### Fee breakdown For each fee rule applied, you'll see: | Field | Description | | --------------------- | ------------------------------------------------ | | **Fee Label** | Name identifying the fee | | **Priority** | Order in which the fee was applied | | **Calculation Type** | `flat` or `percentage` | | **Reference Amount** | The base amount used for calculation | | **Calculated Amount** | The actual fee value | | **Credit Account** | Account that would receive the fee | | **Is Deductible** | Whether the fee is deducted from the transaction | ## Estimation examples *** **Scenario**: Test a \$2.00 flat fee on a \$100 transfer **Input**: * Fee Package: `Standard Transfer Fee` * Asset: `USD` * Amount: `100.00` * Source: `@customer/checking` * Destination: `@merchant/revenue` **Expected Result**: * Total Fee: \$2.00 * Net Amount: \$98.00 (if deductible) or \$100.00 (if not deductible) **Scenario**: Test a 1.5% fee on a \$500 transfer **Input**: * Fee Package: `Percentage Transaction Fee` * Asset: `USD` * Amount: `500.00` * Source: `@customer/savings` * Destination: `@vendor/payments` **Expected Result**: * Total Fee: \$7.50 (1.5% of \$500) * Calculation: 500 × 0.015 = 7.50 **Scenario**: Test max(flat, percentage) fee on different amounts **Test 1** - Small transaction (\$30): * Flat: \$1.00 * Percentage: 2% = \$0.60 * Result: \$1.00 (flat is higher) **Test 2** - Large transaction (\$200): * Flat: \$1.00 * Percentage: 2% = \$4.00 * Result: \$4.00 (percentage is higher) **Scenario**: Package with two fees applied in priority order **Fee A** (Priority 1): 1% on original amount **Fee B** (Priority 2): 0.5% on after fees amount **Input**: \$1000 transaction **Expected Result**: * Fee A: \$10.00 (1% of \$1000) * Fee B: \$4.95 (0.5% of \$990) * Total Fee: \$14.95 ## Use cases for fee estimation *** ### Validate new fee packages Before you enable a new fee package, run estimations at various transaction amounts. Make sure the fees are correct across the full amount range. ### Test edge cases Estimate transactions at minimum and maximum amount boundaries to verify fee behavior at the limits of your configured ranges. ### Compare fee structures Run the same transaction through different fee packages to compare outcomes and choose the best structure for your use case. ### Document expected fees Use estimation results to create documentation for your finance team or customers about expected fee amounts. ### Debug fee issues If a transaction shows unexpected fees, use the Fee Calculator to reproduce the scenario and understand the calculation breakdown. ## Best practices *** * **Test full amount ranges** - Do not test only one amount. Estimate transactions at the minimum, the maximum, and several points in between. * **Verify priority ordering** - When packages have multiple fees, confirm they apply in the expected order. * **Check waived accounts** - Test with accounts that the fee package waives. Confirm the results exclude them from fees. * **Document your tests** - Keep a record of estimation results for audit and compliance purposes. * **Share with stakeholders** - Use estimation breakdowns to align with finance teams on expected fee outcomes before going live. ## What to do next *** After validating the fee result, adjust the Fee Package if needed in [Editing a Fee Package](/en/midaz/fees/console/fe-editing-a-package), or create a real Transaction in [Creating a Transaction](/en/midaz/console/creating-a-transaction). [Estimate Fees endpoint](/en/reference/midaz/plugins/fees-engine/simulate-fees) # Fees Engine in Lerian Console Source: https://docs.lerian.studio/en/midaz/fees/console/fees-engine-overview Explore the Fees Engine in the Midaz Module: enable it, manage Fee Packages, and estimate charges from the Console using Ledgers, segments, and routes. The **Fees Engine** is an optional capability of the Midaz Module that lets operators model transaction fees from the Console. Once enabled, it adds two pages to the Midaz Module sidebar: * **Fee Packages**: create and edit packages and their rules. * **Calculator**: estimate what a package would charge before you apply it to a real Transaction. The Fees Engine builds on the same Ledgers, Segments, Transaction Routes, and Accounts already defined in Midaz Core. Packages use that existing structure to set their scope. Midaz applies a package automatically to each Transaction that matches its conditions. ## Key concepts *** * **Fee Package** — Container that groups one or more fee rules and the conditions under which they apply (transaction route, segment, ledger, amount range). * **Fee Rule** — An individual fee inside a package, with its own calculation method. * **Application Rule** — How Midaz calculates a single fee: **flat**, **percentage**, or **maximum between both**. * **Reference Amount** — Whether a percentage fee applies to the original Transaction amount or to the amount after other fees. * **Priority** — The order in which Midaz applies multiple fees inside the same package. * **Waiver** — A source account exempted from a given package. ## Enabling the Fees Engine *** The Fees Engine appears in the sidebar only after you enable it. The [Plugin settings](/en/platform/console/plugin-settings) page shows different options for each organization license. If you do not see the Fees Engine there, contact the Lerian team. From the Midaz Module sidebar, go to **Settings** > **Plugins**. Toggle **Fees Engine** on. The **Fees Engine** menu item appears in the sidebar with **Fee Packages** and **Calculator** sub-items. If you disable the Fees Engine later, Midaz hides its menu but keeps every Fee Package you created. ## Where to go next *** Create, edit, and delete packages — define rules, scope by ledger or segment, and set waivers. Estimate what a package would charge for a sample Transaction without running a real one. Preview the fees for a period before you apply them. # Fee Calculator Source: https://docs.lerian.studio/en/midaz/fees/console/managing-fee-calculations Preview how Fee Packages apply to a sample Transaction using the Console Fee Calculator, with a detailed breakdown of flat, percentage, and deductible fees. The **Fee Calculator** previews how fees apply before you run a real Transaction. It uses an existing [Fee Package](/en/midaz/fees/console/managing-fee-packages) to estimate the total fees. It also shows a detailed breakdown of each rule — flat fees, percentages, and deductibles. Use it to test changes, validate logic with business teams, or build fee previews in your product UI. ## Accessing the Fee Calculator *** In the Midaz Module sidebar, expand **Fees Engine**. Click **Calculator** to open the Fee Calculator page. You need at least one [Fee Package](/en/midaz/fees/console/fe-creating-a-package) before using the Calculator — the estimator must run against a saved package. ## Why use the Fee Calculator *** The Fee Calculator is a read-only tool that does not affect the ledger or create any transactions. Use it to: * **Validate new fee packages** before enabling them in production * **Test edge cases** at minimum and maximum amount boundaries * **Compare fee structures** by running the same transaction through different packages * **Document expected fees** for finance teams or customers * **Debug fee issues** by reproducing scenarios and analyzing calculation breakdowns # Managing Fee Packages Source: https://docs.lerian.studio/en/midaz/fees/console/managing-fee-packages Browse, create, edit, and delete Fee Packages from the Midaz Console to control how flat, percentage, and combined charges apply to matching Transactions. **Fee Packages** are the base unit of the [Fees Engine](/en/midaz/fees/console/fees-engine-overview). Each package defines a set of fee rules and the conditions under which they apply to Transactions. Enable the Fees Engine in [Midaz module settings](/en/platform/console/plugin-settings) before you use this page. ## What a Fee Package contains *** A Fee Package lets you: * **Define fee calculation rules** — flat fees, percentage-based fees, or combined calculations. * **Set amount ranges** — apply fees only to transactions within a min/max range. * **Link to transaction routes** — target specific transaction flows. * **Waive fees for specific accounts** — exempt selected source accounts. * **Prioritize fee application** — control the order in which Fees Engine applies multiple fees. Fee Packages combine with [Accounting Routes](/en/midaz/console/managing-accounting-routes) and [Segments](/en/midaz/console/managing-segments) for granular control. ## Accessing the Fee Packages page *** In the Midaz Module sidebar, expand **Fees Engine**. Select **Fee Packages**. The page lists every configured package. If you have no Fee Packages, the list is empty and shows a prompt to create your first package. ### Available actions From the Fee Packages page, you can: Define new fee calculation rules for transactions Update fee configurations and rules Delete a fee package from your configuration Select a package from the list to open its **details view**. There you can review the package's configuration — type, scope, and rules — before you edit it. To preview the fees a period generates across packages, use [Billing Runs](/en/midaz/fees/console/billing-runs). ## Understanding Fee Package structure *** When you create or edit a fee package in Midaz Module, you work with these fields: ### Basic information * **Fee Group Label** (required) - A descriptive name for the fee package * **Description** (optional) - Explanation of the package purpose and when it applies * **Transaction Route** (optional) - Link to a specific transaction route * **Segment** (optional) - Associate with a specific segment * **Ledger** (required) - The ledger this package applies to ### Amount range * **Minimum Amount** (required) - Lowest transaction amount for fee application * **Maximum Amount** (required) - Highest transaction amount for fee application * **Waived Accounts** (optional) - List of account aliases exempt from these fees ### Fee rules Each fee package can contain multiple fee rules with: * **Fee Label** - Name that identifies this fee * **Application Rule** - How Fees Engine calculates the fee: * `flatFee` - Fixed amount regardless of transaction value * `percentual` - Percentage of the transaction amount * `maxBetweenTypes` - Higher value between flat and percentage calculations * **Calculation Type** - `flat` or `percentage` * **Calculation Value** - The fee amount or percentage * **Reference Amount** - Base for percentage calculation: * `originalAmount` - Calculate on the original transaction value * `afterFeesAmount` - Calculate after Fees Engine applies other fees * **Priority** - Order of fee application (lower numbers apply first) * **Is Deductible From** - Whether Fees Engine deducts this fee from the transaction * **Credit Account** - Account that receives the fee amount ## Common use cases *** Apply a fixed fee to all transactions within an amount range: * **Application Rule**: `flatFee` * **Calculation**: `flat` with value `2.50` * **Example**: $2.50 fee on every transaction between $10 and \$1000 Charge a percentage of the transaction amount: * **Application Rule**: `percentual` * **Calculation**: `percentage` with value `1.5` * **Example**: 1.5% fee on transfers, where $100 transfer = $1.50 fee Create multiple packages with different amount ranges: * Package 1: $0-$100 → 2% fee * Package 2: $100-$1000 → 1.5% fee * Package 3: \$1000+ → 1% fee * Lower fees for higher-value transactions Apply whichever is higher between flat and percentage: * **Application Rule**: `maxBetweenTypes` * **Calculations**: `flat: 1.00` and `percentage: 2.0` * **Example**: On $30 transaction: max($1.00, $0.60) = $1.00 fee * **Example**: On $100 transaction: max($1.00, $2.00) = $2.00 fee Exempt specific accounts from fees: * Create package with standard fee rules * Add premium account aliases to **Waived Accounts** list * These accounts transact without the configured fees ## Best practices *** Design your fee packages before implementation. Consider transaction types, amount ranges, and customer segments that require different fee treatments. Choose clear fee group labels and descriptions that explain when and why fees apply. This helps with auditing and customer communication. Define minimum and maximum amounts carefully to avoid gaps or overlaps in your fee structure. Use the [Fee Estimation](/en/midaz/fees/console/fe-fee-simulation) feature to preview fee calculations before you enable packages in production. When you use multiple fees in a package, set priorities so the fees apply in the correct order. Record why you waive fees for certain accounts. This supports audit and compliance. ## Fee calculation flow *** When Fees Engine processes a transaction, it: 1. Identifies applicable fee packages based on transaction route and segment 2. Filters packages by amount range (transaction must fall within min/max) 3. Checks whether the account is in the waived accounts list 4. Applies fee rules in priority order 5. Calculates each fee based on application rule and reference amount 6. Credits fee amounts to designated accounts Enable a fee package so it applies to transactions. To pause a package, disable it. A disabled package stays in your configuration. # Fees Engine calculations Source: https://docs.lerian.studio/en/midaz/fees/fee-engine-calculation Learn how the Fees Engine calculates fees, handles decimal precision, splits amounts across accounts, and applies exemptions over daily or monthly periods. ## Numeric values (string) *** Express all financial values in **Fees Engine** as a `string` with the `numeric` type. This gives high-precision decimal handling for assets like BRL or BTC. It also prevents rounding errors during calculations, splits, or exemptions. **Important** * **Required**: Midaz v3.x.x (uses `numeric`). * **Incompatible**: Midaz v2.x.x (deprecated `amount` + `scale` format). Clients using Midaz v2.x.x must upgrade to v3.x.x to ensure proper integration and functionality with Fees Engine. **Example:** ``` "value": "12.50" ``` ## Billing periods *** When triggering a billing calculation, you specify the time window through the `period` field. Fees Engine supports three formats: | Format | Example | Window | | ------- | ------------ | ------------------------------------------------------------------ | | Daily | `2026-03-15` | Start of that day → start of the next day (UTC) | | Weekly | `2026-W13` | Monday 00:00 UTC of the ISO week → following Monday 00:00 UTC | | Monthly | `2026-03` | First instant of the month → first instant of the next month (UTC) | The engine uses the period to count qualifying transactions (for volume packages) or active accounts (for maintenance packages) within that exact window. Weekly periods follow [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Week_dates). Week numbering ranges from `W01` to `W52` (or `W53` in years that have 53 ISO weeks). The week always starts on Monday. Choose the granularity that matches your billing cycle. A prepaid card product billed daily would use `2026-03-15`. A SaaS platform billed monthly would use `2026-03`. A marketplace that settles weekly would use `2026-W13`. ## Fee calculation rules *** Each fee uses an `applicationRule` to define how it's calculated. You can choose from three rule types: * [maxBetweenTypes](#maxbetweentypes) * [flatFee](#flatfee) * [percentual](#percentual) You can combine different rules in a single package to match your use case. Other key fields: * `isDeductibleFrom`: defines if the fee is deducted from the sender or the receiver. * `referenceAmount`: either `originalAmount` or `afterFeesAmount`. * `priority`: defines the order of application. Priority 1 must always use `originalAmount`. ### maxBetweenTypes Applies whichever is greater: a flat or percentage-based fee. **Example** * Flat fee value: R\$5. * Percentual fee: 2%. * Reference amount: R\$1,000. ``` rate = 1000 * 0.02 = R$ 20.00 ``` Since **R\$ 20 > R\$ 5**, the engine applies the percentage-based fee. ### flatFee Applies a fixed fee amount. Behavior depends on `isDeductibleFrom`. **Example** * Flat fee: R\$15. * Reference amount: R\$115. | `isDeductibleFrom` | Formula | Total Fee | | :----------------- | :---------------------- | :--------- | | `false` | `referenceAmount + fee` | R\$ 130.00 | | `true` | `referenceAmount - fee` | R\$ 100.00 | ### percentual Applies a fee as a percentage of the reference amount. **Example** * Value: 30%. * Reference amount: R\$ 389.50. | `isDeductibleFrom` | Formula | Total Fee | | :----------------- | :-------------------------------------------- | :--------- | | `false` | `referenceAmount * value` | R\$ 116.85 | | `true` | `referenceAmount - (referenceAmount * value)` | R\$ 272.65 | ## Fee splitting *** When a transaction has multiple source accounts, Fees Engine splits fees proportionally. ### Example * Total amount: R\$4,000.00 * Fixed fee: R\$15.00 * Tax: 4% * `isDeductibleFrom: false` ### Participation % > **Formula**: (Account Amount ÷ Total Amount) × 100 | Account | Share | Amount | | :-------- | :---- | :-------- | | @account1 | 25% | R\$ 1,000 | | @account2 | 25% | R\$ 1,000 | | @account3 | 40% | R\$ 1,600 | | @account4 | 10% | R\$ 400 | ### Fixed fee distribution > **Formula**: `fixed Fee × participation %` | Account | Fee Share | Total | | :-------- | :-------- | :----------- | | @account1 | R\$ 3.75 | R\$ 1,003.75 | | @account2 | R\$ 3.75 | R\$ 1,003.75 | | @account3 | R\$ 6.00 | R\$ 1,606.00 | | @account4 | R\$ 1.50 | R\$ 401.50 | ### Proportional tax > **Formula**: `account amount × tax %` | Account | Tax | Total w/ Tax | | :-------- | :-------- | :----------- | | @account1 | R\$ 40.00 | R\$ 1,040.00 | | @account2 | R\$ 40.00 | R\$ 1,040.00 | | @account3 | R\$ 64.00 | R\$ 1,664.00 | | @account4 | R\$ 16.00 | R\$ 416.00 | ### Final amount per account > **Formula**: `principal + fee + tax` | Account | Fee | Tax | Final Total | | :-------- | :------- | :-------- | :----------- | | @account1 | R\$ 3.75 | R\$ 40.00 | R\$ 1,043.75 | | @account2 | R\$ 3.75 | R\$ 40.00 | R\$ 1,043.75 | | @account3 | R\$ 6.00 | R\$ 64.00 | R\$ 1,670.00 | | @account4 | R\$ 1.50 | R\$ 16.00 | R\$ 417.50 | #### Validations * Total shares = 100% * Fee split matches flat fee * Tax split = 4% * Total sent = R\$ 4,175.00 ## Fee exemptions: rules & hierarchy *** ### By transaction amount Use `minimumAmount` and `maximumAmount` to define when **fees should apply**. > For example: If the range is R$ 0–300, a transaction of R$ 301 won’t trigger fees. ### By account The system checks `waivedAccounts`. A source in that list is exempt from fees. **Hierarchy:** Value range check > then account exemption ## Mixed example: fee exemptions and proportional fee splitting *** Let’s look at an example of a package that includes accounts with fee exemptions and requires splitting fees proportionally. ### Scenario We’re processing a transaction of **R\$ 4,000**, which includes: * A **fixed fee** of **R\$ 16**. * Only some accounts are subject to the fixed fee * An **IOF tax** of **6%** to be deducted. #### Split on the source side | Source Account | % | Proportional Value | | :------------- | --- | :----------------- | | @account1 | 15% | R\$ 600 | | @account2 | 35% | R\$ 1,400 | | @account3 | 40% | R\$ 1,600 | | @account4 | 10% | R\$ 400 | The engine applies the fixed fee only to `@account3` and `@account4`. #### Result after Admin Fee (proportional) | Account | Admin Fee | Total | | :-------- | :-------- | :----------- | | @account1 | Exempt | R\$ 600 | | @account2 | Exempt | R\$ 1,400 | | @account3 | R\$ 12.80 | R\$ 1,612.80 | | @account4 | R\$ 3.20 | R\$ 403.20 | Total send value increases to **R\$4,016**. #### IOF deduction (recipient) | Recipient | % | Gross | IOF (6%) | Net | | :--------- | --- | :-------- | :------- | :------ | | @donation1 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 | | @donation2 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 | | @donation3 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 | | @donation4 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 | The engine credits fees to the accounts defined in each fee’s `creditAccount`. ## Repeating decimals *** When a fee split produces a repeating decimal (for example, 0.3333...), Fees Engine keeps every fee leg at full precision. It reconciles the small remainder onto the fee for the account with the largest value. This keeps the total exact, avoids rounding drift, and keeps your ledger consistent. ## Billing calculations *** Billing packages use a different calculation model than fee packages. Instead of evaluating individual transactions, they aggregate data over a billing period and return charge payloads for your orchestrator to execute. Billing supports three period formats: monthly (`YYYY-MM`), weekly (`YYYY-Www`, e.g., `2026-W13`), and daily (`YYYY-MM-DD`). ### Volume billing calculation Volume billing counts transactions matching an `eventFilter` (transaction route + status) within the billing period, then applies pricing based on the configured model. The calculation follows this order: 1. **Count** qualifying transactions in the period. 2. **Subtract** the `freeQuota` from the total count to get the billable count. 3. **Apply pricing** based on the `pricingModel` (`tiered` or `fixed`). 4. **Apply a discount** from `discountTiers`, evaluated against the **total** count (before the free quota was subtracted). Amounts are kept at full decimal precision throughout — the engine applies no asset-scale rounding. #### Tiered pricing Tiered pricing is **volume pricing, not graduated pricing**. The engine finds the single tier whose quantity range contains the billable count, then charges **every** billable unit at that tier's unit price. It does not price each unit inside its own range. A tier's range is inclusive on both ends. If you omit the upper bound, the tier is unbounded. The engine looks up a tier only when the billable count is positive. If the billable count is positive and no tier covers it, the calculation fails for that package. A billable count of zero skips the tier lookup and produces a zero amount, so your tiers do not need to cover zero. **Example**: A billing package for boleto issuance with three tiers and a free quota of 50: | Range | Unit price | | --------- | ---------- | | 1–500 | R\$ 1.20 | | 501–2,000 | R\$ 0.80 | | 2,001+ | R\$ 0.45 | For a client that issued 1,800 boletos in the month: * 50 exempt (free quota) → 1,750 billable * 1,750 falls in the 501–2,000 tier, so **all** 1,750 units are priced at R\$ 0.80: R\$ 1,400.00 gross * Discount tier applies on the total count of 1,800 (≥ 1,000 → 5%): −R\$ 70.00 * **Net total: R\$ 1,330.00** #### Fixed pricing A single unit price applies to all billable transactions regardless of volume. The engine still subtracts the free quota before calculation. Fixed pricing takes its unit price from the **first tier** in the package, so a fixed package must still declare at least one tier — otherwise the calculation fails. **Example**: R\$ 0.10 per Pix sent, no free quota: * 5,000 Pix transactions × R\$ 0.10 = R\$ 500.00 #### Discount tiers At most **one** discount tier applies: the one with the largest `minQuantity` that the total count meets or exceeds. Its percentage is applied to the gross amount. Discounts are evaluated against the total transaction count, not the billable count. #### Count scope Volume calculation counts transactions **per transaction route across the whole ledger**. The free quota, the tiers, and the discount tiers all apply to that route-level total. To count two flows separately, create one package per transaction route. ### Maintenance billing calculation Maintenance billing charges a fixed amount per active account in the billing period. The engine resolves the target accounts, keeps only the active ones, and generates a single transaction payload. The result is a N:1 transaction: * Each active account appears as a debit entry (`source.from`) for the configured `feeAmount`. * The `maintenanceCreditAccount` receives the full total as a single credit entry (`distribute.to`). Only accounts whose status code is `active` are included; every other status is excluded. If no account resolves, the package returns an empty payload (`{}`) instead of a transaction. **Example**: Monthly maintenance of R\$ 9.90 for a segment with 12,000 active PF accounts: * 12,000 entries in `source.from`, each debited R\$ 9.90 * 1 entry in `distribute.to` credited R\$ 118,800.00 ### Zero-amount results When a package's net amount comes out zero — for example the free quota covered every transaction — the engine still returns a result for that package, but with an empty transaction payload (`{}`). Treat that as "processed, nothing to submit". Fully exempt usage reaches this result without a tier lookup. The free quota brings the billable count to zero, the engine skips tier matching, and the amount is zero. A package whose tiers start at 1 is correct for this case. ### All-or-nothing failure policy If any billing package fails during a `/billing/calculate` call, the entire operation fails. The engine returns no partial results. The response includes which package and resource caused the failure, so you can fix and re-execute. ### Audit metadata Each billing calculation result carries structured metadata for traceability. Volume results include the billing type, package id and label, period, total and billable event counts, free quota used, pricing model, gross and net amounts, and the discount detail (percentage, amount, and `minQuantity`) when one applied. Maintenance results include the billing type, package id and label, period, total account count, and the per-account fee amount. # Fees Engine best practices Source: https://docs.lerian.studio/en/midaz/fees/fees-engine-best-practices Follow Fees Engine best practices for naming packages, setting fee priorities, segmenting customers, and running production setups with full auditability. Fees Engine controls how you calculate, apply, and track fees. Production use demands careful configuration and operational discipline, because accuracy and auditability are non-negotiable. These recommendations complement the [Fees Engine overview](/en/midaz/fees/fees-engine-overview) and the [calculation mechanics](/en/midaz/fees/fee-engine-calculation) guide. ## 1. Design fee packages with clear naming and segmentation *** Fee packages are the foundation of your fee logic. A well-organized package structure makes it easier to maintain, debug, and audit your fee configuration over time. * **Use descriptive names** that reflect the business context (e.g., "pix-transfer-standard", "wire-premium-segment"). * **Segment by product and customer group** using `segmentId`. This allows you to apply different fee rules to different customer tiers without creating conflicting packages. * **Keep packages focused**. A package that tries to cover too many scenarios becomes hard to test and maintain. Prefer multiple focused packages over one that does everything. * **Document your package structure** internally. As your package count grows, a clear reference of which package applies where prevents misconfiguration. ## 2. Set fee priorities carefully *** When a package contains multiple fees, the `priority` field determines the order of execution. Getting this wrong can produce incorrect calculations. * **Priority 1 must always use `referenceAmount: originalAmount`**. The engine enforces this. * **Fees with `isDeductibleFrom: true` must also use `referenceAmount: originalAmount`**. Deductible fees then always apply to the full transaction value. * **Priority must be unique within a package**. The engine rejects duplicate priorities. * **Think about fee dependencies**. If one fee adjusts the transaction value and another fee should be calculated on the adjusted value, use `referenceAmount: afterFeesAmount` with a higher priority number. If the second fee should reference the original value, use `originalAmount`. When in doubt, start with a simple configuration (one or two fees per package) and validate the results using the estimate endpoint before adding complexity. ## 3. Always estimate before applying fees in production *** Fees Engine provides an [estimate endpoint](/en/reference/midaz/plugins/fees-engine/simulate-fees) that lets you preview fee calculations without writing anything to the ledger. Use estimation to: * **Validate new packages** before activating them. Confirm that the calculated values match your expected results across different transaction amounts. * **Test edge cases**: zero-amount transactions, boundary values at `minimumAmount` and `maximumAmount` thresholds, and exempted accounts. * **Preview fees for users**. If your product shows fees before confirmation, use the estimate endpoint to provide accurate previews. * **Debug unexpected results**. If a calculated fee doesn't match expectations, estimate the same transaction with a specific `packageId` to isolate the issue. The [calculate endpoint](/en/reference/midaz/plugins/fees-engine/calculate-fees) automatically selects the best matching package. The estimate endpoint requires a specific `packageId`, giving you full control over which package to test. ## 4. Manage exemptions explicitly *** Fees Engine supports two types of exemptions: by **transaction amount range** and by **account**. * **Amount ranges** (`minimumAmount`, `maximumAmount`): Define the transaction value window in which fees apply. Transactions outside this range are exempt. Use this for promotional thresholds or tiered pricing. * **Waived accounts** (`waivedAccounts`): Specific accounts exempt from fees within a package. Use this for internal accounts, employee accounts, or partnership arrangements. Best practices for exemptions: * **Keep waived account lists short and reviewed**. Large lists become hard to audit. Periodically review which accounts are exempted and why. * **Document the business reason** for each exemption in your internal records. * **Test exemption boundaries**. If your range is R$ 0–300, make sure transactions at exactly R$ 300 and R\$ 301 behave as expected. ## 5. Enable and tune caching for performance *** Fees Engine caches fee packages in memory to reduce database queries during high traffic. Configure caching through environment variables: ```yaml theme={null} fees: configmap: PACKAGE_CACHE_ENABLED: "true" PACKAGE_CACHE_TTL_SECONDS: "600" ``` * **`PACKAGE_CACHE_ENABLED`** (default: `true`): Enables or disables the package cache. * **`PACKAGE_CACHE_TTL_SECONDS`** (default: `180`): Time-to-live in seconds before the engine refreshes cached packages from the database. Recommendations: * **Keep caching enabled in production**. It significantly reduces latency for high-volume transaction processing. * **Adjust TTL based on your change frequency**. If you update packages frequently, use a shorter TTL (e.g., 60–120 seconds). If packages are stable, the default 180 seconds is appropriate. * **Be aware of cache delay**. After you update a package, the change may take up to the configured TTL to propagate. If you need immediate effect, restart the service or temporarily reduce the TTL. ## 6. Use correct numeric values *** Express all financial values in Fees Engine as **strings** using the `numeric` type. This prevents floating-point precision errors that are common with decimal arithmetic. ```json theme={null} "value": "12.50" ``` * Always send values as strings, even whole numbers (e.g., `"100"` not `100`). * Never use floating-point types for monetary calculations in your integration layer. * Note that the engine automatically adjusts fee splits with repeating decimals (e.g., R\$ 10 divided by 3 accounts) to keep ledger totals exact. Fees Engine requires **Midaz v3.x.x** or later. The v2.x.x `amount` + `scale` format is not compatible. Upgrade Midaz before deploying Fees Engine. ## 7. Use soft delete for auditability *** When you delete a fee package, Fees Engine marks it with a `deletedAt` timestamp rather than removing it from the database. This preserves the audit trail for historical transactions that referenced that package. * **Don't rely on hard deletion** for fee packages in production. Historical transactions may reference deleted packages for reconciliation. * **Periodically review** deleted packages if your database grows significantly. Archiving strategies can help manage storage without losing audit capability. ## 8. Monitor the Fees Engine in production *** Fees Engine supports OpenTelemetry for traces and metrics. Enable it to gain visibility into fee calculation performance and behavior. ```yaml theme={null} fees: configmap: ENABLE_TELEMETRY: "true" OTEL_RESOURCE_SERVICE_NAME: "plugin-fees" ``` In production: * **Monitor health endpoints**. Fees Engine exposes `/health` for readiness and liveness checks (default port: 4002). * **Set up alerts** for sustained high latency on fee calculations, which may indicate database contention or cache misconfiguration. * **Monitor MongoDB** connection pool usage, disk space, and replication health. The default `MONGO_MAX_POOL_SIZE` is 100. * **Review pod resource usage** against your traffic patterns and autoscaling behavior. ## 9. Keep versions compatible *** Before upgrading Fees Engine: * Check the [version compatibility table](/en/platform/plugins/midaz-version-compatibility) to confirm compatibility with your Midaz Core version. * Always upgrade **Midaz Core first**, then Fees Engine. * Back up your MongoDB data and Helm values before any major upgrade. * Test the upgrade in a staging environment before applying it to production. For upgrade procedures, see the [Helm upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide). ## 10. Review the security recommendations *** Fees Engine processes financial data and integrates with Midaz ledger operations. Make sure your deployment follows the platform-wide [Security recommendations](/en/midaz/security-recommendations), which cover: * Network segmentation and Zero Trust Architecture * Secret management and rotation (including `LICENSE_KEY` and database credentials) * TLS 1.2+ enforcement for all communications * RBAC configuration via [Access Manager](/en/platform/access-manager/access-manager) * Patch management and vulnerability scanning ## 11. Design billing packages with clear scope *** Each billing package should represent a single, well-defined charge. Avoid packing unrelated pricing into one package. * **Separate packages per transaction route**. A package for Pix billing and a package for boleto billing are clearer than one package that tries to handle both. * **Use descriptive labels** that include the billing type and target: "Pix Send Monthly Billing — Standard Tier" is better than "Billing Package 1". * **One `accountTarget` type per maintenance package**. You cannot combine `segmentId`, `portfolioId`, and `aliases` in the same package. If you need different targets, create separate packages — a single `/billing/calculate` call evaluates all active packages. ## 12. Write commercial terms against route volume *** Volume calculation counts transactions per transaction route across the whole ledger. The free quota, the tiers, and the discount tiers all apply to that route-level total. * **State thresholds as route volume.** "The first 100 transactions on the `pix-send` route are free" maps directly onto a package. Write the contract in the same terms the engine bills in. * **Split routes to split counts.** One package per transaction route keeps each flow on its own quota, tiers, and discounts. ## 13. Plan free quotas and discount tiers carefully *** Fees Engine evaluates free quotas and discounts in a specific order: 1. The engine subtracts the free quota from the total count to get the billable count. 2. The engine prices the billable count — for tiered packages it charges every billable unit at the single matching tier's rate (volume pricing, not graduated). 3. The engine applies one discount tier to the gross amount, chosen against the **total** count (before the free quota was subtracted). Design considerations: * **Free quotas reset each billing period**. Set the value based on your commercial agreement per period (monthly, weekly, or daily), not lifetime. * **Tiers are volume brackets, not slices**. A billable count of 1,750 against a 501–2,000 tier prices all 1,750 units at that tier's rate. Crossing a bracket boundary changes the rate for the whole volume, so tier boundaries can move the bill sharply. * **Cover every positive billable count.** If the billable count is positive and no tier's range contains it, the calculation fails for that package. Leave the top tier's upper bound open. Start the first tier at 1 — a billable count of zero produces a zero amount and an empty payload without a tier lookup. * **Discount tiers are cumulative thresholds**, not ranges, and only one applies: the highest `minQuantity` the total count reaches. If you define discounts at 200 and 400 transactions, a client with 500 transactions gets the 400+ discount — not both. * **Watch the two different counts.** Pricing uses the billable count (after free quota); the discount uses the total count (before it). Use test calculations with known transaction counts to validate your tier and discount configuration before enabling the package in production. ## 14. Size maintenance account targets appropriately *** Maintenance packages support three target types with different scale profiles: | Target type | Scale | Use case | | ------------- | --------------------- | ------------------------------------------------ | | `segmentId` | 100,000+ accounts | Standard tiers (PF, PJ, premium) | | `portfolioId` | Thousands of accounts | Business portfolios (PME, Corporate, Enterprise) | | `aliases` | Up to 100 accounts | Specific named accounts | Choose the target type that matches your operational scale. If you find yourself listing hundreds of aliases, migrate to a segment or portfolio in Midaz instead. ## 15. Handle billing failures with re-execution *** Billing calculation follows an all-or-nothing policy. If any package fails, the engine returns no results. * **Build retry logic** into your orchestrator. The calculation is stateless — re-executing for the same period produces the same results. * **Check error responses** for the specific package and resource that failed. Common causes: invalid `ledgerId`, unreachable Midaz API, or disabled billing packages. * **Separate volume and maintenance calculations** if one type consistently succeeds while the other fails. Call `/billing/calculate` with `"type": "volume"` and `"type": "maintenance"` independently to isolate failures. # What is Fees Engine? Source: https://docs.lerian.studio/en/midaz/fees/fees-engine-overview Fees Engine controls how fees and billing are configured, calculated, and tracked across per-transaction and per-period scenarios. Fees is an embedded component of the unified Midaz ledger process. It provides fee and billing configuration and calculations alongside the ledger. Deploy and configure it with Midaz; it is not a separate service or plugin. ## Why use Fees Engine? *** The **Fees Engine** helps you manage complex fee logic. It applies flat rates, distributes fees proportionally, and estimates transactions before you run them. Here's what it unlocks: * **Flexible fee configuration** via fee packages—tailored to account groups or specific ledgers. * **Multiple calculation methods**: flat fees, percentual rates, and "max between types" logic. * **Proportional fee distribution** for marketplace flows and multi-account operations. * **Support for accounting routes** via `transactionRoute`, `routeFrom`, and `routeTo`. * **Estimation tools** to preview calculations before executing transactions. * **Fee exemption logic** by account and transaction value ranges. * **Priority-based application** to control the order of multiple fees. * **Accurate deduction mechanics** with `isDeductibleFrom` support. * **Volume-based billing** via billing packages — charge based on accumulated transaction counts per period (daily or monthly). * **Maintenance billing** for recurring per-account charges, targeting accounts by segment, portfolio, or explicit list. * **Free quotas and progressive discounts** to model tiered pricing and volume incentives. * **Segment-based exemptions** to exempt entire account groups from fee packages without listing individual accounts. Fees Engine is a licensed Midaz capability that runs inside the unified ledger process. Deploy and configure it with Midaz. If you'd like to learn more or evaluate it for your use case, [get in touch with our team](https://lerian.studio/contact). ## What are fees? *** Fees are monetary values charged in exchange for services, products, or access to resources. Their purpose depends on the industry, but the need for clarity and consistency is universal. Below are just a few examples: ### Finance In the financial sector, fees cover operational costs and support legal compliance. * **Account maintenance fee**: Keeps accounts operational and covers administrative costs. * **Transfer fee:** Applies to transactions like TEDs or international transfers. ### Logistics and transportation In the logistics sector, fees cover transportation and storage services. * **Handling fee**: Applies during storage and physical movement of goods. * **Unloading fee**: Covers unloading operations at delivery points. ### Pharmaceutical and healthcare In the pharmaceutical sector, fees ensure the quality and regulation of services. * **Medication registration fee**: Related to regulatory approvals and market entry. * **Laboratory analysis fee**: Covers testing and quality control costs. ### Agricultural In the agricultural sector, fees cover commercialization and regulatory processes. * **Sanitary inspection fee**: Ensures health compliance for agricultural exports. * **Agricultural export fee**: Covers admin and regulatory export costs. ## Scope *** Fees and Billing support organization scope and ledger scope in parallel. On the ledger-scoped v2 surface, the ledger named by the request is authoritative: a `ledgerId` query parameter is not accepted, and a body `ledgerId` must match the ledger in the request. A package owned by another ledger is returned as not found. ## Fee Packages *** A **Fee Package** defines how the engine applies fees to a transaction. It groups one or more fee rules. You customize it by segment, ledger, and accounting routes. You can create different packages for different products, transaction types, or customer segments. Each package has its own calculation logic, route configuration, and priority rules. A package includes required fields and can add optional matching or exemption fields: * **ledgerId** – The ledger that records the transaction and its fees. * **transactionRoute** – The main accounting route for the transaction, for route-level matching. * **segmentId** – The product or segment the package applies to, for segment-level matching. * **waivedAccounts** – Accounts to exempt from fees, when you configure exemptions. * **fees** – A map of individual fee rules, each including: * **priority** – Defines execution order. * **routeFrom** and **routeTo** – Custom accounting routes for the fee. * **isDeductibleFrom** – Whether the engine deducts the fee from the original amount. * **referenceAmount** – The base amount for calculations. **Fees Engine** requires explicit route configuration for each fee and direction (e.g., debit or credit). ### Validation rules To ensure consistency and prevent configuration errors, Fees Engine enforces the following rules: * **Fee priority must be unique** within a package. * Fees with `isDeductibleFrom: true` must use `referenceAmount: originalAmount`. * Fees with `priority` 1 must also use `referenceAmount: originalAmount`. * Fields like `organizationId`, `ledgerId`, and `creditAccount` must exist in Midaz. Fees Engine validates them with the [Retrieve an Account by Alias](/en/reference/midaz/retrieve-an-account-by-alias) endpoint. Make sure your configuration meets the latest Midaz standards. Fees Engine validates every package and transaction against them. Check the rules for `ledgerId`, `creditAccount`, and `referenceAmount`, plus optional matching fields like `segmentId` when you configure them. ### Choosing the right endpoint: calculate vs. estimate Fees Engine provides two endpoints to apply fees. They behave differently based on the control you need: #### [Calculate fees for a package](/en/reference/midaz/plugins/fees-engine/calculate-fees) * Automatically fetches all available packages for the given organization and ledger. * Picks the best match based on transaction context. * Applies the corresponding fee rules. * If no package matches, the engine applies no fees. #### [Estimate transaction fees](/en/reference/midaz/plugins/fees-engine/simulate-fees) * Estimates fees for a **specific** package by its `packageId`. * Returns calculated fees **only if the transaction matches** the package's conditions. * Useful for testing, debugging, or a preview of fees, without writing to the ledger. Use `calculate` when you want the engine to decide which package to apply. Use `estimate` when you want full control over which package to test. ### Segment-based exemptions Fee packages support exempting individual accounts by listing their aliases in `waivedAccounts`. To exempt a whole group at once, add a segment reference to that same list using the `segment:` form — for example `"segment:seg_premium_01HZ..."`. The package's own `segmentId` field is **not** an exemption. It scopes which package applies to a transaction (segment-level package matching). Exemptions always live in `waivedAccounts`. Use segment-based exemptions when: * A customer tier (such as premium accounts) is universally exempt from a fee. * Internal or partner accounts belong to an existing segment in Midaz. * Maintaining a list of individual account aliases is impractical at scale. Fees Engine resolves segment-based exemptions at calculation time. When accounts join or leave the segment, the change takes effect on the next calculation. You do not update the package. ## Billing Packages *** **Billing Packages** calculate charges from accumulated transaction volume over a period — daily or monthly. Fee packages charge per individual transaction. Billing packages count the qualifying transactions and return payloads for your orchestrator to execute. Two types are available: * **Volume** — charges based on the number of transactions matching an event filter in the period. * **Maintenance** — charges a fixed recurring fee per active account, once per billing period. Billing packages are a calculation engine, not a billing platform. The engine calculates the charges and returns the payloads. Your orchestrator — Flowker, a cron job, or any other caller — runs the actual charges against Midaz. ### Volume packages A volume package counts transactions that match a given `eventFilter` (transaction route + status) within the billing period. It then applies a charge from the configured pricing model. Key fields: * **eventFilter** — Specifies which transactions to count: `transactionRoute` and `status`. * **pricingModel** — Either `tiered` (unit price varies by quantity range) or `fixed` (single unit price regardless of volume). * **tiers** — Quantity ranges (`minQuantity`, `maxQuantity`) and `unitPrice` per unit within each range. * **freeQuota** — Number of transactions exempt per period. The engine subtracts this count before it applies pricing. * **discountTiers** — Progressive discounts: when total volume meets a threshold, the engine applies the configured discount percentage to the final amount. * **countMode** — Accepts `perRoute` or `perAccount`. Volume calculation counts all matching transactions on the route as a single total. * **debitAccountAlias** / **creditAccountAlias** — Accounting routes for the charge. ### Maintenance packages A maintenance package applies a fixed fee per active account in the billing period, regardless of transaction activity. Key fields: * **feeAmount** — Fixed charge per active account. * **assetCode** — Currency for the charge. * **maintenanceCreditAccount** — Account that receives the fee revenue. * **accountTarget** — Defines which accounts to charge. Use exactly one per package: * `segmentId` — All accounts in the segment. * `portfolioId` — All accounts in the portfolio. * `aliases` — Explicit list of account aliases (maximum 100 accounts). Each maintenance package supports only one `accountTarget` type. You cannot combine `segmentId`, `portfolioId`, and `aliases` in the same package. ### Managing billing packages The following endpoints manage billing packages: * `POST /v1/billing-packages` — Create a billing package. * `GET /v1/billing-packages` — List all billing packages. * `GET /v1/billing-packages/:id` — Retrieve a specific billing package. * `PATCH /v1/billing-packages/:id` — Update a billing package (`label`, `description`, `enable`). * `DELETE /v1/billing-packages/:id` — Soft-delete a billing package. * `POST /v1/billing/calculate` — Calculate billing for a period. ## Fee Packages vs. Billing Packages *** | | Fee Packages | Billing Packages | | --------------------- | ---------------------------------------------- | ---------------------------------------------------------- | | **Trigger** | Per transaction (synchronous) | Per period (monthly, weekly, or daily) | | **Pricing model** | Flat, percentual, maxBetweenTypes | Tiered or fixed by volume | | **Account exemption** | `waivedAccounts` (aliases or `segment:`) | `accountTarget`: segment, portfolio, or alias list | | **Volume discounts** | No | Yes (`discountTiers`) | | **Free quotas** | No | Yes (`freeQuota` per period) | | **Recurring charges** | No | Yes (maintenance type) | | **Execution** | Automatic — engine evaluates each transaction | Caller-triggered — orchestrator calls `/billing/calculate` | | **Output** | Transaction with fees applied | Calculation payloads for the caller to execute | Fee packages and billing packages operate independently. A transaction can trigger a fee package calculation and also count toward a billing package in the same period. These are separate, non-conflicting events. ## Routing fees *** Every fee can have: * A `routeFrom`, which represents the accounting route for the debit (or source). * A `routeTo`, which represents the accounting route for the credit (or destination). * A `transactionRoute`, which represents the overall nature of the transaction. This allows granular tracking of each fee entry in the ledger. ## Deductible fees *** If you flag a fee as deductible ( `isDeductibleFrom: true`), the following logic applies: * The **source account sends the full value**. * The **engine subtracts the fee from the amount the destination account receives**. * The `referenceAmount` for calculations must be `originalAmount`. This way, the sender sends the full amount, and the destination account absorbs the deduction. ## Soft delete for safe recordkeeping *** Fees Engine loses no data. When you delete a resource: * Fees Engine marks it with a `deletedAt` timestamp. Active records return `deletedAt: null`. * Standard queries exclude it, but the database still stores it for audit and history. This keeps full traceability when you need it. ## Integrations *** Use **Fees Engine** in a Midaz deployment alongside other components in your stack. You can call its capabilities from Lerian plugins or your own implementation to apply fees from your business logic. Popular use cases include: * Exchange engines * Lending platforms * Bill payment systems * Smart contracts * Pix (Brazil's instant payment platform) ## Security recommendations *** **Security is foundational when you work with Lerian products and plugins.**\ Before you deploy any component, review our [**Security Recommendations**](/en/midaz/security-recommendations). Implement each product and its plugins in line with security best practices, such as: * Securing network boundaries * Managing and rotating secrets * Applying timely patch management * Enforcing strict role-based access controls (RBAC) These practices keep Lerian's products and plugins secure and compliant across your stack. ## Next steps *** Browse endpoints for fee packages, calculations, and estimations. Learn how to create fee packages and apply them to transactions. # Using Fees Engine Source: https://docs.lerian.studio/en/midaz/fees/using-fee-engine Use the Fees Engine to apply per-transaction fee packages and per-period billing packages that match your ledgers, segments, and transaction routes. Fees Engine applies per-transaction fees and calculates periodic billing charges. This guide covers both workflows: fee packages (per-transaction) and billing packages (per-period). Not sure which to use? Fee packages apply charges at transaction time. Billing packages calculate charges over a period (daily or monthly) for your orchestrator to execute. You can use both simultaneously. ## Fee packages: per-transaction workflow *** Fee packages apply charges synchronously when you create a transaction. This section shows the process, from fee setup to results. ### Step 1 - Create your fee packages First, set up **fee packages** that define how the engine applies fees. Use the [Create a Package](/en/reference/midaz/plugins/fees-engine/create-package) endpoint. Each package contains a set of fee rules and matching criteria. You can tailor packages to different ledgers, segments, and transaction routes with these fields: * **transactionRoute** - Identifies the nature of the transaction, when you need route-level matching. * **segmentId** - Groups customers or product types, when you need segment-level matching. * **ledgerId** - Defines which ledger records the transaction. * **Minimum** and **maximum** amount - Optional thresholds for fee application. * **routeFrom** / **routeTo** - Define how each fee moves across accounting flows. This flexibility lets you apply distinct fees per scenario, from account-based setups to value-based. **Managing Packages** The following endpoints are also available for you to manage the packages: * [List Packages](/en/reference/midaz/plugins/fees-engine/list-packages) - List all packages created. * [Retrieve a Package](/en/reference/midaz/plugins/fees-engine/retrieve-package) - Retrieve information of a specific package. * [Update a Package](/en/reference/midaz/plugins/fees-engine/update-package) - Update the information of a specific package. * [Delete a Package](/en/reference/midaz/plugins/fees-engine/delete-package) - Soft-delete a package. ### (optional) Step 2 - Run an estimation To preview how a fee package behaves before you commit a real transaction, use the [Estimate Transaction Fees](/en/reference/midaz/plugins/fees-engine/simulate-fees) endpoint. This estimation helps validate: * Which fee rules apply. * How the package will behave with the given values. * Whether any exemptions apply. ### Step 3 - Create a transaction Once you set the packages, create the transaction with the [create a transaction](/en/reference/midaz/create-a-transaction-using-json) endpoint. Include `ledgerId` and any configured matching fields, such as `transactionRoute` or `segmentId`, so the engine can evaluate the right package. ### Step 4 - The Fees Engine kicks in Fees Engine automatically calls the [Calculate Fees for a Package](/en/reference/midaz/plugins/fees-engine/calculate-fees) endpoint. It evaluates whether a package applies, based on: * **transactionRoute**, when configured * **segmentId**, when configured * **ledgerId** * **Minimum** and **maximum** amount * **waivedAccounts**, when configured for fee exemptions Fees Engine selects only one package per transaction. ### Step 5 - Check for exemptions The system checks: * If the transaction amount is outside the allowed range. * If the source account is exempt. If either condition is true, Fees Engine applies no fees and the transaction proceeds normally. ### Step 6 - Fee calculation and application If a package applies, Fees Engine: * Calculates fee values based on the selected `applicationRule`. * Applies fees proportionally across accounts if needed. * Uses `isDeductibleFrom` to set whether it adds or deducts the fee. * Routes fees to the correct `creditAccount` with the configured `routeFrom` and `routeTo`. * Returns the full transaction result along with the `packageAppliedID` in the metadata. ### Step 7 - Ledger updates Once Fees Engine calculates the fees, the **Transactions** component takes over. It processes: * Debits from source accounts * Credits to fee destinations * Fee breakdown per route and account The ledger stores every movement for full traceability and auditability. ### Step 8 - Review and confirm After execution, you can: * Inspect the final transaction and amounts per account. * Confirm which fee package applied. * Verify all fee movements via metadata and ledger records. ## Why estimate a transaction? *** Estimations let you preview how a specific fee package behaves, without running a real transaction or writing to the ledger. Use estimations when: * You want to test a specific package. * You debug fee rules or thresholds. * You want to validate exemptions, value ranges, or proportional splits. * You need a preview before you create a real transaction. * You build an interface and want to show estimated fees. Fees Engine provides the [Estimate Transaction Fees](/en/reference/midaz/plugins/fees-engine/simulate-fees) endpoint for this purpose. You pass a `packageId`, and the endpoint returns what would happen if it applied **that exact package**. ### What do you get with an estimation? * A full estimation of the fee rules. * Which accounts the engine would charge. * How the engine would split the fee. * No impact on the ledger. Use estimations when you're not ready to commit the transaction, or want to give your users a clear fee preview. ## Common errors *** Fees Engine validates every request for consistency and correct fee logic. Below are the most frequent issues you might see when you create packages or process transactions. | Code | Title | Message | | :------- | :---------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | FEE-0002 | Missing fields in request | Your request is missing one or more required fields. Please refer to the documentation to ensure all necessary fields are included in your request. | | FEE-0012 | Entity not found | No entity was found for the given ID. Please make sure to use the correct ID for the entity you are trying to manage. | | FEE-0013 | Invalid fee priority | The priority field in fees is invalid. Field can not be repeated. | | FEE-0015 | minimumAmount greater than maximumAmount | minimumAmount value is greater than maximumAmount. | | FEE-0022 | Failed to calculate fee | Error to make the calculation of a fee about a transaction. | | FEE-0024 | originalAmount is required when priority is one | For Priority equals to one, referenceAmount must be 'originalAmount' for fee. | | FEE-0025 | Failed to apply rule: flatFee or percentual | applicationRule flatFee or percentual must have exactly 1 calculation for Fee. | | FEE-0035 | Package amount range overlap | The maximumAmount and minimumAmount of the new package overlap with the amount range of an existing package. | Want the full list of error codes? You'll find it in the [Fees Engine error list](/en/reference/midaz/plugins/fees-engine/fee-engine-error-list) page in the [API reference](/en/reference/introduction). ## Billing packages: per-period workflow *** Billing packages calculate charges based on accumulated transaction volume or per-account maintenance over a billing period. Unlike fee packages, your orchestrator triggers billing. It decides when to calculate and executes the resulting charges. ### Step 1 — Create billing packages Set up billing packages that define your periodic charge rules. Each package is either **volume** or **maintenance** type. **Volume package example** — charge per Pix sent with tiered pricing: ```json theme={null} POST /v1/billing-packages Headers: X-Organization-Id: org_01HZ... Body: { "label": "Pix Send Monthly Billing", "description": "Monthly volume billing for Pix transactions", "ledgerId": "ldg_01HZ...", "type": "volume", "enable": true, "eventFilter": { "transactionRoute": "pix-send", "status": "APPROVED" }, "pricingModel": "tiered", "tiers": [ { "minQuantity": 1, "maxQuantity": 100, "unitPrice": "0.50" }, { "minQuantity": 101, "maxQuantity": 500, "unitPrice": "0.35" }, { "minQuantity": 501, "maxQuantity": null, "unitPrice": "0.20" } ], "freeQuota": 10, "discountTiers": [ { "minQuantity": 200, "discountPercentage": "5.00" }, { "minQuantity": 400, "discountPercentage": "10.00" } ], "countMode": "perRoute", "assetCode": "BRL", "debitAccountAlias": "client-wallet", "creditAccountAlias": "fees-revenue" } ``` **Maintenance package example** — monthly fee per active PF account: ```json theme={null} POST /v1/billing-packages Headers: X-Organization-Id: org_01HZ... Body: { "label": "PF Account Maintenance", "description": "Monthly maintenance fee for active PF accounts", "ledgerId": "ldg_01HZ...", "type": "maintenance", "enable": true, "feeAmount": "9.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pf", "accountTarget": { "segmentId": "seg_pf_01HZ..." } } ``` ### Step 2 — Trigger billing calculation Call `POST /v1/billing/calculate` with the ledger ID and billing period. The engine evaluates all active billing packages matching the criteria. ```json theme={null} POST /v1/billing/calculate Body: { "ledgerId": "ldg_01HZ...", "period": "2026-03", "type": "volume" } ``` The `period` field supports three formats: `YYYY-MM` (monthly), `YYYY-Www` (weekly, e.g., `2026-W13`), and `YYYY-MM-DD` (daily). The `type` field is optional. Use `"volume"` or `"maintenance"` to restrict the calculation to one type. Omit it to calculate both types in a single call. ### Step 3 — Receive calculation results The engine returns an array of results. Each result contains a `transactionPayload` ready to send to Midaz. Each result includes: * The billing package that generated it. * The calculated amounts with full breakdown (tiers applied, discounts, free quota subtracted). * A transaction payload with `source.from` (debit entries) and `distribute.to` (credit entries). * Structured audit metadata for traceability. ### Step 4 — Execute charges Send each `transactionPayload` to Midaz via `POST /transactions/json` to create the actual billing transactions. This step is your orchestrator's responsibility — Flowker, a cron job, or any other system. The billing engine calculates and returns results. It does not create transactions in Midaz. Your orchestrator controls when and how it executes charges. ### Step 5 — Review and reconcile After executing the charges: * Verify the created transactions in Midaz match the billing calculation results. * Use the audit metadata in each result for reconciliation. * The billing calculation is stateless — you can re-run it for the same period to verify results. ### Managing billing packages Use these endpoints to manage existing billing packages: * `GET /v1/billing-packages` — List all billing packages. * `GET /v1/billing-packages/:id` — Retrieve a specific billing package. * `PATCH /v1/billing-packages/:id` — Update a billing package (`label`, `description`, `enable` only). * `DELETE /v1/billing-packages/:id` — Soft-delete a billing package. If any package fails during calculation, the entire operation fails and returns no partial results. Billing calculation follows an all-or-nothing policy. Fix the failing package and re-execute. # Fees Engine in Lerian Console Source: https://docs.lerian.studio/en/midaz/fees/using-fee-engine-with-midaz-console Define, test, and apply fees visually with the Fees Engine in the Midaz Module — no code required for fee packages and estimations. The **Fees Engine** is available through the Midaz Module in [Lerian Console](/en/platform/console/about-lerian-console). It gives you a visual interface to define, test, and update how fees apply to transactions without writing code. ## What you can do *** Through the Lerian Console, you can: * **Create and manage Fee Packages**: Define fee rules with different calculation types (flat fee, percentage, or maximum between both) * **Run fee estimations**: Test how the engine calculates fees for specific amounts before you apply them to real transactions * **Associate fees with your data**: Link fee packages to specific ledgers, segments, or transaction routes ## Accessing the Fees Engine *** To access the Fees Engine in Lerian Console: 1. From the home page, select **Midaz** to enter the Midaz Module 2. In the left sidebar, go to **Plugins** 3. Select **Fees Engine** ## Learn more *** For complete documentation on using the Fees Engine through Lerian Console, see: Complete guide to managing fees through the Lerian Console interface. # Getting help Source: https://docs.lerian.studio/en/midaz/getting-help Find the right support channel for Midaz — community support for Community users and the Lerian Support Portal for Enterprise clients. When you get stuck, this section points you to the right support channel. It serves both first-time users and teams that run Midaz in production. Explore the resources below to get help, report issues, or contact the team. ## Content *** Here, you will find the following pages: * [**Community support**](/en/midaz/community-support): If you use the **Community** version of Midaz, start here. Join GitHub Discussions or Discord to ask questions, report bugs, and collaborate with others. * [**Lerian Support Portal**](/en/midaz/lerian-support-portal): For **Enterprise** clients with a support agreement, this is the official channel. Raise tickets, request services, and get help with production issues. Your SLA sets the guaranteed response times. # Prerequisites Source: https://docs.lerian.studio/en/midaz/installing-and-deploying-overview Prerequisites hub for installing and deploying Midaz — security recommendations, setup, updates, deployment strategies, Terraform, Helm, and Lerian MCP guides. This section shows you how to install, update, and deploy Midaz. You can run it locally or provision a full cloud-native setup. ## Content *** Here, you'll find: * [**Security recommendations**](/en/midaz/security-recommendations): Understand shared responsibilities, best practices, and the configurations you need to deploy Midaz and its plugins securely. * [**Installing Midaz**](/en/midaz/midaz-setup): Instructions to install and configure Midaz. * [**Updating Midaz**](/en/midaz/updating-midaz): Instructions to update your instance when you need to. * [**Deployment strategies**](/en/midaz/deployment): Learn about Midaz deployment strategies and platform requirements. * [**Midaz Terraform Foundation**](/en/midaz/midaz-terraform-foundation): Set up the Midaz cloud foundation with Terraform best practices. * [**Helm**](/en/platform/helm/helm-overview): Deploy, configure, and manage Midaz on Kubernetes with Helm (see the Platform section for full Helm documentation). * [**Version compatibility**](/en/platform/plugins/midaz-version-compatibility): Check which plugin versions work with each Midaz release to plan safe deployments and avoid integration issues. * [**Lerian MCP**](/en/midaz/lerian-mcp): Connect your AI assistant directly to the Midaz ecosystem. This gives your Large Language Model (LLM) secure, real-time access to Midaz documentation, APIs, and local services. By default, Midaz does not require authentication, so you can call the Midaz APIs directly. To require authentication, use the [Access Manager](/en/platform/access-manager/access-manager) plugin. A production deployment or multi-tenant mode always requires authentication, and Midaz does not start without it. For details, [contact us](https://lerian.studio/contact). # Lerian MCP Source: https://docs.lerian.studio/en/midaz/lerian-mcp Connect AI assistants like Claude, Cursor, or Windsurf to the Midaz ecosystem with secure, real-time access to docs, APIs, and local services. Lerian MCP connects your AI assistant directly to the Midaz ecosystem. Whether you use ChatGPT, Claude, Cursor, Windsurf, or another compatible client, this server gives your large language model (LLM) secure, real-time access to Midaz documentation, APIs, and local services. The assistant reads the docs, generates code, and runs actions for you, so you move faster. ## What is MCP? *** MCP stands for Model Context Protocol. Think of it as Bluetooth for LLMs. It is how your AI learns what tools it can use and how your system works. It also learns what questions to ask to finish the job. **Lerian MCP** teaches your assistant to: * Understand documentation, architecture, and SDKs. * Use APIs through built-in tools. * Ask follow-up questions to guide itself. Once connected, your assistant becomes context-aware and capable of action. ## Why use Lerian MCP? *** **Lerian MCP** turns your AI assistant into a development companion. It helps you: * Search Midaz documentation with natural language. * Call local Midaz APIs through tools, not just static text. * Understand Midaz's architecture, endpoints, and SDKs. * Generate code, troubleshoot issues, and automate setup. * Keep everything local and explicitly permissioned. Whether you build new integrations or support production systems, Lerian MCP gives your LLM the context it needs, safely and instantly. ## Built for security *** **Lerian MCP** keeps security first. It: * Runs entirely on **your machine**. * Has **read-only** access by default. * Requires your explicit approval before it writes data. * Needs no API keys for local setup. * Is source available and auditable. Your data stays where it belongs, under your control. ## What can your assistant do? *** Once connected, your assistant can interact with your Midaz environment as if it has already read the documentation. It can: * Explain how Midaz concepts work. * Generate code for real-world tasks (e.g., create an organization). * Search and summarize API endpoints. * Help debug integration issues. * Explore the architecture and available SDKs. * Run pre-configured tools to call your local API services. ### Examples of what you can ask * “How do I create a transaction in Midaz?” * “Show me the Go code to onboard an organization.” * "What's the difference between onboarding and transaction APIs?” * “Help me troubleshoot this 400 error.” * “List all Midaz account types.” ## Available tools and prompts *** **Lerian MCP** gives your assistant a small set of tools to explore documentation, call APIs, and learn through guided steps. Everything runs locally and read-first. ### Core tools * `lerian`: the read-oriented entry point for documentation, learning, SDK examples, product discovery, and search. * `portfolio-workflow`: runs cross-product workflows across Lerian products. ### The `lerian` tool The `lerian` tool takes an `operation` parameter. Each operation covers one need: * `discover`: summarize a product and the tools it exposes. * `docs`: look up product documentation. * `learn`: return guided learning for a topic. * `sdk`: return SDK code examples in Go or TypeScript. * `search`: search across product knowledge. ### Live Midaz API tools Two tools give your assistant live access to a running Midaz: * `midaz-discover`: returns the available resources, actions, parameters, and request schemas. It is read-only. * `midaz-execute`: calls the Midaz API with a contract from `midaz-discover`. Call `midaz-discover` before `midaz-execute`. A write action needs explicit confirmation and an audit reason. ### Built-in prompts **Lerian MCP** also ships built-in prompts that guide common tasks. For example, a prompt can help you onboard a first organization, learn a concept, or debug an API call. Each prompt adapts to your experience level and role. ## How does it work? *** **Lerian MCP** teaches your assistant using three key inputs: #### 1. Documentation Your LLM can immediately use anything you add to llms.txt, including product guides, examples, and concepts. #### 2. API tools The assistant learns how to call your APIs, including endpoints, required fields, and response formats, through structured tools. #### 3. Prompts and workflows It learns how to interact: what to ask, when to ask, and how to validate the next step. ### Tool invocation flow Most assistant interactions use this core flow. When you ask your assistant to “create an organization” or “get ledger details,” it follows this sequence: * The LLM calls the tool. * MCP validates and enriches the input. * The MCP server triggers the tool handler. * The tool sends the API request. * The tool returns the response to the assistant in real time. The following diagram shows how a tool invocation request flows through the MCP server. How a tool invocation flows through the Lerian MCP server, from the assistant's call through input validation and the API request to the returned response ### Tool discovery and registration Every time the MCP server starts, it registers its tools, such as `lerian`, `midaz-discover`, and `midaz-execute`. The server adapts these tools to the capabilities of each MCP client, such as Claude Desktop, Cursor, or ChatGPT. This keeps things up to date. After the server registers a tool, your assistant knows how to use it. The following diagram shows how Lerian MCP announces tools to your assistant and makes them usable within the client. How the Lerian MCP server registers its tools on startup and announces them to the assistant so they become usable inside the client ### Protocol-Level Error Handling If something goes wrong, such as a malformed tool call, an unexpected backend response, or a missing configuration, the MCP server returns a standardized error. In many cases, the assistant can recover and retry with better input or fallback logic. The following diagram shows how Lerian MCP detects, handles, and communicates protocol-level errors. How the Lerian MCP server detects a protocol-level error, returns a standardized error, and lets the assistant recover and retry ## From resources to tools *** Lerian MCP moved from static resources to interactive tools. Your assistant can now explore, generate, test, and troubleshoot directly. | Aspect | Resources | Documentation tools | | :-------------- | :------------- | :---------------------------------------- | | Client support | Limited | Full support across all MCP clients. | | Functionality | Static content | Contextual, interactive, dynamic. | | Examples | Basic text | Production-ready code. | | Search | None | Fuzzy search with filters. | | Troubleshooting | Not available | Diagnostic tools and prevention tips. | | Interactivity | Read-only | Guided tours, demos, and code generation. | ### What the tools unlock **Lerian MCP** tools help your assistant help you. They cover: * **API reference**: Get detailed endpoint docs with payloads, methods, and examples. * **Tutorials and guides**: Learn setup, onboarding, and best practices. * **Architecture**: Explore how components connect, including optional diagrams. * **SDK docs**: Access Go and TypeScript SDK documentation, with code examples. * **Code generation**: Generate working snippets for tasks like account creation or fund transfers. * **Workflow patterns**: Understand common flows like onboarding, reporting, and asset tracking. * **Troubleshooting**: Get real-time help resolving integration issues. * **Search and navigation**: Quickly locate relevant topics with advanced filters. * **Exploration tools**: Run health checks, try guided tours, and explore capabilities. Want the full reference with all tools and parameters? See the [lerian-mcp-server README on GitHub](https://github.com/LerianStudio/lerian-mcp-server). ## Getting started *** **Lerian MCP** works locally and integrates with multiple AI assistants. All you need is [Node.js](https://nodejs.org/en/download) installed and one of the supported tools below. Before you set it up, check which tools are available for your operating system: | Tool | Linux | macOS | Windows | Notes | | :--------------------- | :---------------------------------------------- | :------------------------------- | :------------------------------- | :---------------------------------------------------------------------- | | **ChatGPT Desktop** | Unofficial | Yes | Yes | Linux builds available via Flatpak and AppImage (community maintained). | | **Claude Desktop** | No | Yes | Yes | Not supported on Linux. | | **Claude Code (CLI)** | Yes | Yes | Yes | Terminal-based, works anywhere with Node.js. | | **Cursor IDE** | Yes | Yes | Yes | Electron-based, officially supports all platforms. | | **Windsurf IDE** | Yes | Yes | Yes | Linux support available, though slightly limited. | | **Continue (VS Code)** | Yes | Yes | Yes | VS Code extension, fully cross-platform. | For Linux users, we recommend using **Claude Code**, **Cursor**, **Windsurf**, or **Continue** for the best experience. Now, pick your assistant and follow the instructions below to connect Lerian MCP. ### ChatGPT Desktop * `~/Library/Application Support/ChatGPT/mcp.json` (macOS). * `%APPDATA%\ChatGPT\mcp.json` (Windows). ```bash Shell theme={null} { "mcpServers": { "lerian": { "command": "npx", "args": ["@lerianstudio/lerian-mcp-server@latest"] } } } ``` Restart the app. ### Claude Desktop * `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS). * `%APPDATA%\Claude\claude_desktop_config.json` (Windows). ```bash Shell theme={null} { "mcpServers": { "lerian": { "command": "npx", "args": ["@lerianstudio/lerian-mcp-server@latest"] } } } ``` Restart the app. #### Backward Compatibility The old package name (`@lerianstudio/midaz-mcp-server@latest`) still works but is now deprecated. Migrate to `@lerianstudio/lerian-mcp-server`. ```bash Shell theme={null} { "mcpServers": { "midaz": { "command": "npx", "args": ["@lerianstudio/midaz-mcp-server@latest"] } } } ``` ### Claude Code If you use Claude Code from the command line, use the following commands: * For a one-time setup, use: ```bash Shell theme={null} npx --yes @lerianstudio/lerian-mcp-server ``` * To add it to Claude Code, use: ```bash Shell theme={null} claude mcp add --scope user lerian -- npx --yes @lerianstudio/lerian-mcp-server ``` #### Migration from the old package If you enabled the MCP with the old package `@lerianstudio/midaz-mcp-server`, follow these steps: ```bash Shell theme={null} npm uninstall -g @lerianstudio/midaz-mcp-server ``` ```bash Shell theme={null} npm install -g @lerianstudio/lerian-mcp-server ``` ```bash Shell theme={null} npm install -g @lerianstudio/lerian-mcp-server ``` ```bash Shell theme={null} claude mcp remove midaz\ claude mcp add lerian "lerian-mcp-server" ``` ### Cursor IDE Go to **File** > **Preferences** > **Cursor Settings** > **MCP**. Click the **+Add new global MCP Server** button. Add the following code: ```bash Shell theme={null} { "mcp.servers": { "lerian": { "command": "npm", "args": ["exec", "@lerianstudio/lerian-mcp-server@latest"] } } } ``` Restart the app. ### Windsurf IDE Go to **File**> **Preferences** > **Windsurf Settings**. Click the **Manage plugins** button in the *Cascade* section. Click **View raw config**. Add the following code: ```bash Shell theme={null} { "mcpServers": { "lerian": { "command": "npm", "args": ["exec", "@lerianstudio/lerian-mcp-server@latest"] } } } ``` Save the file. Click **Refresh** in the **Manage plugins** tab. On Windsurf IDE, you must use the Cascade panel to ask about Midaz. ### Continue (VS Code) On VS Code, install the **Continue** extension and add the Lerian MCP code to the `config.yaml`file. You can find the file in the following locations: * `~/.continue/config.yaml` (MacOS / Linux). * `%USERPROFILE%.continue\config.yaml `(Windows). You can also open the file via VS Code: On VS Code, open the **Continue** panel from the activity bar (or press `cmd/ctrl + L`). Click the **Assistant** selector above the main chat input. From that dropdown, select the cog icon next to the "Local Assistant" option. It will open the local `config.yaml`. Add the following code and save the file: ```bash JSON theme={null} mcpServers: - name: Lerian command: npx args: - '@lerianstudio/lerian-mcp-server@latest' ``` Close and reopen VS Code. Open the **Continue** panel from the *Activity bar*. ## Need help? *** #### Something’s not working? Let’s get the basics out of the way first: **Restart your AI assistant** after saving the configuration. Are you editing the right config file? Ask your assistant, “Can you access Lerian documentation?” #### Still stuck? * **Using Claude Desktop?** - Make sure MCP is enabled in your version. * **Using any other AI app?** - Confirm that Node.js is installed on your machine. * **Need a hand?** - Open a ticket on [GitHub Issues](https://github.com/lerianstudio/lerian-mcp-server/issues) . #### Migrating from Midaz MCP? No worries, both packages work exactly the same: * You can use either `@lerianstudio/midaz-mcp-server` or `@lerianstudio/lerian-mcp-server`. * Lerian MCP supports both `MIDAZ_*` and `LERIAN_*` environment variables. * Config files work from either `.midaz/` or `.lerian/` folders. * CLI commands `midaz-mcp-server` and `lerian-mcp-server` are interchangeable. **How to switch:** Point your config to `@lerianstudio/lerian-mcp-server`. Restart your AI assistant. (Optional) Update your env vars from `MIDAZ_*` to `LERIAN_*`. (Optional) Move your config files to `.lerian/`. ## Ready to go? *** Your assistant is ready. Plug in the config, restart your app, and start building. # Lerian Support Portal Source: https://docs.lerian.studio/en/midaz/lerian-support-portal Open tickets, report incidents, and request services through the official Lerian Support Portal — for Enterprise clients with SLA-backed response times. The [Lerian Support Portal](https://lerian.atlassian.net/servicedesk) is the official support channel for **Enterprise clients**. Use it to get support, report issues, and manage your service requests. If something goes wrong, our team responds quickly. If you use the community version of Midaz, see the [Community support](/en/midaz/community-support) page. It explains how to ask questions, report bugs, and get help from the community. ### How it works Use the portal to open tickets, track progress, submit service requests, and report incidents. The portal routes each request to the right team based on its type and context. ## Incident types *** When you raise an incident, choose one of two categories: ### Non Transactional Incident Use this option for issues with **platform and operational products** such as: * Accounts * CRM * Reporter * Lifecycle (Distr) * Flowker * Other non-transactional services ### Transactional Incident Use this option for issues with **financial and payment operations** such as: * Transactions * Fees * Bank transfers * Payments * Pix If you are unsure which category fits, check whether the issue involves money movement or payments. If yes, choose **Transactional Incident**. If not, choose **Non Transactional Incident**. ## Request form fields *** Both incident types use the same form. When you register a request, provide complete and accurate information. This helps us resolve your issue faster. | Field | Description | | :---------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | | **Summary** | A short, clear description of the issue or request. | | **What are the details of your request?** | A detailed explanation of what happened, the expected behavior, and any relevant technical context. This is the most important field in the form. | | **Products** | Select the specific product related to your issue. The available options depend on the incident type you chose (Transactional or Non Transactional). | | **Environment** | Select the environment where the issue occurred (e.g., production, staging). | | **Business Function Impact** | Select how the issue impacts your business operations. | | **Current State Affected Service** | Select the severity level based on the current state of the affected service. See the priority table below for guidance. | | **Current Workaround** | Select whether a workaround is currently available. | | **Data Integrity Security** | Select if there are any data integrity or security concerns related to the issue. | | **Expects to be Resolved** | Select your expected resolution timeframe. | | **Users Affected** | Select the scope of users impacted by this issue. | | **Attachment** | Attach logs, screenshots, payloads (excluding sensitive data), or request IDs to help us investigate. | The more context you provide, the fewer follow-up questions we need to resolve your issue. ## Priority levels and SLA *** When you fill out the **Current State Affected Service** field, select the option that describes your service now. Each option maps to a priority level and an SLA: | Current State Affected Service | Priority | SLA (Response Time) | | :---------------------------------------------------------------- | :------------ | :------------------------------------- | | **Complete outage – service is unreachable** | P1 – Critical | Response within **15 minutes** | | **Severe degradation – service is very slow or partially broken** | P2 – High | Response within **30 minutes** | | **Minor degradation – some features are impaired** | P3 – Medium | Analyzed during regular prioritization | | **Cosmetic issue or minor inconvenience** | P4 – Low | Analyzed during regular prioritization | For P1 or P2 incidents, provide as much detail as possible in the description field. This helps our team engage quickly. ## How to open an incident *** To open an incident, follow these steps: Go to the [Lerian Support Portal](https://lerian.atlassian.net/servicedesk). Log in with your email and password. Select **Incidents**. Choose the incident type: **Non Transactional Incident** or **Transactional Incident**. Fill in all required fields, especially the description and product selection. Click **Submit**. ## Still need help? *** Not sure where your issue fits, even after you check the incident types? Our team can help. Email us at [**suporte@lerian.studio**](/). We route your message to the right team. # Environment variables Source: https://docs.lerian.studio/en/midaz/midaz-environment-variables Reference for Midaz ledger service environment variables — ports, TLS, deployment mode, and CRM or Fees settings for onboarding and transaction APIs. This reference lists the environment variables that configure the **Midaz ledger service**. Midaz is the source-available (ELv2) double-entry engine. It serves the onboarding and transaction APIs on a single port. You set these variables at deploy time, through Helm values, Docker Compose, or your orchestrator's environment. A required variable that you do not set makes the server fail on startup. Every Lerian product shares a set of configuration blocks: TLS posture, OpenTelemetry, Access Manager authentication, multi-tenancy, service discovery, and event streaming. The [BYOC configuration reference](/en/reference/byoc-configuration) documents these blocks. This page focuses on what is distinctive to the ledger. Consolidation has landed. You deploy the **ledger** service (unified onboarding + transaction routes), and **CRM** and **Fees** are compiled into that same ledger process — the ledger binary reads the CRM and Fees variables below. Tracer lives in the same repository and ships as its own optional service. The former `onboarding`, `transaction`, and `mdz` components no longer exist as separate deployables. The Helm chart still carries a legacy standalone `crm` deployment, disabled by default. ## Ports and health endpoints The ledger runs one HTTP process. See the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract. | Surface | Port variable | Default | Endpoints | | ------------------------------------------------------------------------------ | -------------------------------- | ------- | -------------------------------- | | Ledger HTTP (onboarding + transaction) | `SERVER_PORT` / `SERVER_ADDRESS` | `3002` | `/health`, `/readyz`, `/version` | | CRM HTTP (legacy standalone deployment, disabled by default in the Helm chart) | `SERVER_PORT` / `SERVER_ADDRESS` | `4003` | `/health`, `/readyz` | The ledger uses OTLP push for telemetry and does not expose a `/metrics` scrape endpoint. ## Deployment and TLS | Variable | Description | Default | Required | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `DEPLOYMENT_MODE` | Deployment flavor: `local`, `byoc`, or `saas`. In `saas`, TLS is mandatory for every dependency connection and the server refuses to start without it. In `byoc`, TLS is recommended and warned-on rather than enforced. Also tags the `/readyz` response. | `local` | No | | `ALLOW_INSECURE_TLS` | Bypass per-connection TLS enforcement on infrastructure DSNs. Leave unset or `false` in production; set `true` only for local plaintext infrastructure. | `false` | No | ## Application | Variable | Description | Default | Required | | --------------------------------- | --------------------------------------------------------------------------------------- | ------------- | -------- | | `ENV_NAME` | Environment label (for example `development`, `staging`, `production`) | `development` | No | | `VERSION` | Service version tag | varies | No | | `LOG_LEVEL` | Log verbosity: `debug`, `info`, `warn`, or `error` | `debug` | No | | `MAX_PAGINATION_LIMIT` | Maximum page size accepted by list endpoints | `100` | No | | `MAX_PAGINATION_MONTH_DATE_RANGE` | Maximum month span for date-range queries. The bundled example configuration ships `3`. | `1` | No | ## Database (PostgreSQL) The ledger keeps two logical databases — `onboarding` and `transaction` — each with a primary and a replica connection block. The variables share one shape. Replace `{MODULE}` with `ONBOARDING` or `TRANSACTION`. Replica variables carry a `_REPLICA_` infix (for example `DB_ONBOARDING_REPLICA_HOST`). | Variable | Description | Default | Required | | ---------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------- | -------- | | `DB_{MODULE}_HOST` | Primary PostgreSQL host | — | Yes | | `DB_{MODULE}_PORT` | PostgreSQL port | — | Yes | | `DB_{MODULE}_USER` | Database user | — | Yes | | `DB_{MODULE}_PASSWORD` | Database password. Sensitive — do not commit; supply through your secret store. | — | Yes | | `DB_{MODULE}_NAME` | Database name | `onboarding` / `transaction` | Yes | | `DB_{MODULE}_SSLMODE` | libpq SSL mode: `disable`, `require`, `verify-ca`, or `verify-full`. Use `require` or stronger in production. | `disable` | No | | `DB_{MODULE}_MAX_OPEN_CONNS` | Maximum open connections in the pool | `3000` | No | | `DB_{MODULE}_MAX_IDLE_CONNS` | Maximum idle connections in the pool | `3000` | No | ## Document store (MongoDB) The MongoDB variables use one namespace per module: `MONGO_ONBOARDING_*`, `MONGO_TRANSACTION_*`, and — on the consolidated binary — `MONGO_CRM_*` and `MONGO_FEES_*`. All share one shape. Replace `{NS}` with the namespace. They may point at one MongoDB deployment (separate logical databases) or dedicated hosts. | Variable | Description | Default | Required | | -------------------------- | ---------------------------------------------------------------------- | ------------------- | -------- | | `MONGO_{NS}_HOST` | MongoDB host | — | Yes | | `MONGO_{NS}_PORT` | MongoDB port | — | Yes | | `MONGO_{NS}_USER` | Database user | — | Yes | | `MONGO_{NS}_PASSWORD` | Database password. Sensitive. | — | Yes | | `MONGO_{NS}_NAME` | Database name | namespace name | Yes | | `MONGO_{NS}_URI` | Connection scheme: `mongodb` or `mongodb+srv` | `mongodb` | No | | `MONGO_{NS}_MAX_POOL_SIZE` | Maximum connection pool size | `1000` (Fees `100`) | No | | `MONGO_{NS}_TLS_CA_CERT` | Base64-encoded PEM CA certificate for TLS (for example AWS DocumentDB) | — | No | | `MONGO_{NS}_PARAMETERS` | Extra connection-string parameters | — | No | ## Cache (Redis / Valkey) | Variable | Description | Default | Required | | -------------------------------- | -------------------------------------------------------------- | ------- | -------- | | `REDIS_HOST` | Redis/Valkey host and port | — | Yes | | `REDIS_PASSWORD` | Authentication password. Sensitive. | — | No | | `REDIS_TLS` | Enable TLS for the connection | `false` | No | | `REDIS_CA_CERT` | Base64-encoded PEM CA certificate for TLS | — | No | | `REDIS_DB` | Logical database index | `0` | No | | `REDIS_PROTOCOL` | RESP protocol version | `3` | No | | `REDIS_POOL_SIZE` | Connection pool size | `10` | No | | `REDIS_MASTER_NAME` | Sentinel master name (Sentinel deployments) | — | No | | `REDIS_USE_GCP_IAM` | Authenticate to GCP Memorystore with IAM instead of a password | `false` | No | | `REDIS_SERVICE_ACCOUNT` | GCP service account for IAM auth | — | No | | `GOOGLE_APPLICATION_CREDENTIALS` | Path to GCP credentials file for IAM auth | — | No | ## Message broker (RabbitMQ) The transaction module uses RabbitMQ for balance operations and event fan-out. | Variable | Description | Default | Required | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `RABBITMQ_HOST` | Broker host | — | Yes | | `RABBITMQ_PORT_HOST` | **AMQP port** used to dial the broker. Despite the name, this is the port the connection string uses (`3003` in the bundled infrastructure). | — | Yes | | `RABBITMQ_PORT_AMQP` | **Management/HTTP port** reported on the health-check connection (`3004` in the bundled infrastructure). Despite the name, it is not used to dial AMQP. | — | No | | `RABBITMQ_URI` | Connection scheme: `amqp` or `amqps`. Use `amqps` in production. | `amqp` | No | | `RABBITMQ_DEFAULT_USER` | Publisher user | — | Yes | | `RABBITMQ_DEFAULT_PASS` | Publisher password. Sensitive. | — | Yes | | `RABBITMQ_CONSUMER_USER` | Consumer user | — | Yes | | `RABBITMQ_CONSUMER_PASS` | Consumer password. Sensitive. | — | Yes | | `RABBITMQ_VHOST` | Virtual host | `/` | No | | `RABBITMQ_NUMBERS_OF_WORKERS` | Consumer concurrency | `5` | No | | `RABBITMQ_NUMBERS_OF_PREFETCH` | Consumer prefetch count | `10` | No | | `RABBITMQ_TRANSACTION_ASYNC` | Record transactions asynchronously through the broker | `false` | No | | `RABBITMQ_TRANSACTION_EVENTS_ENABLED` | Publish transaction events. Any value other than `false` — including unset — enables publishing; set `false` explicitly to disable. The bundled example configuration ships `false`. | enabled | No | | `RABBITMQ_OVERDRAFT_EVENTS_ENABLED` | Publish overdraft events. Any value other than `false` — including unset — enables publishing; set `false` explicitly to disable. The bundled example configuration ships `false`. | enabled | No | | `AUDIT_LOG_ENABLED` | Append transactions to an audit log exchange. Any value other than `false` — including unset — enables it; set `false` explicitly to disable. The bundled example configuration ships `false`. | enabled | No | ## Throughput | Variable | Description | Default | Required | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- | | `BULK_RECORDER_ENABLED` | Batch transaction writes for high-throughput ingestion | `true` | No | | `BULK_RECORDER_SIZE` | Batch size trigger. When unset, Midaz derives it from `RABBITMQ_NUMBERS_OF_WORKERS` × `RABBITMQ_NUMBERS_OF_PREFETCH` (50 with the default worker and prefetch values). | workers × prefetch | No | | `BULK_RECORDER_FLUSH_TIMEOUT_MS` | Flush interval for the batch recorder (milliseconds) | `100` | No | | `BULK_RECORDER_MAX_ROWS_PER_INSERT` | Maximum rows per batch insert | `1000` | No | ## Tracer integration The optional seam lets the ledger reserve spending limits against Tracer before it commits a transaction. Leave `TRACER_BASE_URL` unset to disable it. | Variable | Description | Default | Required | | ---------------------- | ----------------------------------------------------------------------------- | ------- | --------- | | `TRACER_BASE_URL` | Tracer service URL; setting it enables the reservation client | — | No | | `TRACER_TIMEOUT_MS` | Reservation call deadline (milliseconds) | `250` | No | | `TRACER_TRANSPORT` | Reservation transport: `grpc` or `rest` | `grpc` | No | | `TRACER_TLS_MODE` | Seam security: `mesh` (default, TLS terminated by the service mesh) or `mtls` | `mesh` | No | | `TRACER_TLS_CERT_FILE` | Client certificate PEM path (when `mtls`) | — | If `mtls` | | `TRACER_TLS_KEY_FILE` | Client private-key PEM path (when `mtls`). Sensitive. | — | If `mtls` | | `TRACER_TLS_CA_FILE` | CA certificate PEM path (when `mtls`) | — | If `mtls` | With `TRACER_BASE_URL` set, the seam uses the default `grpc` transport unless you set `TRACER_TRANSPORT=rest`. The gRPC transport requires the Tracer service to expose its reservation gRPC seam — set `TRACER_GRPC_PORT` on Tracer (see [Tracer environment variables](/en/tracer/tracer-environment-variables)). Under `TRACER_TLS_MODE=mtls`, you must set the client certificate, key, and CA paths above. ## CRM and Fees The ledger process reads these variables — CRM and Fees are compiled into the ledger binary. They protect account-holder PII and configure the field-encryption backend. | Variable | Description | Default | Required | | ---------------------------- | --------------------------------------------------------------------------------------- | ------- | --------- | | `LCRYPTO_HASH_SECRET_KEY` | 64-hex hashing key for holder PII. Sensitive — generate a unique value per environment. | — | Yes (CRM) | | `LCRYPTO_ENCRYPT_SECRET_KEY` | 64-hex encryption key for holder PII. Sensitive. | — | Yes (CRM) | | `KMS_VENDOR` | Field-encryption backend: `none` or `hashicorp-vault` | `none` | No | | `KMS_VAULT_ADDR` | Vault address (when `hashicorp-vault`) | — | No | | `KMS_VAULT_AUTH_METHOD` | Vault auth method: `token` or `approle`. Use `approle` in `byoc`/`saas`. | `token` | No | | `KMS_VAULT_ROLE_ID` | Vault AppRole role ID (when `approle`) | — | No | | `KMS_VAULT_SECRET_ID` | Vault AppRole secret ID (when `approle`). Sensitive. | — | No | | `DEFAULT_CURRENCY` | Fallback fee currency (ISO 4217) | `USD` | No | ## Shared configuration backbone The following blocks are identical across Lerian products. The [BYOC configuration reference](/en/reference/byoc-configuration) documents them in full. They default off. A single-tenant BYOC deployment can ignore all the optional ones. * **Access Manager authentication** — `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_HOST`. Enable in production. * **Multi-tenancy** — `MULTI_TENANT_*`. Off by default. Enables per-tenant database resolution. * **Service discovery** — `SD_*` (Consul). Off by default. * **Event streaming** — `STREAMING_*` (lib-streaming producer). Off by default on the ledger. * **OpenTelemetry** — `ENABLE_TELEMETRY`, `OTEL_*`. Telemetry is OTLP push. # Installing Midaz Source: https://docs.lerian.studio/en/midaz/midaz-setup Install Midaz locally with Docker Compose in a few commands — the full source-available environment, ready for local development. Midaz is a source-available ledger. You run, extend, and test it locally. Install it with **Docker Compose**, which sets up the full environment. This guide walks you through the full setup, whether you build something new or explore how Midaz works. Midaz now ships as a single repository. You no longer install its services separately. ## System requirements *** | Requirement | Minimum | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Operating System** | - **Windows**: Windows 10 **via WSL** version 2004 or later (Build 19041+)
- **macOS**: Current or previous two versions
- **Linux**: Ubuntu or Debian recommended | | **RAM** | 8 GB | | **Disk Space** | 20 GB | | **Internet** | Stable connection | | **Browser** | Chrome, Firefox, or equivalent | ## Tools you’ll need *** * [Docker Desktop](https://docs.docker.com/get-docker/) * [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [Postman](https://www.postman.com/downloads/) (optional) * Package Manager: * macOS: [Homebrew](https://brew.sh/) * Linux: Use your system’s package manager (e.g., `apt`, `dnf`) ## On Windows? Install WSL *** On Windows, you must install WSL (Windows Subsystem for Linux) to run Midaz. Open PowerShell as an administrator. Run: ```bash Text theme={null} wsl --install ``` Restart your machine if needed. Once installed, open the Ubuntu app from the Start Menu to continue the setup. ## Install Midaz *** ### Step 1 – Open your terminal * **macOS/Linux:** Use your Terminal app. * **Windows:** Open Ubuntu via WSL. On Windows, **run all commands inside Ubuntu (WSL)**. New to terminals? See [this beginner guide](https://www.codecademy.com/learn/learn-the-command-line). ### Step 2 – Install `make` * **Linux:** ```bash Text theme={null} sudo apt install make ``` * **macOS:** ```bash Text theme={null} brew install make ``` ### Step 3 – Download the Midaz repo ```bash Text theme={null} cd ~/Desktop ``` ```bash Text theme={null} git clone https://github.com/LerianStudio/midaz.git ``` ```bash Bash theme={null} cd midaz ``` ### Step 4 – Set up environment variables Docker must already be running: besides copying the `.env` files, `make set-env` generates the `LCRYPTO_*` CRM keys through a Docker one-shot container and fails if Docker is unavailable. ```bash Bash theme={null} make set-env ``` ### Step 5 – Start the services Make sure Docker is up, then: ```bash Bash theme={null} make up ``` All bundled services start. ### Step 6 – Access your environment * Ledger API: [http://localhost:3002](http://localhost:3002/) Make sure Docker is up and all `.env` files are set before you access the API. ## Try the API *** Want to explore the Midaz APIs? Go to `midaz/postman`. Import `MIDAZ.postman_collection.json` into Postman. Explore available endpoints. Ready to build? Start with our [Quick Start Guide](/en/reference/quick-start-guide). ## Authentication *** By default, Midaz does not require authentication. This makes local testing easier. If you need authentication, use the [Access Manager](/en/platform/access-manager/access-manager) plugin, available for enterprise customers. Run Midaz without authentication only in a secured, private environment. ## Community and contribution *** * Join us on [Discord](https://discord.com/invite/DnhqKwkGv3). * Report issues on [GitHub](https://github.com/LerianStudio/midaz). * Follow [@LerianStudio](https://twitter.com/LerianStudio) on Twitter. Want to contribute? Start with our [Contributing Guide](https://github.com/LerianStudio/midaz/blob/main/CONTRIBUTING.md). Need expert help? [Talk to our team](https://lerian.studio/contact) for hands-on support. ## License *** Midaz is source-available under the [Elastic License 2.0](https://github.com/LerianStudio/midaz/blob/main/LICENSE). # Midaz Terraform Foundation Source: https://docs.lerian.studio/en/midaz/midaz-terraform-foundation Provision the base infrastructure for Midaz on AWS, GCP, or Azure with ready-made Terraform examples — networking, databases, and Kubernetes. Midaz Terraform Foundation is a repository of ready-made Terraform examples. Use them to create the base infrastructure that Midaz needs on AWS, GCP, or Azure. The examples follow each cloud provider's best practices. This base infrastructure includes: * Network (VPC, subnets) * DNS * Database * Redis/Valkey * Kubernetes cluster (EKS, GKE, or AKS) The templates provision a MongoDB-compatible database and a message broker only on some providers. AWS uses Amazon DocumentDB and Amazon MQ (RabbitMQ). Azure uses Cosmos DB with the MongoDB API. GCP has no managed equivalent, so you must provision MongoDB and RabbitMQ yourself on GCP. ## Why use it *** Infrastructure provisioning should not be slow, inconsistent, or error-prone. `midaz-terraform-foundation` follows Lerian's best practices for security, observability, and scalability. The tables below compare it to a manual or ad-hoc setup. ### Speed and standardization | **Criteria** | **With Midaz Terraform** | **Manual Setup / Ad-hoc Scripts** | | :------------------------ | :----------------------------------------------------------------- | :------------------------------------------- | | **Setup speed** | **Fast** – provisions everything in minutes with a single `apply`. | **Slow** – takes days to configure and test. | | **Architecture standard** | **Standardized** – follows Lerian's best practices. | **Unpredictable** – may be inconsistent. | | **Reusability** | **High** – supports multiple environments with minimal changes. | **Low** – hard to reuse across projects. | ### Security and observability | **Criteria** | **With Midaz Terraform** | **Manual Setup / Ad-hoc Scripts** | | :------------------------- | :-------------------------------------------------------------- | :------------------------------------------------------ | | **Security by default** | **Yes** – secure by design (isolated VPCs, IAM, secrets, etc.). | **No** – depends on the team, increasing exposure risk. | | **Built-in observability** | **Built-in** – integrates with Prometheus, Grafana, and more. | **Manual** – requires separate setup, often skipped. | | **Production-ready?** | **Yes** – high availability and autoscaling out of the box. | **Uncertain** – needs extra effort to harden. | ### Maintenance and support | **Criteria** | **With Midaz Terraform** | **Manual Setup / Ad-hoc Scripts** | | :---------------------------- | :----------------------------------------------------- | :-------------------------------------------- | | **Maintainability** | **Easy** – modular and versioned for painless updates. | **Hard** – scripts break easily. | | **Lerian support** | **Included** – verified and supported by Lerian. | **None** – not guaranteed. | | **Estimated deployment time** | **1 day** – including validation. | **1–2 weeks** – with higher operational risk. | Use this repository for a faster, tested setup. `midaz-terraform-foundation` follows Lerian's engineering standards. It helps you deploy faster and avoid common setup errors. ## What you’ll need *** Before you start, make sure you have: * [Terraform v1.5.0 or higher](https://developer.hashicorp.com/terraform/install) — the AWS RDS and Route 53 examples require `>= 1.5.0`; the other modules require `>= 1.0.0` * A cloud provider account (AWS, GCP, or Azure). * A storage bucket for Terraform state files. * The CLI tool for your cloud provider: * `aws` for AWS * `gcloud` for GCP * `az` for Azure ### CI/CD Integration This repository provides Terraform examples to deploy foundation infrastructure. It **does not include a CI/CD pipeline**. Create one that fits your project's needs. Do you already run a Terraform CI/CD pipeline? Follow these steps: **Skip the deployment script.** It is for local use only. Copy the relevant example configs into your private Infrastructure as Code repo. Integrate the Terraform configs into your pipeline as needed. Use your CI/CD platform’s built-in secret management to handle credentials securely. ## Project structure *** Each cloud provider has its own structure in the repository. Every infrastructure component follows a modular, controlled layout. You can deploy only the components you need, or the entire foundation. ```bash theme={null} . ├── examples/ ├── aws/ │ ├── vpc/ │ ├── route53/ │ ├── rds/ │ ├── documentdb/ │ ├── amazonmq/ │ ├── valkey/ │ └── eks/ ├── gcp/ │ ├── vpc/ │ ├── cloud-dns/ │ ├── cloud-sql/ │ ├── valkey/ │ └── gke/ └── azure/ ├── network/ ├── dns/ ├── database/ ├── cosmosdb/ ├── redis/ └── aks/ ``` ### Deployment order matters To avoid errors and connect everything correctly, deploy the components in this order: 1. VPC / Network 2. DNS 3. Database 4. Redis/Valkey 5. Kubernetes cluster ## Creating the state storage *** Terraform requires a remote backend to manage its state. Before you use these templates, create a storage bucket for the Terraform state files. ### AWS **Replace `REGION` and `UNIQUE_BUCKET_NAME` with your own values.** ``` aws s3api create-bucket \ --bucket UNIQUE_BUCKET_NAME \ --region REGION \ --create-bucket-configuration LocationConstraint=REGION ``` ``` aws s3api put-bucket-versioning \ --bucket UNIQUE_BUCKET_NAME \ --versioning-configuration Status=Enabled ``` ``` aws s3api put-bucket-encryption \ --bucket UNIQUE_BUCKET_NAME \ --server-side-encryption-configuration \ '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}' ``` ``` aws s3api put-public-access-block \ --bucket UNIQUE_BUCKET_NAME \ --public-access-block-configuration \ '{"BlockPublicAcls":true,"IgnorePublicAcls":true,"BlockPublicPolicy":true,"RestrictPublicBuckets":true}' ``` ### Google Cloud Platform ``` gsutil mb -l us-central1 gs://your-terraform-state-bucket ``` ``` gsutil versioning set on gs://your-terraform-state-bucket ``` ### Azure ``` az group create --name terraform-state-rg --location eastus ``` ``` az storage account create --name tfstate$RANDOM --resource-group terraform-state-rg --sku Standard_LRS ``` ``` az storage container create --name terraform-state --account-name ``` ## Configuration requirements *** Before you deploy the infrastructure, create and configure the variables file for each cloud component: ``` cd examples// cp midaz.tfvars-example midaz.tfvars ``` Replace all placeholders in the `midaz.tfvars` file with your actual values. \\ i. **This file holds the key configuration for your infrastructure setup.** ## Production credentials and deployment *** In production environments, you must manage credentials with care. This guide shows how to handle credentials securely. ### Cloud provider authentication When you run the deploy script locally, use the cloud provider's CLI authentication tools instead of raw credentials. This method is more secure. It manages credential rotation, MFA, and token refresh automatically. **Why adopt this approach?** * Tokens refresh automatically. * MFA and SSO integration out of the box. * It rotates and stores credentials securely. * Full audit trail for authentication events. #### AWS Use AWS CLI to assume a role. ``` aws sso login --profile your-profile ``` or ``` aws sts assume-role --role-arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME --role-session-name terraform ``` #### GCP Use gcloud authentication. ``` gcloud auth application-default login ``` **For service accounts**, use the following code: ``` gcloud auth activate-service-account --key-file=path/to/service-account.json ``` #### Azure Use Azure CLI. ``` az login ``` For service principals, use the following code: ``` az login --service-principal ``` ### Credential management best practices Stay safe and compliant by following your cloud provider’s official guidance: * **AWS**: [Managing AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html). * **GCP**: [Managing service account keys](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys). * **Azure**: [Identity management best practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/identity-management-best-practices). #### Recommended practices * Rotate credentials on a regular schedule. * Use role-based access control (RBAC) wherever possible. * Require MFA for user accounts. * Prefer short-lived, temporary credentials. * Monitor and audit credential use. * **Never** commit credentials to version control. ## Using the deploy script *** The `deploy.sh` script handles the setup sequence, highlights issues, and deploys each component in the correct order. ### What it does * Allows you to pick your cloud provider (AWS, Azure, or GCP). * Offers options to deploy or destroy the stack. * Checks that all backend configuration placeholders have values. * Runs Terraform commands in the right order for each component. * Outputs clear, color-coded logs so you know what’s happening at every step. ### How to use it Make sure that all **prerequisites are complete** and that you **created your remote state bucket**. Fill in all the **placeholders** in the `backend.tf` files. ``` chmod +x deploy.sh ``` ``` ./deploy.sh ``` When prompted, select your cloud provider. i. Check the remaining placeholders. \\ ii. Run `terraform init`, `plan`, and `apply` for each component. \\ iii. Deploy in the correct order and stop if something fails. ### Error handling We built the script to fail quickly and provide an explanation. If something goes wrong, it will: * Stop immediately if it finds placeholders you forgot to fill in. * Exit if any Terraform command fails. * Show you exactly which component failed and at what step. ## Installing Midaz *** After deploying the foundation infrastructure, you can install Midaz using Helm. For more information, refer to the [Deploying using Helm](/en/platform/helm/midaz/midaz-installation) page. #### Prerequisites * A running Kubernetes cluster (EKS, GKE, or AKS). * `kubectl` configured to access the cluster. * Helm v3.x installed. * Access to the [Midaz Helm repo](https://github.com/LerianStudio/helm). ### Install steps Add the Midaz Helm repository: ``` helm repo add midaz https://lerianstudio.github.io/helm helm repo update ``` Create a values file (`values.yaml`) with your configuration: ```bash expandable theme={null} # Example values.yaml # Disable the bundled dependencies valkey: enabled: false postgresql: enabled: false ## Point the ledger at your external PostgreSQL and Valkey/Redis. ## The ledger serves the onboarding and transaction modules in one process, ## so the DSNs are namespaced per module (DB_ONBOARDING_* / DB_TRANSACTION_*). ledger: configmap: DB_ONBOARDING_HOST: "postgresql.midaz.internal" DB_ONBOARDING_USER: "midaz" DB_ONBOARDING_NAME: "onboarding" DB_ONBOARDING_PORT: "5432" DB_ONBOARDING_REPLICA_HOST: "postgresql-replica.midaz.internal" DB_ONBOARDING_REPLICA_USER: "midaz" DB_ONBOARDING_REPLICA_NAME: "onboarding" DB_ONBOARDING_REPLICA_PORT: "5432" DB_TRANSACTION_HOST: "postgresql.midaz.internal" DB_TRANSACTION_USER: "midaz" DB_TRANSACTION_NAME: "transaction" DB_TRANSACTION_PORT: "5432" DB_TRANSACTION_REPLICA_HOST: "postgresql-replica.midaz.internal" DB_TRANSACTION_REPLICA_USER: "midaz" DB_TRANSACTION_REPLICA_NAME: "transaction" DB_TRANSACTION_REPLICA_PORT: "5432" # REDIS_HOST carries host and port together. REDIS_HOST: "valkey.midaz.internal:6379" secrets: DB_ONBOARDING_PASSWORD: "" DB_ONBOARDING_REPLICA_PASSWORD: "" DB_TRANSACTION_PASSWORD: "" DB_TRANSACTION_REPLICA_PASSWORD: "" REDIS_PASSWORD: "" ``` Install Midaz: ``` helm install midaz midaz/midaz -f values.yaml ``` For detailed configuration options and advanced setup, please refer to the [Midaz Helm Repository](https://github.com/LerianStudio/helm). ## Security tips *** The cloud brings opportunities and responsibilities. To keep your Midaz infrastructure secure, follow these recommendations: * Always use **private Kubernetes clusters** to limit public exposure. * Access the **Kubernetes API via VPN** instead of allowing public access. * Set up and **enforce RBAC** (Role-Based Access Control) to manage user permissions effectively. * Store all secrets in the cloud provider’s **secret management service**. * Give service accounts only the **permissions they truly need**. ## Contributing *** Before you make any changes, set up Git hooks. Git hooks make sure every commit follows our standards and passes the required checks. ``` make hooks ``` ``` git checkout -b feature/your-feature ``` Make your changes and commit using Conventional Commits. Open a pull request targeting the `develop` branch. After tests pass and a maintainer approves, your changes merge into `main`. Check out our [Contributing Guide](https://github.com/LerianStudio/lerian-terraform-foundation/blob/main/CONTRIBUTING.md) to learn more about how we work together and what we expect from contributors. ## License *** Midaz Terraform Foundation uses the [Apache License 2.0](https://github.com/LerianStudio/lerian-terraform-foundation/blob/main/LICENSE). ## Need help? *** * Check the README inside each component folder. * Search existing [issues](https://github.com/LerianStudio/lerian-terraform-foundation/issues). * Open a new issue if needed. # Observability Source: https://docs.lerian.studio/en/midaz/observability-in-midaz Monitor Midaz metrics, logs, and traces with OpenTelemetry, Client Collectors, and Lerian's managed Prometheus, Loki, Tempo, and Grafana observability stack. Midaz gives you metrics, logs, and traces in one place. Monitor performance and find issues before they affect your customers. Observability in **Midaz** lets you monitor metrics, logs, and traces across your deployed environments. You understand system behavior, find issues, and keep operations healthy. Midaz collects telemetry with the **OpenTelemetry framework**. A **Client Collector** in your infrastructure processes it. The collector then routes it securely to Lerian's managed stack: Prometheus, Loki, Tempo, and Grafana. ## How it works *** Midaz uses a **dual observability model**: * **Client side** — For local development, the collector configuration lives in `components/infra/grafana/otelcol-config.yaml`, mounted read-only into the bundled all-in-one Grafana/OTel container. It sets the collector's processors, filters, and exporters. For production and BYOC, configure the `otel-collector-lerian` subchart values instead. * **Lerian side** — Lerian operates the managed collector and storage stack. Lerian keeps it secure and consistent. The bundled collector config masks known sensitive keys — credentials, tokens, `legalDocument`, `accountAlias` — in log bodies and in the `app.request.payload` span attribute. Masking happens in the collector, so it applies after telemetry leaves the application process. The bundled local-development exporters run with TLS disabled (`insecure: true`); enable TLS on the collector exporters for any non-local deployment. ## Key benefits *** * Unified view of metrics, logs, and traces in Grafana * Built on OpenTelemetry standards * Grafana preconfigured and operated by Lerian * Secure routing and tenant isolation **Want full configuration details?** See [**Infra Tools → Observability**](/en/platform/observability) for advanced setup, collector configuration, and processor customization. # Security Source: https://docs.lerian.studio/en/midaz/security See how Midaz protects financial operations with security by design, STRIDE threat modeling, OWASP-aligned controls, and a clear shared responsibility model. Security is the foundation of any financial system. Midaz builds in protections for data integrity, access control, and tenant isolation. It uses strong identity management, fine-grained permissions, and industry-standard practices. This page describes the security architecture Midaz provides by default. It also shows how you run secure and compliant operations. ## Architecture *** Midaz uses a secure architecture. It applied **security by design** and **threat modeling** from the start. It still applies both to every new feature. * **Security by design**: Midaz embeds security controls across the lifecycle, from design to deployment. It follows OWASP guidelines such as the **OWASP Top 10** and the **OWASP Application Security Verification Standard (ASVS)**. * **Threat Modeling**: A structured process identifies, assesses, and reduces security risks before an attacker exploits them. Midaz uses the **STRIDE** methodology. STRIDE groups threats into six types: * **S**poofing (e.g., fake authentication in a banking API). * **T**ampering (e.g., altering transaction data mid-request). * **R**epudiation (e.g., lack of audit logs for transactions). * **I**nformation Disclosure (e.g., leaking sensitive data via API responses). * **D**enial of Service (e.g., overwhelming the API with fake requests). * **E**levation of Privilege (e.g., exploiting a bug to gain admin access). ## Shared responsibility model *** Security is a **shared responsibility** between Lerian and the customer. The exact split depends on your [deployment model](/en/deployment-models). | Lerian | Customer | | ---------------------------- | ---------------------------- | | Application Development | Infrastructure | | Application Security Updates | Network | | | Database | | | Identity & Access Management | | | Encryption | | | User Data | | | Monitoring | | | Additional Security Layers | ### In the BYOC model In BYOC (Bring Your Own Cloud), you deploy Lerian in your own infrastructure. Lerian secures the **application layer**. You secure the **environment**. **What Lerian secures:** * Secure-by-design services. * Proactive vulnerability fixes. * Security updates: dependency upgrades, security patches, and improvements. **What the customer secures:** * **Infrastructure**: Harden the OS and container images, manage patches, and apply secure configurations on the hosting platform. * **Network**: Implement segmentation, firewalls, and IDS/IPS systems. Adopt Zero Trust principles to protect internal and external communication. * **Database**: Set up backups and audit logging. Follow security best practices for data storage. * **Identity & Access Management**: Control access to the environment. Use Midaz's RBAC features to enforce least-privilege policies within the platform. * **Encryption**: Encrypt sensitive data at rest and in transit. Consider tokenization or anonymization where appropriate. * **User Data**: All user data that Midaz stores or processes stays under your control and responsibility. * **Monitoring**: Establish monitoring tools that detect unusual access patterns or suspicious behavior. * **Additional Security Layers**: Reinforce defenses with Web Application Firewalls (WAF), anti-DDoS mechanisms, and bot mitigation tools. ### In the SaaS model In SaaS, Lerian manages the full infrastructure. Lerian takes on more of the security responsibility. **What Lerian secures:** * Everything in the BYOC application layer, plus: * Cloud infrastructure, networking, and compute environment. * Database provisioning, encryption at rest, and automated backups. * OS and container patching. * Monitoring, alerting, and incident response. * High availability and disaster recovery. **What the customer secures:** * **Business-level access control**: Manage users, roles, and permissions within the platform. * **API integration security**: Secure the communication between your systems and the Lerian APIs. * **User Data governance**: Define and enforce data-handling policies that meet your regulatory obligations. * **Compliance**: Make sure your use of the platform aligns with your institution's regulatory requirements. For step-by-step guidance, see [Security Recommendations](/en/midaz/security-recommendations) in the Installing & Deploying section. ## Identity and Access Management *** Midaz accepts a **Bearer JWT** issued by an OAuth 2.0 / OpenID Connect provider. Authentication is off unless you set `PLUGIN_AUTH_ENABLED=true` — and Midaz refuses to start without it when `ENV_NAME=production` or multi-tenancy is enabled. You choose how to manage identity and access. You have two options: * **Use your own external IAM** (Identity and Access Management) solution. * **Use Lerian's native Access Manager Plugin** — a good fit for customers without an existing IAM system, or those who want a fully integrated experience. ### Option 1: External IAM If you integrate your own IAM provider, make sure it follows modern security practices. To keep Midaz secure, we recommend that you: * Use proven protocols such as **OAuth 2.0** and **OpenID Connect**. * Require **Multi-Factor Authentication (MFA)**. * Apply strong password hashing algorithms such as **bcrypt** or **argon2**. * Enforce fine-grained access controls with **RBAC**, **ABAC**, or similar models. * Manage sessions securely, with expiration rules and refresh-token policies. * Protect endpoints from brute-force and replay attacks. * Enable and review **access logs** regularly. ### Option 2: Access Manager Plugin The [Access Manager Plugin](/en/platform/access-manager/access-manager) manages authentication and authorization inside Midaz. It is fully integrated, and it provides: * User lifecycle management * Session token handling * Refresh token rotation * Application registration and management This option simplifies secure access control. It aligns with Midaz's native permission system (RBAC). ## Tenant isolation in multi-tenant deployments *** On Lerian SaaS or BYOC Multi-Tenant, Midaz isolates all resources by tenant at the application layer. This covers organizations, ledgers, accounts, and transactions. Your JWT access token carries the tenant context. On every request, the platform middleware resolves the tenant from the `tenantId` claim in the token. Your API calls never see data from other tenants. Other tenants never see your data. This isolation works independently of the organization hierarchy. Two tenants can create similar organization structures, and their data stays completely separate. In `DATABASE` [isolation mode](/en/multi-tenancy#isolation-modes), each tenant uses its own dedicated database. In `SCHEMA` mode, tenants share a database, but each tenant's data stays isolated behind its own distinct credentials. ### Credential isolation Each tenant has its own credentials. Midaz never shares them between tenants, in both `DATABASE` and `SCHEMA` [isolation modes](/en/multi-tenancy#isolation-modes). Under `SCHEMA` mode, tenants share a database instance. Even so, each tenant's access uses its own distinct credentials. One tenant can never authenticate into another tenant's data. Midaz generates credentials during [automatic provisioning](/en/multi-tenancy/auto-provisioning). It stores them in a **credentials vault**, not in configuration files or environment variables. You can rotate credentials on demand while the platform continues to serve requests. The rotation causes no downtime and no interruption to tenant operations. ### Per-tenant resource limits Multi-tenant deployments enforce resource limits so that no single tenant degrades others. This is the **noisy neighbor** problem: * **Kubernetes resource limits** — CPU and memory limits on each workload cap how much compute it can consume. This bounds the impact of a spike or a runaway workload. * **PostgreSQL statement timeout** — Midaz does not set one itself. Configure `statement_timeout` on your PostgreSQL roles or databases so no single query can hold resources indefinitely. * **Connection pool per service** — each service keeps its own connection pool, with one pool per active tenant. Per-tenant pool limits come from the connection settings each tenant carries in Tenant Manager, and idle tenant pools are evicted over time. This bounds connection capacity per tenant. One tenant cannot exhaust the database connections that others need. These limits complement the data isolation above. Data isolation protects each tenant's *data* from other tenants. Resource limits protect each tenant's *performance and availability* from other tenants' load. For a full overview of multi-tenancy, see [Multi-tenancy](/en/multi-tenancy). ## Data protection *** Midaz enforces **double-entry principles** by design. Every transaction must have balanced debits and credits. Midaz rejects any entry that fails this validation. This protects **ledger integrity**. It also protects the system against **race condition vulnerabilities** and posting discrepancies. ### Built-in safeguards Midaz applies strict validation across all transaction flows to keep data consistent and prevent logic errors: * **Midaz blocks negative balances**, unless you explicitly permit them. * **Midaz verifies account status** before any operation. * **Midaz requires a registered, valid asset** before it posts an entry. These checks reduce risk. They keep financial operations aligned with business rules and regulatory expectations. ### Compliance with LGPD and GDPR Midaz handles transaction validation and secure communication over **TLS 1.2 and 1.3**. You protect **personally identifiable information (PII)**. To stay compliant with **LGPD**, **GDPR**, and similar data protection laws, we recommend that you: * Apply **encryption** to sensitive data, at rest and in transit. * Use **tokenization** or **anonymization** when appropriate. * Store and manage customer data under clearly defined security policies. Data protection is a shared commitment, not a single layer. Midaz provides the foundation. You build the safeguards. ## Responsible disclosure policy *** Transparency builds trust. We share all known security improvements and fixes openly in our [**GitHub Discussions**](https://github.com/LerianStudio/midaz/discussions). This keeps the community informed about security patches and enhancements. If you find a security vulnerability in Midaz, report it directly to our team before you make it public. We support responsible disclosure. We investigate and resolve issues quickly and thoroughly. Do not disclose any findings publicly until we review and address them. The steps to report a vulnerability: Email us at [security@lerian.studio](mailto:security@lerian.studio). We respond within 24 hours. Our team validates the report. We determine the severity and impact. We fix the issue and notify the reporter. We coordinate disclosure with the researcher. Use a PGP key for secure communication. We prioritize confidentiality and fast resolution of all security reports. # Security recommendations Source: https://docs.lerian.studio/en/midaz/security-recommendations Apply Lerian's shared responsibility security model — infrastructure hardening, network segmentation, TLS, and Zero Trust patterns. Read this guide before you install and deploy Midaz. Security is critical. At Lerian, we work under a **Security Shared Responsibility Model**. We secure the application layer: Midaz and all Lerian plugins. You secure the infrastructure where they run. This guide helps your team build and keep a strong security posture across the Lerian ecosystem, from Midaz to every integrated plugin. These recommendations are not exhaustive. They complement your existing security practices, whether you run in the cloud or on-prem. ## Infrastructure & network security *** Midaz and all Lerian plugins are cloud-native. They run in containerized, distributed environments. To strengthen your infrastructure and network security: * Establish a **patch management process** that updates the OS and services with security patches on a regular schedule. * Apply **hardening** procedures to your operating system images before deployment. * Use the **Midaz and plugin Docker images**, which follow the **least privilege principle**. Reinforce this at the cluster level with the right controls. * For **Kubernetes**, enforce security baselines with **policy-as-code** tools such as **Kyverno** or **OPA Gatekeeper**. * Run **routine vulnerability scans** across all layers of your infrastructure, including the layers that host plugins. * Use **network segmentation** to restrict access to Midaz components and plugins to only the systems that need it. * Configure **firewalls** to manage traffic flow and prevent unauthorized access. * Where possible, implement **Intrusion Detection Systems (IDS)** and **Intrusion Prevention Systems (IPS)** to analyze network behavior and block anomalies. * Adopt a **Zero Trust Architecture** for sensitive systems and data flows. * Enforce **TLS 1.2 or higher** for all communication across Midaz and plugin services. ## General security configurations *** You must protect secrets, storage, and data across all Lerian components, including Midaz and its plugins. Follow these practices: * Use trusted solutions such as **AWS Secrets Manager**, **Azure Key Vault**, **Google Secret Manager**, or **HashiCorp Vault** to store secrets and configuration data securely. * **Rotate credentials and secrets** on a regular schedule. Follow the principle of least privilege when you grant access. * Enable **audit logging**. Configure **automated backups** for all databases that Midaz and plugins use. * Apply **encryption at rest** to all persistent data, including the data that plugins such as Access Manager process. * Enforce **access restrictions** on sensitive data. Use **tokenization**, **encryption**, or **anonymization** where appropriate. * Update Midaz and all Lerian plugins to the **latest releases**, including patches and security improvements. ## Identity & Access Management (IAM) *** Apply a unified IAM approach across two layers — the environment where you deploy Midaz and plugins, and the services themselves. Follow these practices: * Implement a centralized IAM process to manage access to the infrastructure that hosts Lerian products. * Enforce **multi-factor authentication (MFA)** across all user and admin accounts. * Monitor and audit access to **service accounts** on a regular schedule, especially the accounts that plugins use. * Define access control models that follow the **least privilege principle** with **RBAC**, **ABAC**, or custom strategies. * Use **temporary credentials** and time-limited access for high-sensitivity operations. * Review all access privileges on a regular schedule, including access to Midaz, Access Manager, Reporter, and other products. ## Monitoring & incident response *** To build observability and incident readiness into how you deploy Midaz and plugins: * Deploy security monitoring tools to track behavior across all Lerian services, including plugin activity. * Enable logs and traces across your infrastructure and each component of Midaz and its plugins. * **Integrate logs** from Midaz and plugin services with fraud detection or anomaly monitoring tools. * Centralize logs in a **SIEM platform** for more efficient threat detection. * Maintain an updated **incident response plan**, with specific playbooks for services such as Access Manager or Fees Engine. ## Additional protection layers *** For production-critical or high-compliance environments, add these protective layers: * Apply OS and service **hardening procedures** across your entire stack. * Maintain **regular patch cycles** for all runtime environments, including third-party services and dependencies. * Conduct **manual penetration tests** to validate your defenses, including the defenses around plugins and integrations. * Deploy **Web Application Firewalls (WAFs)** to protect plugin APIs and Midaz endpoints from Layer 7 attacks. * Use **Anti-DDoS services** and **bot mitigation tools** to block automated or mass-scale attacks on exposed components. Combine infrastructure-level protections with the built-in security validations of **Midaz and its plugins**. Together they create a compliant, scalable financial environment. ## Midaz security configurations *** Security starts with configuration. Review and adjust all credentials and certificates before you go live, whether you run Midaz locally or deploy it in a managed cluster. ### Credentials and secrets Each Midaz component has its own `.env` file for per-module configuration. This gives you flexibility and a shared responsibility. Never deploy Midaz with default credentials. Update all secret values in your environment before you launch. Review and update the following files to protect sensitive information: | **Midaz Repo** | **Midaz Helm** | | :----------------------------- | :-------------------------------------------------- | | `/components/infra/.env` | `/charts/midaz/values.yaml` | | `/components/mdz/.env` | `/charts/midaz/templates/onboarding/secrets.yaml` | | `/components/onboarding/.env` | `/charts/midaz/templates/transactions/secrets.yaml` | | `/components/transaction/.env` | `/charts/midaz/templates/console/secrets.yaml` | Store secrets such as database credentials, encryption keys, and API tokens securely. Rotate them on a regular schedule. ### TLS Certificate configuration When you expose Midaz services through ingress controllers, use valid TLS certificates. Valid certificates secure the communication between services and external clients. You can enable ingress for the following Midaz components: * **Transaction** * **Onboarding** * **Console** To configure a custom TLS certificate: **Generate** a valid certificate with a trusted provider (for example, Let’s Encrypt or GoDaddy). **Store** the full certificate chain as a Kubernetes secret or in an external vault (for example, AWS Secrets Manager). **Reference** the secret in your `values.yaml` file: ```bash YAML theme={null} ingress: ///class, host, annotations, etc tls: - secretName: certificate-tls hosts: - midaz.example.com ``` Always enforce TLS 1.2 or higher. For internal clusters, issue short-lived certificates automatically with a service mesh or cert manager. # Updating Midaz Source: https://docs.lerian.studio/en/midaz/updating-midaz Update your Midaz environment safely — local Docker upgrades and Helm-based rollouts to keep your installation secure and current. Keeping Midaz up to date means you're always working with the latest features, security patches, and improvements. Regular updates also keep everything running smoothly with other components and help prevent issues caused by outdated dependencies. Below are the steps to update Midaz in your local environment and using Helm. From Midaz 3.x onwards, schema migrations are applied by dedicated migration-runner images (`midaz-ledger-migrations`, `midaz-tracer-migrations`) that run **before** the app rollout — the apps no longer migrate at startup. Local `make up` and the official Helm chart wire this up automatically. See [Database migrations](/en/midaz/database-migrations) for details. ## Updating Midaz locally *** If you're running Midaz locally for development or testing, follow these steps to update your environment: ### 1. Stop the running containers Shut down all running services before making updates. This ensures a clean restart with the latest updates. To stop the containers via the terminal, follow these steps: Open your terminal and navigate to the `midaz` directory. Run the stop command: ```Shell Shell theme={null} make down ``` ### 2. (Optional) Clean up unused Docker resources Cleaning up unused resources helps keep your environment efficient: * **Free up disk space**: Prevent old images, containers, and volumes from eating up storage. * **Avoid conflicts with outdated dependencies**: Ensure you're working with the latest configurations. * **Improve performance**: Reduce unnecessary Docker cache layers and optimize resource management. * **Prevent debugging headaches**: Eliminate inconsistencies caused by outdated data lingering in the system. * **Keep your workspace tidy**: Maintain an organized development setup, especially when switching between projects. To remove unused images and containers: ```Shell Shell theme={null} docker system prune --all ``` To remove unused Docker volumes: ```Shell Shell theme={null} docker volume prune ``` ### 3. Update your local codebase Fetch the latest changes from the repository to ensure you're working with the most recent version: ```Shell Shell theme={null} git pull origin main ``` ### 4. Rebuild and restart Midaz Rebuild necessary Docker images and relaunch the application locally with the latest updates: ```Shell Shell theme={null} make up ``` ## Updating Midaz with Helm *** For deployments using Helm, use the following commands based on your needs. Helm simplifies Kubernetes-based deployments, making it easy to install, upgrade, or remove Midaz. ### Fresh installation Perform a fresh installation when setting up Midaz for the first time or when a clean deployment is required. #### Install Midaz Helm with a custom values file Use this method if you have specific configuration requirements defined in `values.yaml`: ```Shell Shell theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --values values.yaml --version -n midaz --create-namespace ``` #### Install Midaz Helm with default values Use this method to install Midaz using the default Helm chart values: ```Shell Shell theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version -n midaz --create-namespace ``` ### Updating an existing deployment If Midaz is already deployed, use these commands to upgrade to a new version while preserving configurations. #### Updating Midaz Helm with a custom values file Ensure that custom configurations remain intact while applying the update: ```Shell Shell theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --values values.yaml --version -n midaz --create-namespace ``` #### Updating Midaz Helm with default values Use this command if you do not need to apply custom configurations: ```Shell Shell theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version -n midaz --create-namespace ``` ## Managing the Helm release *** Use these commands to check and manage your Midaz Helm deployment. ### List installed releases List all Helm releases in the `midaz` namespace, along with their status and version: ```Shell Shell theme={null} helm ls -n midaz ``` ### Delete the Midaz Helm release If you need to completely remove Midaz from your cluster, run: ```Shell Shell theme={null} helm delete midaz -n midaz ``` By keeping Midaz updated, you ensure a stable, secure, and high-performing environment while benefiting from the latest enhancements and best practices. ```Shell Shell theme={null} helm delete midaz -n midaz ``` # Midaz in Lerian Console Source: https://docs.lerian.studio/en/midaz/using-midaz-in-lerian-console Manage Midaz visually inside the Lerian Console — Organizations, Ledgers, Accounts, Transactions, and plugins from a single web interface. Midaz is a product module in the [Lerian Console](/en/platform/console/about-lerian-console), the web interface for all Lerian products. Through the Console, you manage your full ledger infrastructure — from organizations and assets to transactions and plugins. Access the full documentation for using Midaz in the Lerian Console. ## Available actions *** From the Lerian Console, you can: * **Foundation** — Manage [organizations](/en/midaz/console/managing-organizations), [ledgers](/en/midaz/console/managing-ledgers-via-console), and [assets](/en/midaz/console/managing-assets) * **Accounting** — Configure [account types](/en/midaz/console/managing-account-types) and [routes](/en/midaz/console/managing-accounting-routes) * **Accounts** — Manage [accounts](/en/midaz/console/managing-accounts), [holders](/en/midaz/console/managing-crm-holders), [aliases](/en/midaz/console/managing-crm-alias-accounts), [segments](/en/midaz/console/managing-segments), and [portfolios](/en/midaz/console/managing-portfolios) * **Transactions** — [View and create transactions](/en/midaz/console/managing-transactions) * **Plugins** — Configure integrated capabilities such as [Fees Engine](/en/platform/console/mc-console-plugins) # Contexts and sources Source: https://docs.lerian.studio/en/matcher/configuration/matcher-contexts-and-sources Set up a reconciliation context and its sources in Matcher — pick 1:1, 1:N, or N:M cardinality, add bank and ledger feeds, and trigger match runs. Contexts and sources are how you tell Matcher **what** to reconcile and **where the numbers come from**. They are the two building blocks you set up before any matching happens. * A **context** is a single reconciliation you care about — for example, *"our main bank account vs. our books."* It sets the scope: which systems are compared, which rules apply, and over what period. * A **source** is one of the systems feeding numbers into that comparison — a bank statement, an ERP export, a payment processor's settlement file, or a ledger. Every context compares exactly two sides against each other, so each one needs at least two sources. Get these right and everything downstream — matching, exceptions, and reporting — follows. ## What is a reconciliation context? *** A reconciliation context defines the operational boundaries of a reconciliation process. It specifies: * Which data sources are compared * Which matching rules apply * How exceptions are handled * The time window covered by reconciliation **Common examples:** * *Bank Account 1234 vs General Ledger* (daily bank reconciliation) * *Payment Gateway vs Revenue System* (payment reconciliation) * *Intercompany Entity A vs Entity B* (intercompany reconciliation) ## Context types *** Matcher lets you use different reconciliation cardinalities based on transaction structure. ### One-to-one (1:1) Each transaction is reconciled against a single counterpart. **Typical use cases:** * Bank statements * Direct payment matching ### One-to-many (1:n) One transaction is reconciled against multiple counterparts. **Typical use cases:** * Split payments * Batch deposits * Consolidated invoices ### Many-to-many (n:m) Multiple transactions are reconciled across multiple counterparts. **Typical use cases:** * Netting arrangements * Complex payment allocation * Multi-leg financial flows ## Creating a reconciliation context *** Once you know what you're reconciling, create the context. At this stage you're mainly declaring the cardinality (`type`), a required execution label (`interval`), and any fee tolerance the comparison should allow. The `interval` value does not schedule runs; automatic execution requires a separate [reconciliation schedule](/en/matcher/configuration/matcher-schedules). A new context starts in `DRAFT` and remains there until you explicitly activate it. #### Request ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Daily Bank Reconciliation", "interval": "daily", "type": "1:1", "feeToleranceAbs": "0", "feeTolerancePct": "0", "feeNormalization": "NET", "autoMatchOnUpload": false }' ``` #### Context fields Descriptive name for the context Matching cardinality: `1:1`, `1:N`, or `N:M` Required execution label (e.g. `daily`, `weekly`). It does not schedule runs. Absolute fee tolerance for amount comparison, as a decimal string (e.g. `"0.01"`) Percentage fee tolerance for amount comparison, as a decimal string (`"0.5"` means 0.5%) Optional fee normalization mode: `NET` or `GROSS`. Omit it to leave fee normalization disabled. Automatically trigger a match run when a file is uploaded #### Response ```json theme={null} { "id":"019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "tenantId":"11111111-1111-1111-1111-111111111111", "name":"Daily Bank Reconciliation", "type":"1:1", "interval":"daily", "status":"DRAFT", "feeToleranceAbs":"0", "feeTolerancePct":"0", "feeNormalization":"NET", "autoMatchOnUpload":false, "createdAt":"2026-02-02T16:31:22Z", "updatedAt":"2026-02-02T16:31:22Z" } ``` API Reference: [Create context](/en/reference/matcher/create-context) ## Running reconciliation *** A context doesn't reconcile on its own — you trigger a **match run**. A run applies the context's active rules to the transactions in its sources, then produces matches and exceptions. You can trigger runs by hand, or let a [schedule](/en/matcher/configuration/matcher-schedules) fire them automatically. Every run works in one of two modes: | Mode | What it does | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `DRY_RUN` | Does not persist matching results, transaction mutations, fee artifacts, or exceptions, but does persist a completed `MatchRun` and its statistics | | `COMMIT` | Executes matching and persists the results | Trigger a run for a context: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mode": "COMMIT" }' ``` By default a run is **synchronous** — it executes in-request and the response carries the final status. For large volumes, set `"async": true` to submit the run and poll its progress instead. Asynchronous submission requires the match-run worker to be enabled; otherwise Matcher rejects `"async": true` with HTTP 503. Both modes return **HTTP 202 Accepted**, so read the response `status`, not the HTTP code, to know the outcome. A synchronous run returns a terminal `COMPLETED` or `FAILED`; an async run returns `QUEUED`, and you poll `GET /v1/matching/runs/{runId}`. While in flight, a run moves through `PROCESSING` and `FINALIZING` (treat both as not-yet-done) before reaching `COMPLETED` or `FAILED`. To review past runs, list a context's run history with `GET /v1/matching/contexts/{contextId}/runs`. API Reference: * [Run match](/en/reference/matcher/run-match) * [List match runs](/en/reference/matcher/list-match-runs) ## What is a source? *** A source represents a system or data feed that supplies transactions to a reconciliation context. Each context requires at least two sources. **Typical sources include:** * Bank statement feeds * ERP general ledger exports * Payment processor transaction streams * Internal accounting systems ## Adding sources to a context *** A context needs at least two sources — one for each side of the comparison. The `side` field (`LEFT` or `RIGHT`) declares which side a source feeds; Matcher reconciles the `LEFT` side against the `RIGHT` side. Assign one side to each source and keep the assignment consistent. Create a source with a `name`, `type`, `side`, and a `config` object. Leave `config` empty (`{}`) when the source needs no connection-specific settings — as with a bank feed on the `LEFT` side: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Chase Bank - Account 1234", "type": "BANK", "side": "LEFT", "config": {} }' ``` Point the other side at a second source. `config` carries source-specific connection and parsing settings when they're needed — for example a payment gateway on the `RIGHT` side: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Payment Gateway", "type": "GATEWAY", "side": "RIGHT", "config": { "currency": "USD", "provider": "stripe" } }' ``` `name`, `type`, and `side` are required (`name` is 1–50 characters). `config` is optional and defaults to an empty object when omitted. API Reference: [Create source](/en/reference/matcher/create-source) ### Source types | Type | Description | Typical use | | --------- | ----------------------- | -------------------------------------------------------- | | `LEDGER` | Internal ledger | Internal accounting systems (including Midaz) | | `BANK` | Bank statement feed | External bank feeds | | `GATEWAY` | Payment gateway | Payment processors (Stripe, Adyen, PayPal) | | `CUSTOM` | Bespoke feed | Any other data source | | `FETCHER` | Discovery-engine source | Aggregator connections supplied through a source binding | ### Fetcher sources `FETCHER` identifies a source type; it does not enable automatic pulling by itself. Create it like any other source, then wire the upstream aggregator connection through a [source binding](#source-bindings) on the query rail (`connectionId`) — see [Discovery](/en/matcher/integrations/matcher-discovery) for how connections are set up. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Open Banking Aggregator", "type": "FETCHER", "side": "LEFT", "config": { "provider": "pluggy" } }' ``` ## Managing sources *** Sources support a full CRUD lifecycle under `/v1/contexts/{contextId}/sources`. You can rename or reconfigure a source at any time, and **archiving is soft and reversible**. An archived source is excluded from context readiness, matching, and source listings, but keeps its full history until you restore it. Archiving does not disable its bindings; disable or delete them separately to stop scheduler dispatch. | Action | Method & path | Notes | | -------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Create source | `POST /v1/contexts/{contextId}/sources` | Body: `name`, `type`, `side`, `config` (see above). | | List sources | `GET /v1/contexts/{contextId}/sources` | Lists sources in the context. | | Get source | `GET /v1/contexts/{contextId}/sources/{sourceId}` | Retrieves a single source by id. | | Update source | `PATCH /v1/contexts/{contextId}/sources/{sourceId}` | Updates mutable source fields (e.g. `name`, `config`). | | Archive source | `POST /v1/contexts/{contextId}/sources/{sourceId}/archive` | Excludes the source from readiness, matching, and listings; bindings remain enabled until changed separately. | | Restore source | `POST /v1/contexts/{contextId}/sources/{sourceId}/restore` | Reactivates a previously archived source. | API Reference: * [Create source](/en/reference/matcher/create-source) * [Get source](/en/reference/matcher/retrieve-source) * [Update source](/en/reference/matcher/update-source) * [Archive source](/en/reference/matcher/archive-source) * [Restore source](/en/reference/matcher/restore-source) ## Source bindings *** Bindings define how the binding scheduler can pull source data without a manual file upload. A **source binding** ties a source to the rail that supplies its transactions, plus a duration that determines when it is due. Exactly one rail is meaningful per binding `kind`: * `file` — fetches files via a transport (populates `transportConfig`). * `query` — pulls rows through a discovery-engine connection (populates `connectionId`; see [Discovery](/en/matcher/integrations/matcher-discovery)). Bindings live under `/v1/contexts/{contextId}/sources/{sourceId}/bindings`. A binding is dispatched only when the binding scheduler is enabled (it is disabled by default), the binding is enabled, and the binding is due. Creating or enabling a binding does not run it immediately. | Action | Method & path | | -------------- | ------------------------------------------------------------------------- | | Create binding | `POST /v1/contexts/{contextId}/sources/{sourceId}/bindings` | | List bindings | `GET /v1/contexts/{contextId}/sources/{sourceId}/bindings` | | Get binding | `GET /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId}` | | Update binding | `PATCH /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId}` | | Delete binding | `DELETE /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId}` | List returns **every** binding, enabled and disabled, so a disabled binding stays visible instead of silently vanishing. ### Create a query-rail binding ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{sourceId}/bindings" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "kind": "query", "connectionId": "550e8400-e29b-41d4-a716-446655440000", "format": "br/cnab400/default", "scheduleSpec": "1h", "enabled": true }' ``` #### Fields Rail the source is pulled on: `file` or `query` (required). Query-rail discovery-engine connection. Required for `query`, rejected for `file`. Declared format the binding produces (region/family-namespaced descriptor key, e.g. `br/cnab400/default`). Go duration string the binding scheduler reads, such as `1h` or `30m`. Cron and `@every` syntax are invalid. Whether the scheduler can dispatch the binding when it is due. Defaults to `true`; enabling it does not run it immediately. API Reference: * [Create source binding](/en/reference/matcher/create-source-binding) * [List source bindings](/en/reference/matcher/list-source-bindings) * [Get source binding](/en/reference/matcher/get-source-binding) * [Update source binding](/en/reference/matcher/update-source-binding) * [Delete source binding](/en/reference/matcher/delete-source-binding) ## Managing contexts *** As reconciliations evolve, you'll adjust a context's settings, pause it, retire it, or copy it. These lifecycle operations preserve history so you never lose an audit trail. ### Update a context ```bash cURL theme={null} curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Daily Bank Reconciliation - Updated", "interval": "weekly", "status": "PAUSED" }' ``` API Reference: [Update context](/en/reference/matcher/update-context) ### Pause a context To temporarily stop a context from being used in reconciliation runs, update its status to `PAUSED`: ```bash cURL theme={null} curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "PAUSED" }' ``` Pausing a context: * Prevents new match runs * Preserves historical data * Allows future reactivation by setting status back to `ACTIVE` ### Archive a context Archiving is a reversible soft-delete. Instead of permanently removing a context, it moves the context to the `ARCHIVED` status, preserving its full history (sources, rules, match runs, and audit records) while excluding it from the default context listing. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/archive" \ -H "Authorization: Bearer $TOKEN" ``` Archiving a context: * Sets the context status to `ARCHIVED` * Preserves the complete history and audit trail * Excludes the context from the default listing * Can be reversed at any time with the [restore](#restore-a-context) endpoint API Reference: [Archive context](/en/reference/matcher/archive-context) ### Restore a context Restoring reverses an archive, moving the context from `ARCHIVED` back to `DRAFT` so it can be reviewed and reconfigured before being reactivated. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/restore" \ -H "Authorization: Bearer $TOKEN" ``` Restoring a context: * Sets the context status from `ARCHIVED` back to `DRAFT` * Does **not** resume matching automatically—review and reactivate the context to run reconciliation again * Returns `409 Conflict` if called on a context that is not archived API Reference: [Restore context](/en/reference/matcher/restore-context) ### Clone a context To duplicate an existing context with its sources, rules, fee rules, and field maps, use the clone endpoint. This is useful for creating templates or replicating configurations across environments. Cloned fee rules keep referencing the same fee schedules as the source context; the fee schedules themselves are not copied. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/clone" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Q1 2025 Reconciliation (Copy)", "includeSources": true, "includeRules": true }' ``` The response reports how many sources, rules, fee rules, and field maps were copied. A successful clone is returned in `ACTIVE` status. API Reference: [Clone context](/en/reference/matcher/clone-context) ## Context lifecycle *** A reconciliation context follows a well-defined lifecycle that controls when matching can run and how data is preserved. * A context is first created in **Draft**, where sources and settings are configured. * A context remains **Draft** until an explicit update sets it to `ACTIVE`. Activation validates the required sources on both `LEFT` and `RIGHT` sides, field mappings or CAMT options, match rules, and fee rules when fee normalization is enabled. * An active context can be temporarily **Paused** to stop execution without affecting configuration or historical data. * When a context is no longer needed, it can be **Archived** via the [archive](#archive-a-context) endpoint. Archiving is a reversible soft-delete: it moves the context to `ARCHIVED`, preserves the full history and audit records, and excludes it from the default listing. An archived context can be brought back to **Draft** at any time with the [restore](#restore-a-context) endpoint. Matcher Context Lifecycle This lifecycle ensures operational control, predictable execution, and full traceability across reconciliation periods. ## Best practices *** Use explicit names that reflect accounts, systems, and purpose. Favor accuracy over automation initially. Adjust thresholds based on observed results. Use multiple contexts instead of a single broad reconciliation. Always mark sources with compliance requirements. Ensure source timezones reflect the original data feed. Explicitly define debit and credit semantics for each source. ## Next steps *** Define how source fields map to Matcher's schema. Configure the rules that drive reconciliation. # Exception routing Source: https://docs.lerian.studio/en/matcher/configuration/matcher-exception-routing Understand automatic severity classification and use explicit assignment, bulk actions, caller-directed dispatch, and callbacks. Matcher automatically classifies unmatched transactions by severity. Assignment, bulk operations, and dispatch are explicit API actions; Matcher does not automatically route or escalate exceptions. ## Severity classification *** Matcher classifies exceptions automatically from base amount, age, and source signals to support review prioritization. ### Default severity rules | Severity | Default amount or age criteria | | ------------ | ---------------------------------------- | | **Critical** | Base amount ≥ 100,000 OR age ≥ 120 hours | | **High** | Base amount ≥ 10,000 OR age ≥ 72 hours | | **Medium** | Base amount ≥ 1,000 OR age ≥ 24 hours | | **Low** | All other cases | Source signals can also influence classification. Exceptions with reason `FEE_DATA_MISSING` are capped at `MEDIUM`, even when amount or age thresholds would otherwise classify them as `HIGH` or `CRITICAL`. ## Assignment *** Assignment is explicit. For an `OPEN` exception, the assignment API accepts one opaque `assignee` string and changes the exception to `ASSIGNED`. Matcher has no user-group model and does not implement automatic assignment, round-robin routing, or least-loaded routing. If you use a user or group identifier, encode it in the `assignee` string and resolve its meaning in your own identity system. ## SLA behavior *** Matcher contains domain helpers that can calculate SLA due dates, but the production exception flow does not call them. Matcher does not currently set SLA deadlines, emit SLA warnings, escalate exceptions, or route them automatically. Track and enforce operational SLAs outside Matcher. ## Additional exception endpoints *** Beyond the basic exception CRUD, Matcher provides endpoints for advanced exception workflows: | Endpoint | Method | Description | | -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | | [Dispatch exception](/en/reference/matcher/dispatch-exception) | `POST` | Attempt caller-selected dispatch without changing exception status | | [Process callback](/en/reference/matcher/process-exception-callback) | `POST` | Apply a token-authenticated, idempotent external update | | [Bulk assign](/en/reference/matcher/bulk-assign-exceptions) | `POST` | Assign exceptions to one `assignee` string | | [Bulk resolve](/en/reference/matcher/bulk-resolve-exceptions) | `POST` | Resolve multiple exceptions independently | | [Bulk dispatch](/en/reference/matcher/bulk-dispatch-exceptions) | `POST` | Dispatch multiple exceptions independently | | [List comments](/en/reference/matcher/list-exception-comments) | `GET` | Retrieve all comments on an exception | | [Add comment](/en/reference/matcher/add-exception-comment) | `POST` | Add a comment to an exception for audit and collaboration | | [Delete comment](/en/reference/matcher/delete-exception-comment) | `DELETE` | Remove a comment from an exception | | [List disputes](/en/reference/matcher/list-disputes) | `GET` | Retrieve all disputes with filtering | | [Get dispute](/en/reference/matcher/retrieve-dispute) | `GET` | Retrieve details of a specific dispute | | [Open dispute](/en/reference/matcher/open-dispute) | `POST` | Flag an exception as disputed for escalated review | | [Close dispute](/en/reference/matcher/close-dispute) | `POST` | Close a dispute with a resolution | | [Submit evidence ](/en/reference/matcher/submit-evidence) | `POST` | Add evidence to support a dispute case | Bulk assign, resolve, and dispatch accept 1–100 exception IDs. Matcher processes each ID independently, so partial success is expected. Bulk assignment accepts a single `assignee` string, not a user or group object. ## Dispatch and callbacks *** Dispatch is caller-directed: each request names the target. Dispatch records an audit event but does not change the exception status. Do not treat the accepted target names as preconfigured integrations. ### Dispatch targets When dispatching an exception, the `targetSystem` field must be one of the following values: | Target | Description | | ------------ | ------------------------------------------------------------------------------------------- | | `JIRA` | Attempts caller-directed JIRA dispatch; runtime connector configuration is required. | | `SERVICENOW` | Not supported; it reaches the generic unsupported-target failure path and returns HTTP 500. | | `WEBHOOK` | Attempts caller-directed webhook dispatch; runtime connector configuration is required. | | `MANUAL` | Acknowledges the dispatch locally without sending to an external system. | Inbound callbacks are a separate token-authenticated, idempotent flow. A callback can set an exception to `ASSIGNED` when it includes an assignee, or to `RESOLVED`; it is not bidirectional synchronization performed by dispatch. ### Filtering by external system When listing exceptions, the `external_system` query parameter accepts any string value for filtering. This allows you to filter exceptions dispatched to any system, including custom identifiers that may have been set through callbacks. ### Dispatch error handling Request validation and connector failures use API problem responses. In particular, `SERVICENOW` does not return `MTCH-0508`; it currently returns the generic HTTP 500 unsupported-target failure. A successful dispatch acknowledges the target operation but still leaves the exception status unchanged. ## Queue summaries and observability *** The exception list exposes queue-scoped summary counts. Matcher does not expose SLA breach rates, routing-rule distribution, or integration success and failure analytics. Use your external observability stack for those operational signals. ## Best practices *** Use the classified severity to prioritize review, and account for the `FEE_DATA_MISSING` cap at `MEDIUM`. Pass a stable identifier in the opaque `assignee` string and resolve ownership in your identity system. Define deadlines, warnings, and escalation in your workflow system because Matcher does not enforce them. Confirm the selected connector is configured before relying on caller-directed JIRA or webhook dispatch. Treat bulk operations as independent per ID and handle partial success explicitly. Protect callback tokens and use stable idempotency keys when external systems update exception status. ## Next steps *** Resolve exceptions through the API or external systems. Advanced event delivery and callback handling. # Fee rules Source: https://docs.lerian.studio/en/matcher/configuration/matcher-fee-rules Configure fee schedules and predicate rules used by NET or GROSS normalization when the matching side and currencies are compatible. Fee handling in Matcher is split across two entities that work together: * A **fee schedule** defines *how much* fee to compute — the currency, rounding, and one or more fee items (flat, percentage, tiered, or expression-based). * A **fee rule** decides *when* a fee schedule applies. It belongs to a reconciliation context, targets a matching side, and carries predicates that a transaction's metadata must satisfy. When the predicates match, the rule selects its fee schedule. When fee normalization is enabled and the transaction and schedule currencies match, modeling expected fees this way lets Matcher account for predictable charges before comparing transactions. ## What fee handling solves *** Reconciliation fails when one side of a transaction includes fees or charges that the other side doesn't record. A payment gateway deducts a processing fee before settling. A bank charges a wire transfer fee. An acquirer nets fees against payouts. Without modeled fees, Matcher treats these differences as amount mismatches and generates exceptions — even when the difference is expected and documented. Fee schedules describe the charge, and fee rules select when to use it during enabled fee normalization. ## How rules and schedules fit together *** A fee rule does not contain the fee amount or calculation itself. It **references** a fee schedule by ID and applies it to the transactions its predicates select. 1. A **fee schedule** is created once at the tenant level and can be reused by many rules across many contexts. 2. A **fee rule** is created inside a context. It sets a `side`, a `feeScheduleId`, a `priority`, and a list of `predicates`. 3. When Matcher processes a context with fee normalization enabled, it evaluates the relevant side's fee rules in `priority` order (lowest first). The first matching rule selects the referenced schedule. This separation means you change *how* a fee is computed by editing the schedule, and change *when* it applies by editing the rule — without touching the other. A fee rule alone does not change comparison amounts. Fee-rule evaluation affects fee reconciliation only when the context sets `feeNormalization` to `NET` or `GROSS`, the relevant side has rules, and the transaction and schedule currencies match. ## Fee rule structure *** A fee rule belongs to a context and has the following fields. | Field | Type | Description | | --------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `side` | Enum | Which matching side the rule applies to: `LEFT`, `RIGHT`, or `ANY`. `ANY` matches transactions on either side. | | `feeScheduleId` | UUID | The fee schedule this rule applies when its predicates match. | | `name` | String | Human-readable name of the fee rule. | | `priority` | Integer | Evaluation priority; lower numbers are evaluated first. Must be unique within the context. `LEFT`, `RIGHT`, and `ANY` rules share the same priority space. | | `predicates` | Array | Predicates (ANDed together) that a transaction's metadata must satisfy for this rule to apply. | ### Predicates Each predicate tests one transaction metadata field with an operator. | Field | Description | | ---------- | ------------------------------------------------------------------------------ | | `field` | The transaction metadata field the predicate tests (e.g. `institution`). | | `operator` | The comparison operator (see below). | | `value` | Single comparison value, used by `EQUALS`, `NEQ`, and the numeric comparators. | | `values` | List of candidate values, used by `IN` and `BETWEEN`. | **Available operators:** | Operator | Meaning | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `EQUALS` | Case-insensitive string equality with a single `value`. | | `NEQ` | For a present field, numeric inequality when both values coerce to decimals; otherwise case-insensitive string inequality. | | `IN` | Matches any entry in `values`. | | `EXISTS` | Asserts the field is present (no value needed). | | `GT` / `GTE` / `LT` / `LTE` | Numeric comparison of the field against a single decimal `value`. | | `BETWEEN` | Inclusive numeric membership in `values` = `[lo, hi]` (with `lo <= hi`). | Missing fields evaluate to `false` for every operator except `EXISTS`. ## Fee schedule structure *** A fee schedule is a tenant-level entity that computes a fee from a gross amount. | Field | Type | Description | | ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | String | Human-readable name of the schedule. | | `currency` | String | ISO 4217 currency the schedule's amounts are denominated in. | | `applicationOrder` | Enum | How items combine: `PARALLEL` applies every item to the same gross base; `CASCADING` applies each item to the net remaining after prior items. | | `roundingScale` | Integer | Number of decimal places fee amounts are rounded to. | | `roundingMode` | Enum | Rounding strategy: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE`. | | `items` | Array | One or more fee items that make up the schedule (at least one required). | ### Fee items Each item declares a `name`, a `priority` (application order, relevant for `CASCADING`), a `structureType`, and a type-specific `structure`. | `structureType` | `structure` shape | Notes | | --------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `FLAT` | `{ "amount": "1.50" }` | Fixed amount. | | `PERCENTAGE` | `{ "rate": "0.029" }` | `rate` is a `0..1` fraction of the base (`0.029` = 2.9%), **not** a percent value. | | `TIERED` | `{ "tiers": [ { "rate": "0.01", "upTo": "1000" }, ... ] }` | Same `0..1` fraction semantics per tier rate. | | `EXPRESSION` | `{ "expression": "gross - desconto + multa" }` | Formula over identifiers (`+ - * /`, parentheses, and the functions `days_late`, `days_between`, `max`, `min`, `abs`, `clamp`). `gross` is the engine-provided base amount and overrides any metadata key named `gross`; other identifiers resolve from metadata. | ## Managing fee schedules *** Fee schedules are tenant-scoped and managed independently of any context. | Operation | Endpoint | | -------------------------- | ---------------------------------------------- | | Create a fee schedule | `POST /v1/fee-schedules` | | List fee schedules | `GET /v1/fee-schedules` | | Retrieve a fee schedule | `GET /v1/fee-schedules/{scheduleId}` | | Update a fee schedule | `PATCH /v1/fee-schedules/{scheduleId}` | | Delete a fee schedule | `DELETE /v1/fee-schedules/{scheduleId}` | | Simulate a fee calculation | `POST /v1/fee-schedules/{scheduleId}/simulate` | ### Create a fee schedule This schedule computes a 2.9% processing fee on the gross amount, denominated in BRL. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/fee-schedules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Gateway Processing - 2.9%", "currency": "BRL", "applicationOrder": "PARALLEL", "roundingScale": 2, "roundingMode": "HALF_UP", "items": [ { "name": "processing", "priority": 1, "structureType": "PERCENTAGE", "structure": { "rate": "0.029" } } ] }' ``` API Reference: * [Create fee schedule](/en/reference/matcher/create-fee-schedule) * [List fee schedules](/en/reference/matcher/list-fee-schedules) * [Retrieve fee schedule](/en/reference/matcher/retrieve-fee-schedule) * [Update fee schedule](/en/reference/matcher/update-fee-schedule) * [Delete fee schedule](/en/reference/matcher/delete-fee-schedule) ### Simulate a fee schedule Before wiring a schedule into a rule, simulate it against a gross amount to confirm the computed fee. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/fee-schedules/{scheduleId}/simulate" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "grossAmount": "100.00", "currency": "BRL" }' ``` The response returns the `netAmount`, `totalFee`, and a per-item breakdown. API Reference: [Simulate fee calculation](/en/reference/matcher/simulate-fee-schedule) A fee schedule cannot be deleted while a fee rule or fee-variance history references it. The delete request returns `409 Conflict` with the code `MTCH-0108`; the API problem does not list the blocking contexts. Remove or repoint current rule references first. Historical variance references continue to block deletion. ## Managing fee rules *** Fee rules are created inside a context and reference a fee schedule. | Operation | Endpoint | | ---------------------------- | ----------------------------------------- | | Create a fee rule | `POST /v1/contexts/{contextId}/fee-rules` | | List fee rules for a context | `GET /v1/contexts/{contextId}/fee-rules` | | Retrieve a fee rule | `GET /v1/fee-rules/{feeRuleId}` | | Update a fee rule | `PATCH /v1/fee-rules/{feeRuleId}` | | Delete a fee rule | `DELETE /v1/fee-rules/{feeRuleId}` | ### Create a fee rule This rule applies the fee schedule created above to right-side transactions whose `institution` metadata equals `Banco do Brasil`. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/fee-rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "side": "RIGHT", "feeScheduleId": "550e8400-e29b-41d4-a716-446655440000", "name": "BB Right-Side Processing Fee", "priority": 0, "predicates": [ { "field": "institution", "operator": "EQUALS", "value": "Banco do Brasil" } ] }' ``` API Reference: * [Create fee rule](/en/reference/matcher/create-fee-rule) * [List fee rules](/en/reference/matcher/list-fee-rules) * [Retrieve fee rule](/en/reference/matcher/retrieve-fee-rule) * [Update fee rule](/en/reference/matcher/update-fee-rule) * [Delete fee rule](/en/reference/matcher/delete-fee-rule) ## End-to-end flow *** Putting it together, expected-fee handling follows three steps: Define how the fee is computed once at the tenant level with `POST /v1/fee-schedules`. Optionally validate it with the simulate endpoint. Note the returned `id`. Inside the reconciliation context, create a fee rule with `POST /v1/contexts/{contextId}/fee-rules`. Set `feeScheduleId` to the schedule's `id`, choose the `side`, set a unique `priority`, and add the `predicates` that select the transactions the fee applies to. Set the context's `feeNormalization` to `NET` or `GROSS`. During matching, Matcher evaluates rules for each relevant side in `priority` order. A matching rule changes the comparison amount only when its schedule and the transaction use the same currency. ## Best practices *** A fee schedule is tenant-scoped and can be referenced by many rules. Define a schedule once (e.g. "Card Processing - Visa") and point rules in different contexts at it. Editing the schedule updates every rule that uses it. Priorities are unique within a context and shared across `LEFT`, `RIGHT`, and `ANY` rules. Order them from most specific to most general so a narrow rule wins before a broad fallback. Predicates are ANDed together and tested against transaction metadata. Combine operators (`EQUALS`, `IN`, `BETWEEN`, `EXISTS`) to target exactly the transactions a fee applies to and avoid unintended fee attribution. Use `POST /v1/fee-schedules/{scheduleId}/simulate` to confirm a schedule produces the expected fee for representative amounts before referencing it from a rule. A schedule referenced by a rule or fee-variance history cannot be deleted (`409 MTCH-0108`). The conflict response does not list blocking contexts. Update or remove current rule references first; historical variance references continue to block deletion. ## Next steps *** Configure how transactions are compared once expected fees are accounted for. Review explicit assignment, dispatch, and callback behavior for exceptions that fee handling doesn't cover. Full API reference for fee schedule endpoints. Full API reference for fee rule endpoints. # Field mapping Source: https://docs.lerian.studio/en/matcher/configuration/matcher-field-mapping Rename each source's raw columns to Matcher's canonical fields — external_id, amount, currency, date, plus optional description and fee slots per source. A field map tells Matcher which raw column in a source carries each canonical transaction field. Because every source (bank statements, ledger exports, gateway reports) names its columns differently, the field map normalizes those column names into one fixed vocabulary before matching runs. A field map only **renames columns**. It does not parse, compute, transform, or combine values. Each canonical field is populated from exactly one source column. ## What a field map is *** A field map belongs to a single **source** inside a **context**. A context reconciles two sides — a `LEFT` source and a `RIGHT` source — and each source has its own field map. Matcher compares the canonical fields produced by both maps, so both sides must resolve to the same vocabulary even when their raw files look nothing alike. The mapping is a JSON object in the form: ```JSON theme={null} { "": "" } ``` * The **key** is a canonical field. Keys come from a **closed, case-sensitive vocabulary** — Matcher rejects any key outside it. * The **value** is the name of the column in the raw source that carries that field. Values are free text (whatever your file calls the column) and must be non-empty strings. ## Canonical vocabulary *** The key space is closed. These are the only keys Matcher accepts. ### Required keys Every field map must declare all four: | Key | Description | | ------------- | ------------------------------------------------- | | `external_id` | Unique identifier of the record within the source | | `amount` | Transaction amount | | `currency` | ISO 4217 currency code | | `date` | Transaction date | ### Optional keys Declare these only when the source carries them: | Key | Description | | -------------- | ---------------------------------------------------------------- | | `description` | Free-text label copied into the transaction's description column | | `fee_amount` | Column carrying a fee amount for the record | | `fee_currency` | Column carrying the currency of that fee | `fee_amount` and `fee_currency` are the optional **fee slot**. When present, the mapped column's value is copied into the transaction metadata that fee verification reads, so a column named anything (for example `mdr_fee`) can carry fees end to end without hand-built metadata. Omit them and behavior is identical to a map without a fee slot. ## Creating a field map *** Field maps are created **per source**. Send the mapping object to the source's field-map endpoint: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{sourceId}/field-maps" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mapping": { "external_id": "Transaction ID", "amount": "Amount", "currency": "Currency", "date": "Post Date", "description": "Memo" } }' ``` **Response** ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149", "sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "mapping": { "external_id": "Transaction ID", "amount": "Amount", "currency": "Currency", "date": "Post Date", "description": "Memo" }, "version": 1, "createdAt": "2025-01-15T10:00:00Z", "updatedAt": "2025-01-15T10:00:00Z" } ``` API Reference: [Create field map](/en/reference/matcher/create-field-map) ## Updating a field map *** Each source has one field map. To change a mapping, `PATCH` it by its own ID (not the source ID). Send the full mapping — it replaces the previous one and increments `version`. ```bash cURL theme={null} curl -X PATCH "https://api.matcher.example.com/v1/field-maps/{fieldMapId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mapping": { "external_id": "Transaction ID", "amount": "Amount", "currency": "Currency", "date": "Value Date", "description": "Memo", "fee_amount": "Fee", "fee_currency": "Fee Currency" } }' ``` API Reference: [Update field map](/en/reference/matcher/update-field-map) Other operations: | Operation | Endpoint | | --------------------------------- | ------------------------------------------------------------ | | Get a source's field map | `GET /v1/contexts/{contextId}/sources/{sourceId}/field-maps` | | List every field map in a context | `GET /v1/contexts/{contextId}/field-maps` | | Delete a field map | `DELETE /v1/field-maps/{fieldMapId}` | ## Example: both sides of a context *** A context reconciles a bank feed against an internal ledger export. The two files use different column names, so each source declares its own map — but both resolve to the same canonical keys. ### LEFT source — bank statement (CSV) Raw columns: ```csv theme={null} BankRef,BookingDate,Amount,Ccy,Narrative BANK-001,2025-01-15,-500.00,USD,Wire to Acme Corp ``` Field map: ```json theme={null} { "mapping": { "external_id": "BankRef", "amount": "Amount", "currency": "Ccy", "date": "BookingDate", "description": "Narrative" } } ``` ### RIGHT source — ledger export (CSV) Raw columns: ```csv theme={null} entry_id,posted_at,value,asset,memo,mdr_fee,fee_ccy LDG-9931,2025-01-15,-500.00,USD,Payment Acme Corp,2.50,USD ``` Field map: ```json theme={null} { "mapping": { "external_id": "entry_id", "amount": "value", "currency": "asset", "date": "posted_at", "description": "memo", "fee_amount": "mdr_fee", "fee_currency": "fee_ccy" } } ``` Both sources now expose `external_id`, `amount`, `currency`, and `date` in the canonical vocabulary, so match rules can compare them directly — even though one file called the amount `Amount` and the other called it `value`. ## Common mistakes *** The key is the canonical field and the value is your column — `{"external_id": "BankRef"}`, not `{"BankRef": "external_id"}`. Writing it backwards puts an unknown key (`BankRef`) on the left and is rejected. Only `external_id`, `amount`, `currency`, `date`, `description`, `fee_amount`, and `fee_currency` are accepted. Keys such as `transaction_id`, `reference`, `counterparty`, or `type` are rejected as unknown keys, and the error names each offender. Keys are case-sensitive lowercase tokens. `External_Id`, `Amount`, or `CURRENCY` are treated as unknown keys. All of `external_id`, `amount`, `currency`, and `date` must be present. A map missing any of them fails validation with a "missing required keys" message. Every value must be a non-empty string naming a source column. `null`, numbers, objects, or `""` are rejected. Field maps do not parse dates, divide amounts, concatenate columns, or apply conditionals. Deliver values already in the expected shape from the source file, or normalize upstream before upload. ## Next steps *** Define how the canonical fields are compared and grouped. Import transactions using your field maps. # Match rules Source: https://docs.lerian.studio/en/matcher/configuration/matcher-match-rules Author exact, tolerance, date-lag, and fuzzy rules in Matcher — set priorities, tolerances, and reference matching to control how transactions pair up. Match rules are where you set your reconciliation policy — how strict or forgiving Matcher should be when deciding two transactions are the same. Tight rules mean more manual review but fewer false matches; looser rules automate more but need careful oversight. You can enforce exact matches, allow controlled variance, tolerate timing differences, or compare free-text references by similarity. ## How rules work *** When a matching run starts, Matcher evaluates rules in priority order. * Rules are evaluated from the lowest priority number to the highest. * Each rule creates all matches it can from transactions not already used by higher-priority rules. * After every rule runs, transactions that remain unmatched become exceptions. This approach keeps higher-priority matches from being reused while allowing progressively looser rules to process the remaining transactions. ## Rule types *** ### Exact Requires a strict match on configured fields. * **Best for**: Deterministic matches where values should align 1:1. ### Tolerance Allows controlled variance in amount matching. * **Best for**: Known variance patterns such as fees, rounding, or FX differences. ### Date lag Allows date differences between transactions. * **Best for**: Posting delays between systems. ### Fuzzy Replaces exact reference equality with normalized string-similarity scoring. Amount, currency, and date gates require exact equality by default, but `matchAmount`, `matchCurrency`, and `matchDate` independently control whether each gate applies. FUZZY always **proposes** a match for review and never auto-confirms. * **Best for**: Free-text memos or truncated references where the reference varies but the enabled financial gates still align. ## Creating match rules *** ### Exact rule ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "EXACT", "priority": 1, "config": { "matchAmount": true, "matchCurrency": true, "matchDate": true, "matchReference": true, "datePrecision": "DAY", "caseInsensitive": true, "referenceMustSet": false, "matchBaseAmount": false, "matchBaseCurrency": false, "matchScore": 100, "matchBaseScore": 90 } }' ``` #### Config reference Require exact amount match Require exact currency match Require exact date match Require exact reference match Date comparison precision: `DAY` or `TIMESTAMP` Case-insensitive reference comparison Require reference to be present on both sides Match on base (converted) amount instead of original Match on base currency instead of original Accepted and validated, but **reserved/inert** — does not change the calculated confidence score (see note below) Accepted and validated, but **reserved/inert** — does not change the calculated confidence score (see note below) **`matchScore` and `matchBaseScore` are currently inert.** They are accepted and validated in the rule config, but the scoring engine ignores them: confidence is always computed from the fixed internal component weights (amount 40, currency 30, date 20, reference 10). These fields are reserved for future use and setting them does **not** alter the confidence score or auto-confirm behavior. See [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring). The response echoes the persisted rule with its assigned `id` and timestamps. API Reference: [Create match rule](/en/reference/matcher/create-match-rule) ### Tolerance rule ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "TOLERANCE", "priority": 2, "config": { "percentTolerance": 0.005, "absTolerance": 0.50, "dateWindowDays": 3, "roundingScale": 2, "roundingMode": "HALF_UP", "percentageBase": "MAX", "matchCurrency": true, "matchReference": true, "caseInsensitive": true, "referenceMustSet": false, "matchBaseAmount": false, "matchBaseCurrency": false, "matchScore": 85, "matchBaseScore": 80 } }' ``` #### Config reference Percentage threshold applied to `percentageBase` (0.005 = 0.5%). Defaults to `0`; Matcher compares this threshold with `absTolerance` and uses the larger one Absolute amount threshold. Defaults to `0`; Matcher compares it with the percentage threshold and uses the larger one Both thresholds default to zero, so you must configure any permitted amount variance explicitly. Number of days allowed between transaction dates Decimal places for rounding Rounding strategy: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE` Base for percentage calculation: `MAX`, `MIN`, `AVERAGE`, `LEFT`, or `RIGHT` Require currency match Require reference match Case-insensitive reference comparison Require reference to be present on both sides Match on base (converted) amount Match on base currency Accepted and validated, but **reserved/inert** — does not change the calculated confidence score Accepted and validated, but **reserved/inert** — does not change the calculated confidence score **Example:** * Transaction A: \$1,000.00 * Transaction B: \$1,005.00 * Amount difference: \$5.00 * Percentage threshold: $1,005.00 × 0.5% = $5.025 (`percentageBase: MAX`) * Absolute threshold: \$0.50 * Effective threshold: `MAX($5.025, $0.50)` = \$5.025 → **Matches** ### Fuzzy rule ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "FUZZY", "priority": 4, "config": { "minSimilarity": 0.85, "matchAmount": true, "matchCurrency": true, "matchDate": true, "datePrecision": "DAY", "referenceMustSet": true, "matchScore": 70 } }' ``` #### Config reference Minimum normalized reference similarity (0–1) required to gate as a match When `true`, require an exact amount match When `true`, require an exact currency match When `true`, require an exact date match Date comparison precision: `DAY` or `TIMESTAMP` Require a non-empty reference on both sides Accepted and defaulted to `70`, but **reserved/inert** — it does not cap or change calculated confidence or auto-confirm behavior FUZZY replaces reference equality with similarity. By default, it also requires exact amount, currency, and date matches; disable each gate independently with `matchAmount`, `matchCurrency`, or `matchDate`. FUZZY always proposes matches for human review and never auto-confirms them. ### Date lag rule ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "DATE_LAG", "priority": 3, "config": { "maxDays": 3, "minDays": 0, "inclusive": true, "direction": "ABS", "feeTolerance": 0, "matchScore": 80, "matchCurrency": true } }' ``` #### Config reference Maximum number of days difference allowed Minimum number of days difference required Whether the boundary days are inclusive How to measure the lag: `ABS` (absolute), `LEFT_BEFORE_RIGHT`, or `RIGHT_BEFORE_LEFT` Allowed amount difference to account for fees Accepted and validated, but **reserved/inert** — does not change the calculated confidence score. Note that DATE\_LAG rules always score the reference component as 0, capping the maximum score at 90 Require currency match ### Allocation settings (all rule types) All rule types accept additional allocation settings for split and aggregate matching: | Field | Type | Description | | -------------------------- | ------- | -------------------------------------------------------- | | `allowPartial` | Boolean | Allow partial allocation of transaction amounts | | `allocationDirection` | String | Allocation order: `LEFT_TO_RIGHT` or `RIGHT_TO_LEFT` | | `allocationToleranceMode` | String | How tolerance is measured: `ABS` (absolute) or `PERCENT` | | `allocationToleranceValue` | Decimal | Tolerance threshold for allocation | | `allocationUseBaseAmount` | Boolean | Use base (converted) amount for allocation | ## Rule priority *** Rules are evaluated by priority. Lower numbers run first. ### Priority strategy | Priority | Rule type | Use case | | -------- | --------- | -------------------------------- | | 1–10 | EXACT | Deterministic matches | | 11–50 | TOLERANCE | Small, expected variance | | 51–100 | DATE\_LAG | Date differences between systems | ### Reorder rules You can reorder rules by providing the rule IDs in the desired order: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules/reorder" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "ruleIds": [ "550e8400-e29b-41d4-a716-446655440001", "550e8400-e29b-41d4-a716-446655440002", "550e8400-e29b-41d4-a716-446655440000" ] }' ``` API Reference: [Reorder match rules](/en/reference/matcher/reorder-match-rules) ## Testing rules *** Test rules in dry-run mode before committing matches. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mode": "DRY_RUN" }' ``` Dry run mode evaluates all rules and returns potential matches. It does not create exceptions, but Matcher completes and persists the `MatchRun` with statistics and emits its completion event. ## Managing rules *** ### List rules ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" ``` #### Response The list endpoint returns a summary view of rules. To see the full configuration details for a specific rule, use the individual rule endpoint or the create response which includes the complete `config` object. ```json theme={null} { "items": [ { "id": "019c96a0-2b20-7123-9a1b-2c3d4e5f6a7b", "contextId": "019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "type": "EXACT", "priority": 1, "config": { "matchAmount": true, "matchCurrency": true, "matchDate": true, "matchReference": true, "datePrecision": "DAY", "matchScore": 100, "matchBaseScore": 90 }, "createdAt": "2026-02-02T16:40:00Z", "updatedAt": "2026-02-02T16:40:00Z" } ], "limit": 20, "hasMore": false } ``` API Reference: [List match rules](/en/reference/matcher/list-match-rules) ### Update a rule ```bash cURL theme={null} curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}/rules/{ruleId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "priority": 5, "type": "TOLERANCE", "config": { "percentTolerance": 0.02, "absTolerance": 10.0 } }' ``` API Reference: [Update match rule](/en/reference/matcher/update-match-rule) ### Delete a rule ```bash cURL theme={null} curl -X DELETE "https://api.matcher.example.com/v1/contexts/{contextId}/rules/{ruleId}" \ -H "Authorization: Bearer $TOKEN" ``` API Reference: [Delete match rule](/en/reference/matcher/delete-match-rule) ## Best practices *** Lead with exact rules. Add tolerance rules only for the variance you can justify and explain. Use gaps (1, 10, 20, 50) so you can insert rules without renumbering your entire set. Treat rule updates as production changes. Validate match rates and exception volume before committing. A rule should document the variance it covers and the risk it introduces. If a rule never matches, it may be unnecessary. If it matches too often, it may be too broad. High tolerance increases false positives. Use it as a fallback and review results carefully. ## Next steps *** Configure classification, assignment, and escalation for unmatched transactions. Understand how scores are calculated and how thresholds impact automation. # Schedules Source: https://docs.lerian.studio/en/matcher/configuration/matcher-schedules Automate reconciliation runs with cron-based schedules per context, and manage them through a full create, list, retrieve, update, and delete lifecycle. Schedules let you run reconciliation automatically on a recurring cadence instead of triggering match runs by hand. Each schedule belongs to a reconciliation context and fires on a cron expression, with a minimum interval of five minutes between firings. ## What is a schedule? *** A schedule is a cron-based trigger attached to a context. When it fires, Matcher launches a reconciliation run for that context using its active rules and sources. | Field | Type | Description | | ------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- | | `id` | UUID | Unique identifier for the schedule | | `contextId` | UUID | Context this schedule belongs to | | `cronExpression` | String | Cron expression defining the frequency (e.g. `0 0 * * *` for daily at midnight) | | `enabled` | Boolean | Whether the schedule is active | | `lastRunAt` | Timestamp | Time Matcher last dispatched the asynchronous match-run trigger; it does not indicate completion or success (RFC 3339) | | `nextRunAt` | Timestamp | Next scheduled run time (RFC 3339) | | `createdAt` / `updatedAt` | Timestamp | Creation and last-update timestamps (RFC 3339) | ## Schedule lifecycle *** Schedules support a full CRUD lifecycle under `/v1/contexts/{contextId}/schedules`. | Action | Method & path | | ----------------- | -------------------------------------------------------- | | Create schedule | `POST /v1/contexts/{contextId}/schedules` | | List schedules | `GET /v1/contexts/{contextId}/schedules` | | Retrieve schedule | `GET /v1/contexts/{contextId}/schedules/{scheduleId}` | | Update schedule | `PATCH /v1/contexts/{contextId}/schedules/{scheduleId}` | | Delete schedule | `DELETE /v1/contexts/{contextId}/schedules/{scheduleId}` | API Reference: * [Create schedule](/en/reference/matcher/create-schedule) * [List schedules](/en/reference/matcher/list-schedules) * [Get schedule](/en/reference/matcher/retrieve-schedule) * [Update schedule](/en/reference/matcher/update-schedule) * [Delete schedule](/en/reference/matcher/delete-schedule) ## Creating a schedule *** Provide a `cronExpression`; `enabled` defaults to active when omitted. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/schedules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "cronExpression": "0 0 * * *", "enabled": true }' ``` Cron expression defining the run frequency (1–100 chars), with at least five minutes between firings. Per-minute and sub-minute schedules are rejected Whether the schedule is active immediately The response returns the created schedule, including its `id`, `nextRunAt`, and timestamps. Listing schedules (`GET`) returns every schedule for the context, enabled and disabled alike. ## Pausing vs. deleting a schedule *** When you need to stop a recurring reconciliation, you have two options — and the choice matters. **Disable** the schedule to pause automatic runs while keeping its configuration and history. Re-enabling later is a single call, with no need to recreate it. Update the cron expression, toggle `enabled`, or both (omitted fields are left unchanged): ```bash cURL theme={null} curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}/schedules/{scheduleId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "cronExpression": "0 6 * * *", "enabled": false }' ``` **Delete** the schedule (`DELETE .../schedules/{scheduleId}`) only when the cadence is gone for good — deletion is permanent. To simply take a break, disable instead. ## Best practices *** Schedule runs to fire after all sources for the context have delivered their data for the period. Running before ingestion completes produces avoidable exceptions. When pausing a reconciliation cadence, disable the schedule so history and configuration are preserved and re-enabling is a single call. Keep cron expressions readable and documented (e.g. `0 0 * * *` = daily at 00:00). Verify the timezone assumptions for your deployment before relying on a schedule for SLA-sensitive runs. ## Next steps *** Configure the contexts and sources a schedule reconciles. Review the results produced by scheduled runs. # Generating reports Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-generating-reports Generate reconciliation summaries, match details, exception views, and variance reports to monitor outcomes and support compliance. Reports turn a reconciliation run into something you can act on: how much matched, what's still open, and how much money is exposed. Operations teams use them to work the day's queue; finance and compliance use them to close the books and document outcomes. ## Available reports *** Each report answers a different question: * **Reconciliation summary**: A high-level snapshot of match rates, exception volume, and total variances. * **Match detail report**: A complete list of matches, including transaction details, confidence scores, and variance breakdowns. * **Unmatched report**: A list of transactions that remain unmatched for follow-up. * **Exception report**: A focused view of unresolved exceptions with aging, severity, and resolution status. * **Variance report**: A breakdown of differences between matched transactions. Use the reporting dashboard endpoints to access reconciliation metrics and export data. API Reference: * [Dashboard aggregates](/en/reference/matcher/get-dashboard-aggregates) * [Export matched report](/en/reference/matcher/export-matched-report) * [Export unmatched report](/en/reference/matcher/export-unmatched-report) ## Dashboard analytics *** The reporting dashboard provides real-time reconciliation metrics for a context and date range. Reporting endpoints default to the 30-day window ending tomorrow (UTC) and normally limit the window to 90 days. Only the unmatched list and count endpoints accept `unbounded=true` to query without date bounds. ### Dashboard aggregates Use the combined dashboard aggregates endpoint for a single call that returns volume, match-rate, and SLA statistics for a context: ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/dashboard?date_from=2025-01-01&date_to=2025-01-31" \ -H "Authorization: Bearer $TOKEN" ``` `GET /v1/reports/contexts/{contextId}/dashboard` accepts `date_from`, `date_to`, and an optional `source_id` filter, and returns a `DashboardAggregatesResponse`: | Field | Description | | ----------- | ------------------------------------------------- | | `volume` | Transaction volume statistics for the range | | `matchRate` | Match-rate statistics (matched vs. total) | | `sla` | SLA statistics for exception handling | | `updatedAt` | When the aggregates were last computed (RFC 3339) | More granular dashboard slices are available under `/v1/reports/contexts/{contextId}/dashboard/*` (for example `metrics`, `match-rate`, `sla`, `volume`, `source-breakdown`, and `cash-impact`). There is no bare `GET /v1/reports/contexts/{contextId}` endpoint. Report data is served through the typed sub-paths under `/v1/reports/contexts/{contextId}/...` — for example `dashboard`, `summary`, `matched`, `unmatched`, and `variance` (each with an `/export` variant). API Reference: [Get dashboard aggregates](/en/reference/matcher/get-dashboard-aggregates) ### Source breakdown View reconciliation performance by source — including match rates, transaction counts, and unmatched amounts: ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/dashboard/source-breakdown?date_from=2025-01-01&date_to=2025-01-31" \ -H "Authorization: Bearer $TOKEN" ``` API Reference: [Get source breakdown](/en/reference/matcher/get-source-breakdown) ### Cash impact Assess the total financial exposure from unmatched transactions, broken down by currency and age: ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/dashboard/cash-impact?date_from=2025-01-01&date_to=2025-01-31" \ -H "Authorization: Bearer $TOKEN" ``` The response includes `byCurrency` and `byAge` breakdowns to help prioritize resolution efforts. API Reference: [Get cash impact](/en/reference/matcher/get-cash-impact) ## Pagination *** The matched, unmatched, and variance report endpoints use cursor-based pagination. Pass the `cursor` value from a previous response to retrieve the next page of results. If an invalid cursor value is provided, the API returns a `400 Bad Request` error with a message indicating the pagination parameters are invalid. Previous versions returned a `500` error in this case. ## Quick counts *** Use count endpoints for lightweight status checks without fetching full result sets: | Endpoint | Description | | -------------------------------------------------------------- | ------------------------------------- | | [Count matches](/en/reference/matcher/count-matches) | Total matched items in a date range | | [Count transactions](/en/reference/matcher/count-transactions) | Total transactions in a date range | | [Count exceptions](/en/reference/matcher/count-exceptions) | Total exceptions in a date range | | [Count unmatched](/en/reference/matcher/count-unmatched) | Total unmatched items in a date range | ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/matches/count?date_from=2025-01-01&date_to=2025-01-31" \ -H "Authorization: Bearer $TOKEN" ``` Each count endpoint returns a single `count` value — ideal for lightweight dashboards or health checks that don't need the full result set. ## Best practices *** Automate a daily summary report delivered each morning to keep stakeholders aligned. Store reports in secure, durable storage. Financial artifacts often require multi-year retention. Generate targeted reports by date and source—avoid exporting everything by default. Include source names, rule names, and key identifiers so the output can stand alone outside Matcher. Large reports can fail or stall. Export jobs use `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED`, `EXPIRED`, and `CANCELED`; alert on `FAILED` or prolonged `RUNNING`. ## Next steps *** Set up reconciliation contexts and data sources. Learn how access control and data protection work in Matcher. # Resolving exceptions Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-resolving-exceptions Review, prioritize, and resolve transactions Matcher couldn't reconcile automatically using severity, lifecycle, and audit-friendly actions. Exceptions are transactions that Matcher can't reconcile automatically. This guide shows how to review exceptions, prioritize work based on severity, and resolve items with the right level of documentation. ## What is an exception? *** An exception is created when a transaction from one source has no valid counterpart in another source. Common causes include: * **No candidate found**: No transaction in the other source meets the active rule criteria. * **Below confidence threshold**: Candidates exist, but score below the minimum confidence (default: 60). * **Duplicate rejection**: A previous match was rejected and no alternative candidate remains. * **Source imbalance**: One source contains transactions that are missing from the other. ## Exception lifecycle *** Exceptions move through a simple workflow: * When Matcher can't reconcile a transaction, it creates an exception in `OPEN` status. * Assigning the exception moves it from `OPEN` to `ASSIGNED`. The API does not expose an unassign operation; `assignee` is required and must be non-empty. * Force match and adjust entry persist `PENDING_RESOLUTION` only while the operation is in progress. Success moves the exception to `RESOLVED`; failure returns it to its previous `OPEN` or `ASSIGNED` status. * Direct resolution moves an `OPEN` or `ASSIGNED` exception to `RESOLVED`. * Dispatch sends the connector request, writes a `DISPATCH` audit event, and emits `exception.dispatched`. It does not change the exception status. Matcher Exception Lifecycle ### Status definitions | Status | Description | Who Can Transition | | -------------------- | ------------------------------------------ | ------------------ | | `OPEN` | New exception waiting for assignment | System | | `ASSIGNED` | Assigned to an analyst for investigation | System, Analyst | | `PENDING_RESOLUTION` | Force match or adjust entry is in progress | System | | `RESOLVED` | Closed with an auditable resolution | Analyst, System | ### State-machine endpoints The following single-exception endpoints change the lifecycle or record related actions. Each is addressed by the exception's `exceptionId` in the path. | Endpoint | Method & path | Purpose | | -------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Assign exception | `POST /v1/exceptions/{exceptionId}/assign` | Assigns the exception to an analyst. Body: `assignee` (required). Returns the updated exception. (`OPEN` → `ASSIGNED`) | | Dispatch exception | `POST /v1/exceptions/{exceptionId}/dispatch` | Sends the exception through the configured connector. Writes a `DISPATCH` audit event and emits `exception.dispatched` without changing status. | | Resolve exception | `POST /v1/exceptions/{exceptionId}/resolve` | Resolves a single exception. Body: `resolution` (required), `reason` (optional). Mirrors bulk-resolve validation for one exception. (`OPEN` or `ASSIGNED` → `RESOLVED`) | | Force match | `POST /v1/exceptions/{exceptionId}/force-match` | Resolves an exception with `overrideReason` and `notes`. Uses `PENDING_RESOLUTION` while the operation is in progress, then resolves or returns to the previous status on failure. | | Adjust entry | `POST /v1/exceptions/{exceptionId}/adjust-entry` | Resolves an exception by creating an accounting adjustment entry. Body: `amount`, `currency`, `effectiveAt`, `notes`, `reasonCode` (all required). Uses `PENDING_RESOLUTION` while the operation is in progress, then resolves or returns to the previous status on failure. | | Exception history | `GET /v1/exceptions/{exceptionId}/history` | Returns the ordered history of state transitions and actions for the exception (`HistoryResponse`). Supports `cursor`/`limit` pagination. *(read-only)* | | Select exception IDs | `GET /v1/exceptions/ids` | Returns the full set of exception IDs matching the current filters (`contextId`, `status`, `severity`, `reason`, …). Use it to drive a bulk selection before calling the bulk endpoints. *(read-only)* | Matcher wires the webhook connector path and callback handling. JIRA connector code exists but is not configured by default. `MANUAL` acknowledges the dispatch locally without calling an external system. ServiceNow dispatch is not implemented: `SERVICENOW` reaches the generic unsupported-destination failure path and returns HTTP 500. See [Exception routing](/en/matcher/configuration/matcher-exception-routing) for the full dispatch contract. API Reference: * [Assign exception](/en/reference/matcher/assign-exception) * [Dispatch exception](/en/reference/matcher/dispatch-exception) * [Resolve exception](/en/reference/matcher/resolve-exception) * [Force match](/en/reference/matcher/force-match-exception) * [Adjust entry](/en/reference/matcher/adjust-entry-exception) * [Get exception history](/en/reference/matcher/retrieve-exception-history) * [Select exception IDs](/en/reference/matcher/select-exception-ids) #### Assign example ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/assign" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "assignee": "john.doe@company.com" }' ``` #### Resolve example ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/resolve" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resolution": "ACCEPTED", "reason": "Variance within tolerance" }' ``` #### Adjust entry example ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/adjust-entry" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "amount": "150.50", "currency": "BRL", "effectiveAt": "2026-02-02T16:40:00Z", "reasonCode": "AMOUNT_CORRECTION", "notes": "Correcting processing fee discrepancy" }' ``` #### Bulk selection with `selectExceptionIDs` ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/exceptions/ids?contextId={contextId}&status=OPEN&severity=CRITICAL" \ -H "Authorization: Bearer $TOKEN" ``` Feed the returned IDs into the [bulk operations](#bulk-operations) below. ## Exception severity *** Matcher classifies exceptions by severity so you can work the queue in the right order. | Severity | Criteria | SLA | | ------------ | ------------------------------------- | ------------------ | | **Critical** | Amount >= 100,000 OR age >= 120 hours | 24 hours | | **High** | Amount >= 10,000 OR age >= 72 hours | 72 hours | | **Medium** | Amount >= 1,000 OR age >= 24 hours | 120 hours (5 days) | | **Low** | All others | 168 hours (7 days) | ### Severity escalation Severity is re-evaluated as an exception ages. The classification uses OR logic — either the amount or the age threshold is enough to trigger a higher severity: * An exception under 1,000 starts as **Low**, but escalates to **Medium** after 24 hours. * An exception under 10,000 escalates to **High** after 72 hours. * Any unresolved exception escalates to **Critical** after 120 hours. ## Resolution methods *** Matcher exposes three exception-resolution actions. ### 1. Resolve directly Close an exception with a required `resolution` and an optional `reason` when no force match or adjustment is needed. ### 2. Force match Manually link transactions when you've confirmed they belong together, but the system couldn't match them. **Use Force Match when:** * The correct counterpart exists, but variances blocked automatic matching. * You can clearly explain and document the rationale. * The variance is expected (fees, timing, rounding). Force Match bypasses scoring and rule logic. Use it only when you can justify the decision in writing. ### 3. Create adjustment Create an adjusting entry to account for a variance or to balance an unmatched item. **Adjustment reason codes:** | Reason code | Use case | | --------------------- | ------------------------------------ | | `AMOUNT_CORRECTION` | Correct the transaction amount | | `CURRENCY_CORRECTION` | Correct the transaction currency | | `DATE_CORRECTION` | Correct the effective date | | `OTHER` | Record another documented correction | **Validation rules:** * Adjustment amounts must be positive. A request with a zero or negative amount returns a `400 Bad Request` error. * `POST /v1/exceptions/{exceptionId}/adjust-entry` requires a valid ISO 4217 currency code. `POST /v1/matching/adjustments` accepts any non-empty currency string and does not validate ISO 4217 membership. * `reasonCode` must use `AMOUNT_CORRECTION`, `CURRENCY_CORRECTION`, `DATE_CORRECTION`, or `OTHER`. ## Resolution records *** Matcher records supported resolution actions in the exception history and audit stream. | Resolution | Request fields | | ----------------- | ---------------------------------------------------------- | | Direct resolution | `resolution` (required), `reason` (optional) | | Force match | `overrideReason`, `notes` | | Adjust entry | `reasonCode`, `amount`, `currency`, `effectiveAt`, `notes` | Matcher does not expose exception split or independent write-off resolution contracts, and it does not enforce amount-based approval thresholds for these actions. Apply any additional approval requirements through your organization's controls. ## Bulk operations *** When dealing with large volumes of exceptions, bulk endpoints let you process up to 100 exceptions in a single request. ### Bulk assign Assign multiple exceptions to a team member at once: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/bulk/assign" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "exceptionIds": ["019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "019c96a0-2b20-7123-9a1b-2c3d4e5f6a7b"], "assignee": "john.doe@company.com" }' ``` API Reference: * [Bulk assign](/en/reference/matcher/bulk-assign-exceptions) * [Bulk resolve](/en/reference/matcher/bulk-resolve-exceptions) * [Bulk dispatch](/en/reference/matcher/bulk-dispatch-exceptions) ### Bulk resolve Resolve multiple exceptions with a shared resolution: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/bulk/resolve" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "exceptionIds": ["019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "019c96a0-2b20-7123-9a1b-2c3d4e5f6a7b"], "resolution": "ACCEPTED", "reason": "Verified as valid bank fees" }' ``` The response includes `succeeded` and `failed` arrays, so you can handle partial failures gracefully. ### Bulk dispatch Dispatch multiple exceptions to an external system: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/bulk/dispatch" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "exceptionIds": ["019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f"], "targetSystem": "WEBHOOK", "queue": "RECON-TEAM" }' ``` ## Exception comments *** Comments give each exception an audit trail of investigation notes and team discussion — invaluable when someone else has to pick up or review the case later. Add a comment as an analyst works an item: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/comments" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "content": "Contacted bank to verify wire transfer fee. Awaiting confirmation." }' ``` Listing (`GET`) returns the full thread ordered oldest-first. You cannot add comments after an exception is resolved. Only the comment author can delete it, and the comment must belong to the exception identified in the URL. | Action | Method & path | Key fields | | -------------- | ---------------------------------------------------------- | ----------------------------------------- | | Add comment | `POST /v1/exceptions/{exceptionId}/comments` | `content` (comment body) | | List comments | `GET /v1/exceptions/{exceptionId}/comments` | — (returns the full thread, oldest first) | | Delete comment | `DELETE /v1/exceptions/{exceptionId}/comments/{commentId}` | `commentId` in path | API Reference: * [List comments](/en/reference/matcher/list-exception-comments) * [Add comment](/en/reference/matcher/add-exception-comment) * [Delete comment](/en/reference/matcher/delete-exception-comment) ## Disputes *** When an exception needs formal investigation or involves an external party — a chargeback, a bank query — escalate it to a **dispute**. Disputes track evidence, state changes, and the final outcome. List disputes with `GET /v1/disputes` (filter by `state`, e.g. `OPEN`) or retrieve one by its `disputeId`. API Reference: * [List disputes](/en/reference/matcher/list-disputes) * [Get dispute](/en/reference/matcher/retrieve-dispute) * [Open dispute](/en/reference/matcher/open-dispute) * [Close dispute](/en/reference/matcher/close-dispute) ### Dispute states and transitions A dispute has five states: `DRAFT`, `OPEN`, `PENDING_EVIDENCE`, `WON`, and `LOST`. The flow is **not** strictly linear: * `PENDING_EVIDENCE` is **optional** — an `OPEN` dispute can go directly to `WON` or `LOST` without ever collecting evidence. * A `LOST` dispute can be **reopened** back to `OPEN`. * `WON` is terminal. The complete set of valid transitions: | From state | Allowed next states | Notes | | ------------------ | --------------------------------- | ------------------------------------------------------- | | `DRAFT` | `OPEN` | Dispute is opened for investigation | | `OPEN` | `PENDING_EVIDENCE`, `WON`, `LOST` | Can resolve directly, or request evidence first | | `PENDING_EVIDENCE` | `OPEN`, `WON`, `LOST` | Returns to `OPEN` or resolves once evidence is reviewed | | `WON` | *(none)* | Terminal state | | `LOST` | `OPEN` | A lost dispute can be reopened | ## Exception resolution workflow *** Use this flow to keep reviews consistent and audit-friendly. Review the queue by severity and SLA. Start with Critical and High. Use the exception payload to understand what failed and what candidates exist. * Read `reason_details` to see why matching failed. * Review `candidates` for near matches below threshold. * Look for patterns (same counterparty, recurring reference formats). Pick the resolution that best reflects reality and policy. * **Resolve directly**: You can close the exception without a force match or adjustment. * **Force Match**: You found the correct counterpart. * **Adjust**: You need an adjusting entry for variance. Capture enough detail for someone else to replay your decision later: * What you checked * What you concluded * Links or IDs for supporting evidence If the exception requires external handling, dispatch it through the configured webhook connector. Dispatch records the action but does not change the exception status. JIRA requires connector configuration that Matcher does not provide by default; ServiceNow is unavailable. ## Best practices *** Start with Critical and High items. They carry the highest risk and the tightest deadlines. Notes aren't optional. Treat them as part of the resolution: * What you checked * Why this resolution is correct * Any ticket IDs, statements, or confirmations Repeating exceptions usually point to configuration issues: * Same counterparty → Normalize names or mapping * Same date window → Validate ingestion completeness * Same source → Review field mapping and sign conventions If you force-match regularly, your rules or tolerances need attention. Assign exceptions through the assignment endpoints. Matcher does not apply assignment rules automatically. ## Next steps *** Create reconciliation reports, export results, and support audits. Review severity, SLA, and routing concepts for exceptions. # Reviewing matches Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-reviewing-matches Review matches after a run — understand the 0–100 confidence score, its four weighted components, variance causes, and how to reject or revoke a match group. After running a matching job, you'll need to review the results. This guide explains how to interpret match results, understand confidence scores, and reject proposed or revoke confirmed match groups. ## Match status lifecycle *** Matches progress through a defined lifecycle: * When the matching engine finds a pair of transactions that belong together, it creates a match in `PROPOSED` status. * Eligible engine-created matches (score 90 or above) from EXACT and TOLERANCE rules are auto-confirmed immediately. Manual matches are created in `CONFIRMED` status. FUZZY and DATE\_LAG matches remain `PROPOSED` and are never auto-confirmed. * Matches that are not auto-confirmed remain in `PROPOSED`. The public match-group API does not expose manual confirmation, but you can reject a proposed group with the unmatch operation and a required reason. * Rejecting a `PROPOSED` group returns its transactions to the unmatched pool. A `CONFIRMED` group can be revoked only when Matcher can also reverse every residual/open-item effect that confirmation applied; on success, those changes and the transaction return happen atomically. **FUZZY and DATE\_LAG matches are never auto-confirmed.** Auto-confirmation at score ≥ 90 applies to eligible engine-created EXACT and TOLERANCE matches. A manual match is created in `CONFIRMED` status. A match produced by a FUZZY or DATE\_LAG rule stays in `PROPOSED`, regardless of its score — even a FUZZY match scoring 90+. You can reject it through the unmatch operation; there is no public manual-confirm operation. See [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring#fuzzy-matches-never-auto-confirm). Match Status Lifecycle ### Status definitions | Status | Description | Next Actions | | ----------- | ----------------------------------------------------- | ------------------------------------------------- | | `PROPOSED` | Match identified by the system but not confirmed | Review or reject through the unmatch operation | | `CONFIRMED` | Match was auto-confirmed or created as a manual match | Revoke through the unmatch operation if incorrect | | `REJECTED` | Match was declined | Transactions return to unmatched pool | | `REVOKED` | A previously confirmed match was revoked | Transactions return to unmatched pool | ## Confidence tiers *** Matcher assigns a confidence score (0-100) to each proposed match. The score determines how the match is handled. ### Confidence levels | Tier | Score range | Behavior | | --------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Auto-confirm eligible | 90-100 | Eligible engine-created EXACT and TOLERANCE matches are automatically confirmed without manual review. Manual matches are created `CONFIRMED`; FUZZY and DATE\_LAG matches remain `PROPOSED`. | | Needs Review | 60-89 | Medium confidence matches remain `PROPOSED`. You can review and reject them, but the public API does not expose manual confirmation. | | No Match | Below 60 | Low confidence candidates are not proposed as matches and become exceptions. | ### Understanding the score The confidence score is calculated from weighted components: | Component | Weight | What it Measures | | --------------- | ------ | ------------------------------------- | | Amount match | 40% | How closely transaction amounts align | | Currency match | 30% | Whether currencies are the same | | Date tolerance | 20% | How close the transaction dates are | | Reference match | 10% | Whether transaction references match | **Example Score Breakdown:** ``` Match: BANK-001 ↔ LED-001 Amount: $1,000.00 vs $1,000.00 → 100% × 40% = 40 points Currency: USD vs USD → 100% × 30% = 30 points Date: 2024-01-15 vs 2024-01-15 → 100% × 20% = 20 points Reference: PAY-001 vs PAY-001 → match → 10 points ───────────────────────── Total Confidence: 100 points ``` ## Understanding variances *** When matches have differences, review the variance details: ### Amount variance Common causes of amount variance: * Bank fees * Currency conversion differences * Rounding differences * Partial payments ### Date variance Common causes of date variance: * Settlement timing * Time zone differences * Posting vs. transaction date * Weekend/holiday processing ## Match candidates, open items, and adjustments *** As you work a review queue, three questions come up again and again: *why did the engine propose this pairing?*, *what's still unsettled after a partial match?*, and *how do I book a small difference so both sides balance?* Matcher answers each with a dedicated surface — match candidates, open items, and adjustments. ### List match candidates Retrieve the ranked candidate proposals the engine considered for a transaction — the "why" behind a proposed match, including the per-component contributions. ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/matching/candidates?contextId={contextId}&transactionId={transactionId}&limit=20" \ -H "Authorization: Bearer $TOKEN" ``` `GET /v1/matching/candidates` accepts `contextId`, `transactionId`, and `limit` query parameters and returns a `CandidateProposalsResponse`. The `limit` defaults to 50 and accepts values from 1 to 200. The candidate picker scans at most 5,000 unmatched transactions from the opposite side and returns only 1:1 candidates. It applies the shared scorer to raw transaction data, without the run-time fee normalization or FX variance-band matching used by a matching run. ### List open items Open items are residual balances left when a transaction is only partially netted. Track them to surface amounts that still need clearing or that have aged past their threshold. ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/matching/contexts/{contextId}/open-items?status=OPEN&limit=50" \ -H "Authorization: Bearer $TOKEN" ``` `GET /v1/matching/contexts/{contextId}/open-items` supports a `status` filter, plus `limit`/`cursor` pagination. #### Open item statuses | Status | When it occurs | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OPEN` | Fresh residual — the item has a remaining balance and no leg has netted against it yet. | | `PARTIALLY_CLEARED` | At least one leg has netted against the balance, but a residual still remains. | | `CLEARED` | The residual has been fully netted within tolerance. | | `AGED` | The item remained open beyond its aging threshold, measured from the obligation business date or the legacy first-seen time. | | `WITHDRAWN` | A confirmed-group unmatch removed the final live contribution behind the obligation. The item remains as history, but is terminal and cannot be netted or carried forward. | The normal lifecycle flows `OPEN` → `PARTIALLY_CLEARED` → `CLEARED`, with any still-open item able to become `AGED` once it passes the aging threshold. A successful confirmed-group unmatch withdraws that group's residual effects. If no live contribution remains behind the obligation, the item ends `WITHDRAWN` — it was taken back, not cleared or aged. ### Create an adjustment Post an accounting adjustment to account for a variance (bank fee, FX difference, rounding, write-off, and so on) against a match group or transaction. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/adjustments?contextId={contextId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "amount": "10.50", "currency": "BRL", "direction": "DEBIT", "type": "BANK_FEE", "reason": "Variance due to bank processing fee", "description": "Bank wire fee adjustment", "matchGroupId": "019c96a0-0b74-768c-8d25-2bf065dca2f8" }' ``` `POST /v1/matching/adjustments` requires the `contextId` query parameter. Body fields: Adjustment amount Non-empty currency string. Matcher does not validate ISO 4217 membership for this adjustment. `DEBIT` or `CREDIT` `BANK_FEE`, `FX_DIFFERENCE`, `ROUNDING`, `WRITE_OFF`, or `MISCELLANEOUS` Business reason for the adjustment Human-readable description Match group the adjustment applies to Transaction the adjustment applies to At least one of `matchGroupId` or `transactionId` is required. API Reference: * [List match candidates](/en/reference/matcher/list-match-candidates) * [List open items](/en/reference/matcher/list-open-items) * [Create adjustment](/en/reference/matcher/create-adjustment) ## Rejecting or revoking match groups *** Use the **unmatch** operation to reject a `PROPOSED` group or revoke a `CONFIRMED` group. After a successful operation, Matcher returns the group's transactions to `UNMATCHED` so they can be matched again. Use `DELETE /v1/matching/groups/{matchGroupId}` with the required `contextId` query parameter and a `reason` in the request body (operationId `unmatch`): ```bash cURL theme={null} curl -X DELETE "https://api.matcher.example.com/v1/matching/groups/{matchGroupId}?contextId={contextId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reason": "incorrect match - amounts do not match" }' ``` A successful unmatch returns **204 No Content**. The `reason` field is required (non-empty). For a `CONFIRMED` group, Matcher verifies the residual/open-item reversal before changing the group or its transactions; a successful operation atomically appends the compensating ledger entries, revokes the group, and returns its transactions to `UNMATCHED`. The unmatch operation requires a non-empty reason. For a confirmed group, it either reverses the residual/open-item effects together with the group and its transactions, or returns **409 Conflict** before changing anything. A later live entry on the residual blocks the earlier reversal; a newer live obligation on the same identity also blocks it when restoring a terminal item would conflict. ### When to reject or revoke Common scenarios for rejecting or revoking a group: * **Incorrect match confirmed**: The match was confirmed but the transactions belong to different records * **New information**: Additional data shows the match is wrong * **Source correction**: The source system issued a correction or reversal * **Duplicate transaction**: One of the transactions was a duplicate that should be removed ### What happens after unmatch When a match group is unmatched: 1. **Confirmed residuals are checked first**: Matcher verifies that every residual/open-item effect of a `CONFIRMED` group can be reversed. If a later entry or a conflicting newer obligation blocks it, the operation returns `409 Conflict` and leaves the group, transactions, and open items unchanged. 2. **Match status changes**: A `CONFIRMED` group becomes `REVOKED`; a still-`PROPOSED` group becomes `REJECTED`. 3. **Transactions returned**: On success, all associated transactions revert to `UNMATCHED` status. 4. **Open-item effects reversed**: On a confirmed-group unmatch, compensating ledger entries take back the group's residual effects in the same transaction. If that leaves no live contribution behind an obligation, it becomes terminal `WITHDRAWN` and is not carried forward. 5. **Reason recorded**: The group stores the supplied rejection or revocation reason. 6. **Streaming event emitted**: A `match_group.unmatched` event is emitted only when the group was previously `CONFIRMED`. 7. **Re-matching possible**: Transactions can be matched again in the next run after a successful unmatch. ## Best practices *** Review matches with the lowest confidence scores first. These are most likely to be incorrect and need the most attention. Confidence thresholds are fixed in the system. Eligible engine-created EXACT and TOLERANCE matches with scores of 90 or above are auto-confirmed. Manual matches are created `CONFIRMED`. Scores between 60 and 89 remain `PROPOSED`, and scores below 60 become exceptions. These values are not configurable per context. **FUZZY and DATE\_LAG matches are the exception: they are never auto-confirmed and remain `PROPOSED`, regardless of score.** Use rule tuning (priority, tolerance values) to influence how many matches land in each tier. Provide a specific reason when rejecting a proposed group or revoking a confirmed group. The unmatch operation requires it and stores it with the group. Review the group and its source transactions before using the unmatch operation. Matcher exposes this operation per group; it does not expose bulk confirmation. Regardless of confidence score, give extra attention to high-value matches. The impact of an incorrect match is proportional to the amount. ## Next steps *** Handle transactions that couldn't be matched automatically. Deep dive into how confidence scores are calculated. # Uploading files Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-uploading-files Import transaction data into Matcher from CSV, JSON, XML, or banking formats like camt.053 — preview column detection, then upload files to a source. This guide covers how to import transaction data from external sources into Matcher for reconciliation. ## Supported formats *** Matcher accepts transaction files in three general-purpose formats: * **CSV**: Comma-separated values with headers. Most common for bank exports. * **JSON**: Array of transaction objects. Best for API integrations. * **XML**: Structured elements. Common for enterprise systems. Beyond these, the upload endpoint also accepts specialized banking formats such as `camt053` and the namespaced descriptor keys from the format catalog (CNAB, acquirer layouts) — see [Import formats](/en/matcher/imports/matcher-import-formats) for the full catalog. ## File structure requirements *** Each file must contain transaction records with fields that can be mapped to Matcher's internal schema. ### Required fields Every transaction must have these fields (or mappable equivalents): | Field | Type | Description | | ---------------- | ------------- | ----------------------------------------- | | `transaction_id` | String | Unique identifier within the source | | `amount` | Decimal | Transaction amount (positive or negative) | | `currency` | String | ISO 4217 currency code | | `date` | Date/DateTime | Transaction date | ### Optional fields | Field | Type | Description | | -------------- | ------ | -------------------------------------- | | `reference` | String | External reference or description | | `counterparty` | String | Other party in the transaction | | `type` | String | Transaction type (credit, debit, etc.) | | `metadata` | Object | Additional custom fields | ## Format examples *** ### CSV **CSV Requirements:** * First row must be column headers * UTF-8 encoding * Comma delimiter (configurable) * Quote fields containing commas or newlines **Code example** ```csv theme={null} transaction_id,amount,currency,date,reference,type BANK-2024-001,1500.00,USD,2024-01-15,Invoice #1234,credit BANK-2024-002,-250.00,USD,2024-01-15,Service fee,debit BANK-2024-003,3200.50,USD,2024-01-16,Customer payment,credit BANK-2024-004,-89.99,USD,2024-01-16,Subscription,debit ``` ### JSON **JSON Requirements:** * Root element must be an array * Consistent field names across objects * UTF-8 encoding **Code example** ```json theme={null} [ { "transaction_id": "BANK-2024-001", "amount": 1500.0, "currency": "USD", "date": "2024-01-15", "reference": "Invoice #1234", "type": "credit" }, { "transaction_id": "BANK-2024-002", "amount": -250.0, "currency": "USD", "date": "2024-01-15", "reference": "Service fee", "type": "debit" } ] ``` ### XML **XML Requirements:** * Valid XML with declaration * Root element containing transaction elements * UTF-8 encoding **Code example** ```xml theme={null} BANK-2024-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" } ``` API Reference: [Preview file](/en/reference/matcher/preview-upload) ### Single file upload ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/upload" \ -H "Authorization: Bearer $TOKEN" \ -F "format=csv" \ -F "file=@bank_statement_january.csv" ``` Send the `format` field **before** the `file` part. If `file` arrives first, the format is inferred from the filename extension for `.csv` and `.json` only; `.xml` is never inferred (it is a format family — plain XML, camt.053 — so the explicit `format` field is required, otherwise the upload is rejected). Upload returns **202 Accepted** with the created job. The upload limit defaults to **1 GiB** and applies to the entire multipart request, including every part, header, and boundary—not only the file. You can configure `ingestion.max_upload_bytes` from **1 MiB** to **8 GiB**. API Reference: [Upload file](/en/reference/matcher/upload-transaction-file) #### Response ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149", "sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "QUEUED", "fileName": "bank_statement_january.csv", "totalRows": 0, "persistedRows": 0, "droppedDuplicateRows": 0, "failedRows": 0, "failureRatePercent": 0, "completedWithErrors": false, "createdAt": "2024-01-20T10:30:00Z" } ``` ### Check import status ```bash cURL theme={null} curl -X GET https://api.matcher.example.com/v1/imports/contexts/{contextId}/jobs/{jobId} \ -H "Authorization: Bearer $TOKEN" ``` API Reference: [Get import status](/en/reference/matcher/retrieve-ingestion-job) #### Response (Processing) ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149", "sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "PROCESSING", "fileName": "bank_statement_january.csv", "totalRows": 1250, "startedAt": "2024-01-20T10:30:05Z" } ``` #### Response (Completed) ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149", "sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "COMPLETED", "completedWithErrors": true, "fileName": "bank_statement_january.csv", "totalRows": 1250, "persistedRows": 1233, "droppedDuplicateRows": 12, "failedRows": 5, "failureRatePercent": 1, "reviewRows": 0, "diagnosis": "", "startedAt": "2024-01-20T10:30:05Z", "completedAt": "2024-01-20T10:30:45Z" } ``` Per-row parse/normalization errors are **not** embedded in the job. When `completedWithErrors` is `true` (or the job `FAILED`), fetch the details from `GET /v1/imports/contexts/{contextId}/jobs/{jobId}/errors` (capped at 100 stored rows, with `totalErrors`/`truncated` accounting). For a wholesale `FAILED` job, `diagnosis` carries a safe one-line reason. ### Import job status values | Status | Description | | ------------ | ------------------------------------------------------------------ | | `QUEUED` | Job received, awaiting a worker | | `PROCESSING` | File is being parsed and normalized | | `COMPLETED` | Import finished (check `completedWithErrors` for partial failures) | | `FAILED` | Import aborted wholesale (see `diagnosis`) | ## Validation and error handling *** Matcher validates uploaded files at multiple stages. ### Validation stages Verifies the file is valid CSV, JSON, or XML with correct structure. Checks that required fields are present and match the configured field map. Validates amounts are valid decimals, dates are parseable, currencies are valid ISO codes. Applies context-specific rules like date ranges, amount limits, etc. ### Common validation errors | Error | Cause | Solution | | ------------------------ | ------------------------------- | ----------------------------------------------- | | `INVALID_FORMAT` | File cannot be parsed | Check file encoding and structure | | `MISSING_REQUIRED_FIELD` | Required field not found | Verify field mapping configuration | | `INVALID_AMOUNT` | Amount not a valid number | Check for currency symbols or commas in numbers | | `INVALID_DATE` | Date cannot be parsed | Use ISO 8601 format (YYYY-MM-DD) | | `UNKNOWN_CURRENCY` | Currency code not recognized | Use ISO 4217 codes (USD, EUR, BRL) | | `DATE_OUT_OF_RANGE` | Date before/after allowed range | Check context date boundaries | ### Handling errors By default, valid rows are imported even if some rows have errors. Configure error handling behavior through context settings or handle errors after import completion by reviewing the job status response. ## Duplicate detection *** Matcher automatically detects and handles duplicate transactions to prevent double-counting. ### How duplicates are detected Duplicates are identified by the row's dedup key within a source: * `source_id` * `external_id` (the source-system transaction identifier) If a row repeats that key—within the same upload or against already-persisted data—it is treated as a duplicate. ### Duplicate handling options Set the `duplicate_policy` key in the source `config` to control handling: | Policy | Behavior | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `KEEP_FIRST` (default) | Keeps the first occurrence and silently drops repeats, counted in `droppedDuplicateRows` | | `REJECT` | Turns each repeated row into an ingestion row error, counted in `failedRows` | | `FLAG_AS_EXCEPTION` | Drops the repeat and raises a `DUPLICATE_TRANSACTION` exception on the surviving transaction (subset reported as `flaggedDuplicateRows`) | When the key is absent, `KEEP_FIRST` applies. ### Viewing duplicate details The import summary shows how many duplicates were found: ```json theme={null} { "totalRows": 1000, "persistedRows": 950, "droppedDuplicateRows": 50, "failedRows": 0, "failureRatePercent": 0 } ``` ## Batch uploads *** For large reconciliation jobs, you can upload multiple files in sequence. ### Upload multiple files ```bash theme={null} # Upload bank statement curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{bankSourceId}/upload" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@bank_january.csv" \ -F "format=csv" # Upload ledger export curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{ledgerSourceId}/upload" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@ledger_january.csv" \ -F "format=csv" ``` ### Wait for all imports Before running matching, ensure all imports are complete: ```bash theme={null} # List imports for context curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/jobs" \ -H "Authorization: Bearer $TOKEN" ``` ## Search uploaded transactions *** After importing files, you can search across all transactions in a context to verify data quality or investigate specific records. ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/transactions/search?q=Invoice&amount_min=1000&status=UNMATCHED" \ -H "Authorization: Bearer $TOKEN" ``` #### Response ```json theme={null} { "items": [ { "id": "019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "amount": "1500.00", "currency": "USD", "date": "2024-01-15T00:00:00Z", "description": "Invoice #1234", "status": "UNMATCHED" } ], "total": 1, "limit": 20, "offset": 0 } ``` API Reference: [Search transactions](/en/reference/matcher/search-transactions) Supported filters include `amount_min`, `amount_max`, `date_from`, `date_to`, `currency`, `source_id`, `status`, and free-text search via the `q` parameter. ## Best practices *** Check file format and encoding locally before uploading. This catches obvious errors faster. ```bash theme={null} # Check CSV is valid head -5 transactions.csv # Check encoding file transactions.csv ``` Standardize on ISO 8601 format (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ`) across all sources to avoid parsing issues. Always include unique transaction IDs from the source system. This enables proper duplicate detection and audit trails. Decide on a convention (negative for debits, positive for credits) and apply it consistently. Document this in your field mapping. For files larger than 50 MB, consider splitting them into smaller chunks by date range. This is a reliability recommendation, not the upload limit, and allows partial retries. For recurring reconciliation, automate file uploads using scheduled jobs or webhooks from source systems. ```bash theme={null} # Example: Daily upload via cron 0 6 * * * /scripts/upload_bank_statement.sh ``` ## Next steps *** Learn how to interpret match results and confidence scores. Configure how source fields map to Matcher's schema. # Installing Matcher Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-installation Deploy Matcher locally with Docker Compose or in production with the official Helm chart — configure PostgreSQL, Redis, RabbitMQ, and object storage. Matcher automates financial reconciliation across multiple data sources, eliminating manual matching work and providing a full audit trail for every transaction. Setting up Matcher means putting in place the foundation for exception management, compliance reporting, and operational visibility. This guide walks you through deploying Matcher in development and production environments. Matcher is available to licensed customers; its repository is maintained internally. The instructions below assume you already have access to the required Matcher project files. ## Docker compose (development) *** Docker Compose is the recommended approach for local development and testing. ### 1. Access the Matcher project From the Matcher project directory: ```bash theme={null} cd matcher ``` ### 2. Configure the environment The `docker-compose.yml` file includes sensible defaults for local development. You can override any value by setting environment variables in your shell or by creating a `.env` file in the project root. Refer to [Environment variables](#environment-variables) for details on available settings. ### 3. Start services Start the required infrastructure services: ```bash theme={null} docker-compose up -d postgres redis rabbitmq ``` Wait until all services report a healthy status: ```bash theme={null} docker-compose ps ``` Start the Matcher application: ```bash theme={null} docker-compose up -d app ``` To start all services at once: ```bash theme={null} docker-compose up -d ``` ### 4. Verify the installation Confirm that Matcher is running by listing configuration contexts. On a fresh install, the cursor-paginated response has an empty `items` array: ```bash theme={null} curl -H "Authorization: Bearer $TOKEN" http://localhost:4018/v1/contexts ``` Then verify the required dependencies through the public readiness endpoint: ```bash theme={null} curl http://localhost:4018/readyz ``` The endpoint returns `200` when every required dependency is ready. It returns `503` with per-check details when a required dependency is unavailable. ### Docker compose services The default `docker-compose.yml` includes: | Service | Port | Purpose | | ------------------ | ----------- | --------------------------------- | | `postgres` | 5432 | PostgreSQL primary database | | `postgres-replica` | 5433 | PostgreSQL read replica | | `redis` | 6379 | Valkey (Redis-compatible) cache | | `rabbitmq` | 5672, 15672 | RabbitMQ (AMQP and management UI) | | `seaweedfs` | 8333, 9333 | S3-compatible object storage | | `app` | 4018 | Matcher API | ### Development with hot reload For active development, use: ```bash theme={null} make dev ``` This starts Matcher with live reload enabled using Air. ## Kubernetes / helm (production) *** Production deployments should use the official Helm chart. ### Prerequisites * Kubernetes 1.28+ * Helm 3.12+ * `kubectl` configured for the target cluster ### 1. Create a namespace ```bash theme={null} kubectl create namespace matcher ``` ### 2. Configure values Create a `values.yaml` file with your deployment configuration: ```yaml theme={null} replicaCount: 2 image: repository: lerianstudio/matcher tag: "latest" pullPolicy: IfNotPresent service: type: ClusterIP port: 4018 ingress: enabled: true className: nginx hosts: - host: matcher.example.com paths: - path: / pathType: Prefix tls: - secretName: matcher-tls hosts: - matcher.example.com postgresql: external: true host: postgres.example.com port: 5432 database: matcher username: matcher existingSecret: matcher-db-credentials existingSecretKey: password redis: external: true host: redis.example.com port: 6379 existingSecret: matcher-redis-credentials rabbitmq: external: true host: rabbitmq.example.com port: 5672 username: matcher existingSecret: matcher-rabbitmq-credentials auth: enabled: true serviceAddress: https://auth.example.com observability: enabled: true otelExporterEndpoint: http://otel-collector:4317 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2000m memory: 2Gi ``` ### 3. Create secrets Create Kubernetes secrets for sensitive credentials: ```bash theme={null} kubectl create secret generic matcher-db-credentials \ --from-literal=password=your-db-password \ -n matcher kubectl create secret generic matcher-redis-credentials \ --from-literal=password=your-redis-password \ -n matcher kubectl create secret generic matcher-rabbitmq-credentials \ --from-literal=password=your-rabbitmq-password \ -n matcher ``` ### 4. Install the chart ```bash theme={null} helm install matcher oci://registry-1.docker.io/lerianstudio/matcher-helm \ --version 4.1.0 \ --namespace matcher \ --values values.yaml ``` ### 5. Verify the deployment ```bash theme={null} kubectl get pods -n matcher kubectl get svc -n matcher kubectl logs -f deployment/matcher -n matcher ``` ### Upgrading To upgrade an existing deployment: ```bash theme={null} helm upgrade matcher oci://registry-1.docker.io/lerianstudio/matcher-helm \ --version 4.1.0 \ --namespace matcher \ --values values.yaml ``` ## Environment variables *** Environment variables provide Matcher's bootstrap configuration. Systemplane can override runtime-mutable settings after startup. ### Application | Variable | Default | Description | | ----------------------- | ------------- | ----------------------------------------------------------- | | `ENV_NAME` | `development` | Runtime environment name | | `LOG_LEVEL` | `info` | Log verbosity | | `DEPLOYMENT_MODE` | `local` | Deployment mode (`local`, `byoc`, `saas`) | | `SERVER_ADDRESS` | `:4018` | HTTP server bind address | | `HTTP_BODY_LIMIT_BYTES` | `104857600` | Maximum buffered request body size (bytes, default 100 MiB) | ### CORS | Variable | Default | Description | | ---------------------- | ------------------------------------------------------- | ----------------------- | | `CORS_ALLOWED_ORIGINS` | `http://localhost:3000` | Allowed origins | | `CORS_ALLOWED_METHODS` | `GET,POST,PUT,PATCH,DELETE,OPTIONS` | Allowed HTTP methods | | `CORS_ALLOWED_HEADERS` | `Origin,Content-Type,Accept,Authorization,X-Request-ID` | Allowed request headers | ### Database (PostgreSQL) | Variable | Default | Description | | ---------------------------------- | ---------------------- | ---------------------------------- | | `POSTGRES_HOST` | `localhost` | Primary database host | | `POSTGRES_PORT` | `5432` | Primary database port | | `POSTGRES_USER` | `matcher` | Username | | `POSTGRES_PASSWORD` | `matcher_dev_password` | Password | | `POSTGRES_DB` | `matcher` | Database name | | `POSTGRES_SSLMODE` | `disable` | SSL mode | | `POSTGRES_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap | | `POSTGRES_MAX_OPEN_CONNS` | `25` | Max open connections | | `POSTGRES_MAX_IDLE_CONNS` | `5` | Max idle connections | | `POSTGRES_CONN_MAX_LIFETIME_MINS` | `30` | Connection max lifetime (minutes) | | `POSTGRES_CONN_MAX_IDLE_TIME_MINS` | `5` | Connection max idle time (minutes) | | `POSTGRES_CONNECT_TIMEOUT_SEC` | `10` | Connection timeout (seconds) | | `POSTGRES_QUERY_TIMEOUT_SEC` | `30` | Query timeout (seconds) | ### Database replica (PostgreSQL) | Variable | Default | Description | | ------------------------------- | ------- | ----------------------- | | `POSTGRES_REPLICA_HOST` | — | Replica host | | `POSTGRES_REPLICA_PORT` | — | Replica port | | `POSTGRES_REPLICA_USER` | — | Replica username | | `POSTGRES_REPLICA_PASSWORD` | — | Replica password | | `POSTGRES_REPLICA_DB` | — | Replica database name | | `POSTGRES_REPLICA_SSLMODE` | — | Replica SSL mode | | `POSTGRES_REPLICA_TLS_REQUIRED` | `false` | Enforce TLS for replica | ### Cache (Redis) | Variable | Default | Description | | ------------------------ | ---------------- | ---------------------------- | | `REDIS_HOST` | `localhost:6379` | Redis address (host:port) | | `REDIS_MASTER_NAME` | — | Sentinel master name | | `REDIS_PASSWORD` | — | Password | | `REDIS_DB` | `0` | Database index | | `REDIS_TLS` | `false` | Enable TLS | | `REDIS_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap | | `REDIS_CA_CERT` | — | CA certificate path | | `REDIS_POOL_SIZE` | `10` | Connection pool size | | `REDIS_MIN_IDLE_CONNS` | `2` | Minimum idle connections | | `REDIS_READ_TIMEOUT_MS` | `3000` | Read timeout (milliseconds) | | `REDIS_WRITE_TIMEOUT_MS` | `3000` | Write timeout (milliseconds) | | `REDIS_DIAL_TIMEOUT_MS` | `5000` | Dial timeout (milliseconds) | ### Messaging (RabbitMQ) | Variable | Default | Description | | -------------------------------------- | ------------------------ | ------------------------------------ | | `RABBITMQ_URI` | `amqp` | URI scheme (`amqp` or `amqps`) | | `RABBITMQ_HOST` | `localhost` | Broker host | | `RABBITMQ_PORT` | `5672` | Broker port | | `RABBITMQ_USER` | `matcher_admin` | Username | | `RABBITMQ_PASSWORD` | `matcher_dev_password` | Password | | `RABBITMQ_VHOST` | `/` | Virtual host | | `RABBITMQ_HEALTH_URL` | `http://localhost:15672` | Management API URL for health checks | | `RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK` | `false` | Allow HTTP (non-TLS) health check | | `RABBITMQ_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap | ### Authentication | Variable | Default | Description | | --------------------- | ------- | ---------------------------------------------------------------------------------------- | | `PLUGIN_AUTH_ENABLED` | `false` | Enable authentication | | `PLUGIN_AUTH_ADDRESS` | — | Auth service URL (token validation is delegated here; Matcher holds no local JWT secret) | ### Object storage (S3-compatible) | Variable | Default | Description | | ---------------------------------------- | ----------------------- | ----------------------------- | | `OBJECT_STORAGE_ENDPOINT` | `http://localhost:8333` | S3 endpoint URL | | `OBJECT_STORAGE_REGION` | `us-east-1` | S3 region | | `OBJECT_STORAGE_BUCKET` | `matcher-exports` | Bucket for exports | | `OBJECT_STORAGE_ACCESS_KEY_ID` | — | Access key ID | | `OBJECT_STORAGE_SECRET_ACCESS_KEY` | — | Secret access key | | `OBJECT_STORAGE_USE_PATH_STYLE` | `true` | Use path-style addressing | | `OBJECT_STORAGE_ALLOW_INSECURE_ENDPOINT` | `false` | Allow HTTP (non-TLS) endpoint | | `OBJECT_STORAGE_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap | ### Observability | Variable | Default | Description | | -------------------------------------- | --------------------------------- | ------------------------------- | | `ENABLE_TELEMETRY` | `false` | Enable OpenTelemetry | | `OTEL_RESOURCE_SERVICE_NAME` | `matcher` | Service name for traces/metrics | | `OTEL_LIBRARY_NAME` | `github.com/LerianStudio/matcher` | Instrumentation library name | | `OTEL_RESOURCE_SERVICE_VERSION` | `1.1.0` | Service version | | `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT` | `development` | Deployment environment label | | `OTEL_EXPORTER_OTLP_ENDPOINT` | `localhost:4317` | OTLP collector endpoint | | `DB_METRICS_INTERVAL_SEC` | `15` | DB metrics collection interval | ### TLS | Variable | Default | Description | | ------------------------- | ------- | ---------------------------------------------------- | | `SERVER_TLS_CERT_FILE` | — | TLS certificate path | | `SERVER_TLS_KEY_FILE` | — | TLS private key path | | `TLS_TERMINATED_UPSTREAM` | `false` | Trust upstream TLS termination (e.g., load balancer) | | `TRUSTED_PROXIES` | — | Trusted proxy CIDR ranges | ### Rate limiting | Variable | Default | Description | | -------------------------------- | ------- | ------------------------------------ | | `RATE_LIMIT_ENABLED` | `true` | Enable global rate limiting | | `RATE_LIMIT_MAX` | `100` | Max requests per window | | `RATE_LIMIT_EXPIRY_SEC` | `60` | Rate limit window (seconds) | | `EXPORT_RATE_LIMIT_MAX` | `10` | Max export requests per window | | `EXPORT_RATE_LIMIT_EXPIRY_SEC` | `60` | Export rate limit window (seconds) | | `DISPATCH_RATE_LIMIT_MAX` | `50` | Max dispatch requests per window | | `DISPATCH_RATE_LIMIT_EXPIRY_SEC` | `60` | Dispatch rate limit window (seconds) | | `ADMIN_RATE_LIMIT_MAX` | `30` | Max admin requests per window | | `ADMIN_RATE_LIMIT_EXPIRY_SEC` | `60` | Admin rate limit window (seconds) | ### Swagger | Variable | Default | Description | | ----------------- | ------- | -------------------------------------- | | `SWAGGER_ENABLED` | `false` | Enable Swagger UI | | `SWAGGER_HOST` | — | Override Swagger spec host | | `SWAGGER_SCHEMES` | `https` | Swagger spec schemes (comma-separated) | ### Idempotency | Variable | Default | Description | | ------------------------------- | ------- | -------------------------------------------------------- | | `IDEMPOTENCY_RETRY_WINDOW_SEC` | `300` | Window for retrying failed idempotent requests (seconds) | | `IDEMPOTENCY_SUCCESS_TTL_HOURS` | `168` | How long completed idempotency keys are cached (hours) | | `IDEMPOTENCY_HMAC_SECRET` | — | HMAC secret for signing idempotency keys (min 32 bytes) | ### Deduplication | Variable | Default | Description | | ---------------- | ------- | ------------------------------------ | | `DEDUPE_TTL_SEC` | `3600` | TTL for deduplication keys (seconds) | ### Outbox | Variable | Default | Description | | ------------------------------ | ------- | ------------------------------------------------ | | `OUTBOX_RETRY_WINDOW_SEC` | `300` | Cooldown before retrying failed events (seconds) | | `OUTBOX_DISPATCH_INTERVAL_SEC` | `2` | Dispatcher poll interval (seconds) | ### Workers | Variable | Default | Description | | --------------------------------- | ------- | -------------------------------------------- | | `EXPORT_WORKER_ENABLED` | `true` | Enable export worker | | `EXPORT_WORKER_POLL_INTERVAL_SEC` | `5` | Export worker poll interval (seconds) | | `EXPORT_WORKER_PAGE_SIZE` | `1000` | Rows per export page | | `EXPORT_PRESIGN_EXPIRY_SEC` | `3600` | Pre-signed URL expiry for exports (seconds) | | `CLEANUP_WORKER_ENABLED` | `true` | Enable cleanup worker | | `CLEANUP_WORKER_INTERVAL_SEC` | `3600` | Cleanup worker interval (seconds) | | `CLEANUP_WORKER_BATCH_SIZE` | `100` | Cleanup batch size | | `CLEANUP_WORKER_GRACE_PERIOD_SEC` | `3600` | Grace period before cleanup (seconds) | | `WEBHOOK_TIMEOUT_SEC` | `30` | Webhook dispatch timeout (seconds) | | `CALLBACK_RATE_LIMIT_PER_MIN` | `60` | Max callbacks per external system per minute | ### Scheduler | Variable | Default | Description | | ------------------------ | ------- | -------------------------------------------- | | `SCHEDULER_INTERVAL_SEC` | `60` | Cron-based scheduler poll interval (seconds) | ### Archival | Variable | Default | Description | | -------------------------------- | ------------------ | -------------------------------------------- | | `ARCHIVAL_WORKER_ENABLED` | `false` | Enable audit log archival worker | | `ARCHIVAL_WORKER_INTERVAL_HOURS` | `24` | Archival run interval (hours) | | `ARCHIVAL_HOT_RETENTION_DAYS` | `90` | Days to keep data in hot storage | | `ARCHIVAL_WARM_RETENTION_MONTHS` | `24` | Months to keep data in warm storage | | `ARCHIVAL_COLD_RETENTION_MONTHS` | `84` | Months to keep data in cold storage | | `ARCHIVAL_BATCH_SIZE` | `5000` | Rows per archival batch | | `ARCHIVAL_STORAGE_BUCKET` | `matcher-archives` | S3 bucket for archives | | `ARCHIVAL_STORAGE_CLASS` | `GLACIER` | S3 storage class for archives | | `ARCHIVAL_PARTITION_LOOKAHEAD` | `3` | Partition lookahead count | | `ARCHIVAL_PRESIGN_EXPIRY_SEC` | `3600` | Pre-signed URL expiry for archives (seconds) | ### Fetcher / Discovery These settings control Discovery, which reads from external databases through an in-process extraction engine embedded in Matcher — not a separate networked service. See [Discovery](/en/matcher/integrations/matcher-discovery) for how it works. | Variable | Default | Description | | -------------------------------- | ------------ | ------------------------------------------------------------------------ | | `FETCHER_DISCOVERY_INTERVAL_SEC` | `60` | Discovery poll interval (seconds) | | `FETCHER_SCHEMA_CACHE_TTL_SEC` | `300` | Schema cache TTL (seconds) | | `FETCHER_EXTRACTION_TIMEOUT_SEC` | `600` | Extraction timeout (seconds) | | `FETCHER_MAX_EXTRACTION_BYTES` | `2147483648` | Max extraction payload size (bytes, default 2 GiB) | | `APP_ENC_KEY` | — | Base64-encoded master key for the embedded engine's credential protector | ### Infrastructure | Variable | Default | Description | | --------------------------- | ------- | --------------------------------------------------- | | `INFRA_CONNECT_TIMEOUT_SEC` | `30` | Infrastructure startup connection timeout (seconds) | | `HEALTH_CHECK_TIMEOUT_SEC` | `5` | Legacy per-check probe timeout (seconds) | | `HEALTH_CHECK_TIMEOUT_MS` | `800` | Per-check probe timeout (milliseconds, preferred) | For multi-tenant deployment settings, see [Multi-Tenant Mode](/en/matcher/configuration/matcher-multi-tenant). For runtime configuration management, see [Runtime Configuration (Systemplane)](/en/matcher/configuration/matcher-systemplane). ## Verify the installation *** Validate that Matcher and its required dependencies are ready: ```bash theme={null} curl http://localhost:4018/readyz ``` The endpoint returns `200` when every required dependency is ready. It returns `503` with per-check details when a required dependency is unavailable. Configure Kubernetes readiness probes to use this endpoint. ## Troubleshooting *** ### Common issues * **Cause:** PostgreSQL is not running or unreachable. * **Resolution:** 1. Verify PostgreSQL is running: `docker-compose ps postgres` 2. Check connection values in `.env` 3. Test connectivity: `nc -zv localhost 5432` 4. Review logs: `docker-compose logs postgres` * **Cause:** Redis is not running or credentials are incorrect. * **Resolution:** 1. Verify Redis is running: `docker-compose ps redis` 2. Confirm `REDIS_PASSWORD` 3. Test connectivity: `redis-cli -h localhost ping` * **Cause:** RabbitMQ is still initializing or the virtual host is missing. * **Resolution:** 1. Wait until RabbitMQ is healthy 2. Access the management UI at [http://localhost:15672](http://localhost:15672) 3. Verify `RABBITMQ_VHOST` * **Cause:** Auth service is unreachable or the token is invalid. * **Resolution:** 1. Verify `PLUGIN_AUTH_ADDRESS` 2. Disable auth for development: `PLUGIN_AUTH_ENABLED=false` 3. Review auth service logs * **Cause:** Database migrations could not be applied. * **Resolution:** 1. Check migration status: `make migrate-status` 2. Review migration logs 3. Apply migrations manually: `make migrate-up` 4. Inspect the `schema_migrations` table if needed ### Viewing logs ```bash theme={null} docker-compose logs -f app kubectl logs -f deployment/matcher -n matcher ``` ### Debug mode Enable debug logging for additional visibility: ```bash theme={null} LOG_LEVEL=debug docker-compose up app ``` ## Next steps *** Run your first reconciliation. Configure contexts, sources, and match rules. # Prerequisites Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-prerequisites Check the infrastructure, dependencies, authentication, and file-format requirements your environment needs before you deploy Matcher for reconciliation. Before deploying Matcher, ensure that your environment meets the requirements described on this page. These prerequisites define the baseline for running reconciliation reliably in development and production environments. ## System requirements *** ### Infrastructure The values below are platform-validated operational starting points, not product-enforced minimums. Adjust them for your transaction volume and retention needs. | Component | Validated starting point / scaling guidance | Purpose | | ----------- | ------------------------------------------- | -------------------------------------------------- | | **CPU** | 2 cores / 4+ cores | Matching and scoring logic is CPU-intensive | | **Memory** | 2 GB / 4+ GB | In-memory processing of transaction batches | | **Storage** | 10 GB / 50+ GB | Persistent storage for transactions and audit logs | ### Dependencies The local Compose stack is the platform-validated dependency baseline. It pins: * **PostgreSQL 17**: Primary data store for reconciliation contexts, transactions, matches, and audit logs. * **Valkey 8**: Redis-compatible service used for caching, duplicate detection, distributed locking, and idempotency control. * **RabbitMQ 4.1.3**: Message broker for asynchronous processing across bounded contexts. ### Runtime The following versions are the platform-validated tooling baseline, not a product support matrix: * **Go 1.26+** (only required when building from source) * **Docker 24+** and **Docker Compose 2.20+** for containerized deployments * **Kubernetes 1.28+** for production-grade deployments using Helm ## Optional: reconciling Midaz data *** Matcher pairs naturally with Midaz Ledger, but there is **no live connector between them** — Matcher has no `MIDAZ_API_URL` and opens no connection to Midaz. Reconciling Midaz data is entirely optional; Matcher works as a stand-alone product reconciling any data sources. ### When to reconcile Midaz data Reconcile ledger data from Midaz if: * You use Midaz as your ledger system * You want to reconcile Midaz postings against external sources (bank statements, gateway reports) ### When Midaz is not involved Matcher works independently when: * Reconciling between external systems (banks, ERPs, payment processors) * Using a different ledger system * Importing ledger data via CSV/JSON/XML files ### How it works Matcher reconciles Midaz data the same way it ingests any source — by import, not a live query: 1. Export the ledger data for the period you want to reconcile. 2. Import that export into a Matcher context as a source of type `LEDGER`. 3. Import the counterparty data (bank statement or gateway report) as the other side. 4. Matcher matches the two sides using your match rules. See the [Matcher and Midaz](/en/matcher/integrations/matcher-midaz-integration) guide for the full flow. ## Authentication *** Matcher uses **lib-auth** for authentication and authorization, consistent with the rest of the Lerian ecosystem. ### Authentication flow 1. The client obtains a JWT from the identity provider 2. The token is sent in the `Authorization: Bearer ***` header. 3. Matcher validates the token via lib-auth 4. Tenant identity and permissions are extracted from token claims ### Required permissions Access to Matcher features is controlled through fine-grained permissions: | Permission | Description | | -------------------- | ------------------------------ | | `contexts:create` | Create reconciliation contexts | | `contexts:read` | View context configuration | | `rules:create` | Create and update match rules | | `imports:create` | Upload transaction files | | `match-runs:run` | Execute matching jobs | | `exceptions:read` | View exceptions | | `exceptions:resolve` | Resolve exceptions | | `reports:read` | Access reports and audit views | ### Single-tenant mode `MULTI_TENANT_ENABLED` controls this mode. Its default is `false`, which makes Matcher use the default tenant below. Authentication state or a missing JWT tenant claim does not switch Matcher into single-tenant mode. ```bash theme={null} # Default tenant configuration (single-tenant mode) DEFAULT_TENANT_ID=11111111-1111-1111-1111-111111111111 DEFAULT_TENANT_SLUG=default ``` ## Generic import formats *** Matcher's generic importers accept CSV, JSON, and XML. Built-in parsers also support CAMT.053, CNAB 240/400, OFX, several acquirer formats, and receivables formats. See the [import format catalog](/en/matcher/imports/matcher-import-formats) for the complete inventory. Each generic format has specific structural requirements for successful ingestion. ### CSV (comma-separated values) Commonly used for bank statements and exports. **Requirements:** * Header row is required * UTF-8 encoding * Comma delimiter (configurable) * Quoted fields for values containing delimiters **Example:** ```csv theme={null} transaction_id,amount,currency,date,reference TXN-001,1000.00,USD,2024-01-15,Invoice payment TXN-002,-250.50,USD,2024-01-16,Refund ``` ### JSON (javascript object notation) Recommended for API-based integrations. **Requirements:** * Valid JSON array of transaction objects * UTF-8 encoding * Consistent field names across records **Example:** ```json theme={null} [ { "transaction_id": "TXN-001", "amount": 1000.0, "currency": "USD", "date": "2024-01-15", "reference": "Invoice payment" } ] ``` ### XML (extensible markup language) Common in enterprise and banking integrations. **Requirements:** * Single root element * UTF-8 encoding * Consistent element structure **Example:** ```xml theme={null} TXN-001 1000.00 USD 2024-01-15 Invoice payment ``` ### File size limits | Limit | Default | Configuration | | ----------------------------- | ------- | ------------------------------------------------------------------------------------ | | Maximum upload file size | 1 GiB | `INGESTION_MAX_UPLOAD_BYTES` at bootstrap or Systemplane at runtime (1 MiB to 8 GiB) | | Maximum buffered request body | 100 MiB | `HTTP_BODY_LIMIT_BYTES` (non-upload requests) | ## Network requirements *** ### Inbound access Matcher exposes a REST API that must be reachable by clients: | Port | Protocol | Purpose | | ---- | ---------- | ----------------------------------------------------------------- | | 4018 | HTTP/HTTPS | API server (default `:4018`; serves HTTPS when TLS is configured) | ### Outbound access Matcher must be able to reach the following services: | Service | Purpose | Required | | ---------------------------- | ------------------------ | ------------------------------------------------------ | | PostgreSQL | Data persistence | Yes | | Redis | Caching and coordination | Yes | | RabbitMQ | Messaging | Yes | | S3-compatible object storage | Exports and archival | When the export worker is enabled (enabled by default) | | Auth service | Token validation | If authentication is enabled | | JIRA | Exception routing | Optional | | Custom webhooks | Event notifications | Optional | ### TLS configuration For production environments, configure TLS: ```bash theme={null} SERVER_TLS_CERT_FILE=/path/to/cert.pem SERVER_TLS_KEY_FILE=/path/to/key.pem ``` ## Environment checklist *** Before proceeding with installation, confirm that: * **Infrastructure is ready**: PostgreSQL, Redis, and RabbitMQ are running and accessible; S3-compatible object storage is also ready when the export worker is enabled (the default) * **Authentication is configured**: Auth service is available, or auth is explicitly disabled * **Network access is validated**: Required inbound and outbound connectivity is in place * **Credentials are available**: Database credentials and API tokens are configured * **Sample data is prepared**: Transaction files are ready for testing (see [Quick Start](/en/matcher/getting-started/matcher-quick-start)) ## Next steps *** Deploy Matcher using Docker or Kubernetes. Run your first reconciliation. # Getting started with Matcher Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-quick-start Walk the five-stage reconciliation lifecycle in Matcher — define a context, connect sources, set rules, run matching, and resolve exceptions step by step. This guide walks through the reconciliation lifecycle in Matcher, from initial setup to reviewing results. It focuses on the concepts and decisions involved at each stage. For step-by-step API instructions with request and response examples, see the [Matcher API quick start](/en/reference/matcher/matcher-developer-quick-start). ## The reconciliation lifecycle *** Every reconciliation in Matcher follows the same five-stage lifecycle: Create a context that describes what you are reconciling and records its reconciliation interval. Register the systems whose transactions you want to compare. Configure the criteria Matcher uses to pair transactions. Upload data and let Matcher find pairs, starting with a preview before committing. Review unmatched transactions and decide how to handle them. The sections below explain each stage. ## Define scope with a context *** A **context** is the top-level container for a reconciliation workflow. It answers three questions: * **What are you reconciling?** For example, a bank account against your general ledger. * **What type of pairing?** One-to-one, one-to-many, or many-to-many. * **What interval label describes the reconciliation period?** For example, `daily`, `weekly`, or `on-demand`. The required `interval` value is free-text metadata; it does not schedule execution. Automated runs use a separate cron-backed `ReconciliationSchedule`, with a minimum cadence of five minutes. | Pairing type | When to use | Example | | ------------ | --------------------------------------------------- | ------------------------------------- | | `1:1` | Each transaction has exactly one counterpart | Bank statement vs. ledger entries | | `1:N` | One record maps to several on the other side | A single invoice paid in installments | | `N:M` | Multiple records on both sides relate to each other | Batch payments split across accounts | Most reconciliations start with `1:1`. You can change the pairing type later as your process evolves. API reference: * [Create context](/en/reference/matcher/create-context) * [Update context](/en/reference/matcher/update-context) ## Connect data sources *** Each context needs at least **two sources**: the systems whose transaction data you want to compare. A source represents a single data feed such as a bank statement, a ledger export, or a payment gateway file. ### Source types | Type | Typical use | | --------- | -------------------------------------------- | | `BANK` | Bank statements and account extracts | | `LEDGER` | General ledger or ERP exports | | `GATEWAY` | Payment processor data (Stripe, Adyen, etc.) | | `FETCHER` | Data extracted through Fetcher/Discovery | | `CUSTOM` | Any other structured data | ### Field mapping Transaction files from different systems rarely use the same column names. **Field maps** translate your source columns into Matcher's standard schema so transactions can be compared. For example, a bank file might call the transaction date "Post Date", while your ledger calls it "posting\_date". Field maps normalize both to Matcher's `date` field. Every transaction must provide at least four fields after mapping: | Field | Description | | ------------- | --------------------------------------- | | `external_id` | Unique identifier within the source | | `amount` | Transaction value | | `currency` | ISO 4217 currency code (e.g., USD, BRL) | | `date` | Transaction date | API reference: [Create source](/en/reference/matcher/create-source) | [Create field map](/en/reference/matcher/create-field-map) ## Set match rules *** Rules define **how Matcher decides whether two transactions are the same**. You can stack multiple rules with different priorities. Matcher evaluates them in order: only transactions left unmatched by the first rule pass to the next. ### Rule types | Rule | What it does | When to use | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | **Exact** | Requires identical values on the selected fields | When data is clean and systems are in sync | | **Tolerance** | Allows small numeric or date differences | When bank fees, rounding, or processing delays cause minor discrepancies | | **Fuzzy** | Uses normalized string similarity for references. Amount, currency, and date must match by default, but you can configure each gate independently. | When free-text or truncated references vary between sources | | **Date lag** | Permits a configurable date window | When settlement dates differ between systems | ### Recommended starting configuration 1. **Priority 1 — Exact rule** on amount, currency, and date. This catches all perfect matches first. 2. **Priority 10 — Tolerance rule** with a small amount tolerance (e.g., 1%) and a 2-day date window. This catches near-matches caused by fees or timing. As you observe results over time, adjust rules or add new ones to improve your match rate. API reference: [Create match rule](/en/reference/matcher/create-match-rule) ## Run matching *** Once sources are configured and data is uploaded, you can run the matching engine. ### Preview first, commit second Matcher supports two execution modes: | Mode | Behavior | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Dry run** | Calculates matches and generates a preview without persisting match artifacts or exceptions. It creates and completes a `MatchRun` record. | | **Commit** | Persists the results: confirmed matches, confidence scores, and exceptions. | Always start with a dry run. Review the preview to verify match quality before committing. ### Understanding confidence scores Every match receives a confidence score from 0 to 100: | Score range | Meaning | Action required | | ----------- | ----------------- | ------------------------------------------------------------------------------------------------- | | 90–100 | High confidence | Eligible for auto-confirmation, except `FUZZY` and `DATE_LAG` groups, which always require review | | 60–89 | Medium confidence | Flagged for manual review | | Below 60 | Low confidence | Not matched — becomes an exception | Scores derive from the matched components and their configured weights. Exact and tolerance rules use the same weighting framework; neither rule type inherently yields higher scores. API reference: [Run match](/en/reference/matcher/run-match) | [List match run groups](/en/reference/matcher/list-match-run-groups) ## Resolve exceptions *** Exceptions are transactions that Matcher could not pair automatically. They represent the items that need human attention. ### Exception severity Matcher classifies each exception by severity based on the transaction amount and how long it has been unmatched: | Severity | Criteria | Suggested SLA | | ------------ | ------------------------------------------- | ------------- | | **Critical** | Amount >= 100,000 or unmatched >= 120 hours | 24 hours | | **High** | Amount >= 10,000 or unmatched >= 72 hours | 72 hours | | **Medium** | Amount >= 1,000 or unmatched >= 24 hours | 5 days | | **Low** | All others | 7 days | ### Resolution options * **Force match** — Manually pair the transaction with a counterpart when you know they belong together. * **Create adjustment** — Record a correcting entry to account for the difference. * **Unmatch** — If a confirmed match is incorrect, undo it so both transactions return to the unmatched pool. * **Dispatch** — Send the exception through its configured JIRA or webhook route. This caller-directed action does not change its status. API reference: * [List exceptions](/en/reference/matcher/list-exceptions) * [Unmatch group](/en/reference/matcher/unmatch-group) ## Example scenario *** A fintech company reconciles its daily bank statement against internal ledger records. **Setup:** * Context: "Daily Bank Reconciliation", type `1:1`, interval `daily` * Two sources: Chase Bank statement (`BANK`) and General Ledger (`LEDGER`) * Two rules: Exact match (priority 1) and Tolerance match with 1% and 2-day window (priority 10) **Daily workflow:** 1. Finance uploads the bank statement and ledger export. 2. Matcher runs a dry run. The preview shows 95% of transactions matched with high confidence. 3. The team reviews the preview and commits the results. 4. Five transactions remain as exceptions: two have small fee differences, three have no counterpart. 5. The team resolves the fee exceptions by creating adjustments. The three missing transactions are escalated for investigation. ## Next steps *** Full guide to setting up reconciliation contexts. Deep dive into all rule types and configuration options. Map different file formats to Matcher's standard schema. Strategies for handling unmatched transactions. # Setup progress Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-setup-progress Read a context's aggregated setup and activation-readiness state in a single request to drive an onboarding checklist or setup wizard. The setup-progress endpoint returns configured-resource counts, the last-run state, and activation readiness for a context in a single aggregate — so a setup wizard or onboarding checklist derives its state from one request instead of many. ## Get setup progress *** ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/contexts/{contextId}/setup-progress" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "contextId": "550e8400-e29b-41d4-a716-446655440000", "status": "DRAFT", "sources": { "total": 2, "left": 1, "right": 1 }, "fieldMaps": { "mappedSources": 2 }, "matchRules": { "total": 3 }, "schedules": { "total": 1 }, "lastRun": { "id": "550e8400-e29b-41d4-a716-446655440000", "status": "COMPLETED", "completedAt": "2025-01-15T10:30:00Z" }, "readiness": { "ready": true, "missing": [] }, "next": null } ``` ## What it returns *** * **`status`** — the context lifecycle status: `DRAFT` (being configured), `ACTIVE` (running), `PAUSED` (suspended), or `ARCHIVED` (retired). * **`sources`** — source counts split by matching side: `total`, `left`, `right`. * **`fieldMaps.mappedSources`** — number of **mapped** sources: those with a field map, plus CAMT.053 sources, which are self-mapped (the parser embeds the ISO 20022 mapping and ignores field maps). * **`matchRules.total`** — match-rule count for the context. * **`schedules.total`** — schedule count for the context. * **`lastRun`** — the most recent match run (`id`, `status` of `PROCESSING`/`COMPLETED`/`FAILED`, and `completedAt`). It is `null` when the context has never run. * **`readiness`** — activation-readiness summary (see below). * **`next`** — the deterministic next setup action to take, or `null` when the context is ready (see below). ## Readiness and the checklist *** The `readiness` block reports whether the context satisfies every activation requirement: ```json theme={null} { "ready": false, "missing": ["field_maps", "match_rules"] } ``` `missing` holds **stable slugs** you can map to per-requirement checklist items. The possible values are: * `sources_left` — the context needs at least one LEFT-side source. * `sources_right` — the context needs at least one RIGHT-side source. * `field_maps` — at least one source is unmapped: it has no field map and is not a self-mapped CAMT.053 source. * `match_rules` — the context needs at least one match rule. * `fee_rules` — the context enables fee normalization but has no fee rule. This requirement is **conditional**: it appears only when `feeNormalization` is set (`NET` or `GROSS`), and it mirrors the run precondition, which gates on fee **rules** — not fee schedules — being non-empty. When a context is transitioned to `ACTIVE` before it is ready, the update is rejected with the generic configuration invalid-state `409`. That response does not include the missing slugs. Re-read setup progress after the conflict and use `readiness.missing` to render the remaining setup guidance. ## The next action *** `next` turns `readiness.missing` into a concrete call. It is derived from `missing[0]` — the first unsatisfied requirement in the stable order above — and is `null` when the context is ready: ```json theme={null} { "slug": "field_maps", "action": "add_field_map", "operationId": "createFieldMap", "method": "POST", "path": "/v1/contexts/{contextId}/sources/{sourceId}/field-maps", "requiredFields": ["mapping"], "forSource": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Bank statement" } } ``` * **`slug`** — the readiness slug this action satisfies. * **`action`** — a stable semantic label you can key UI copy on. * **`operationId`**, **`method`**, **`path`** — the endpoint to call to satisfy the requirement. * **`requiredFields`** — the **names** of the fields that create request requires. They never carry values; you supply those. * **`forSource`** — present only for the `field_maps` action, naming the first unmapped source so you can fill `{sourceId}` without a separate lookup. The full slug-to-action table: | `slug` | `action` | Endpoint | `requiredFields` | | --------------- | ------------------ | ------------------------------------------------------------- | ------------------------------- | | `sources_left` | `add_left_source` | `POST /v1/contexts/{contextId}/sources` | `name`, `type`, `side` | | `sources_right` | `add_right_source` | `POST /v1/contexts/{contextId}/sources` | `name`, `type`, `side` | | `field_maps` | `add_field_map` | `POST /v1/contexts/{contextId}/sources/{sourceId}/field-maps` | `mapping` | | `match_rules` | `add_match_rule` | `POST /v1/contexts/{contextId}/rules` | `priority`, `type`, `config` | | `fee_rules` | `add_fee_rule` | `POST /v1/contexts/{contextId}/fee-rules` | `side`, `feeScheduleId`, `name` | Both source-side actions resolve to the same `createSource` operation — the `side` field is what distinguishes them. ## How to use it during setup *** 1. **Render the checklist.** On each step of the wizard, GET setup-progress and use the counts (`sources`, `fieldMaps`, `matchRules`, `schedules`) to tick off completed items. 2. **Drive the primary button from `next`.** Instead of reimplementing the requirement order client-side, call the operation `next` names; re-read setup-progress afterwards to get the following action. 3. **Gate the "Activate" button.** Enable activation only when `readiness.ready` is `true`; otherwise list `readiness.missing` as the remaining steps. 4. **Show run health.** Once `lastRun` is present, surface its `status` and `completedAt` so operators can confirm the context is producing results. Because the whole state comes from one call, you can poll this endpoint to keep the wizard live without orchestrating separate source, rule, and run reads. ## Response codes *** | Status | Meaning | | ------ | ----------------------- | | `200` | Setup progress returned | | `404` | Context not found | # Extraction reviews Source: https://docs.lerian.studio/en/matcher/imports/matcher-extraction-reviews Review, approve, or reject AI-extracted transaction candidates before they are ingested, and use mapping proposals and job actions to prepare source data. Matcher can extract transaction candidates from documents and propose field mappings using AI — but **AI output is never authoritative**. Nothing is reconciled until a human approves it. This guide covers the human-in-the-loop (HITL) extraction-review queue, AI mapping proposals, and the related job actions. The document-extraction lane is gated by a global kill-switch **and** a per-tenant opt-in. A tenant that has not opted in receives `403` before any document bytes are stored or egressed. ## Enqueue a document for extraction *** Upload a source document (PDF) to run deterministic + AI extraction. The resulting transaction candidates are queued in a review — nothing is reconciled yet. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/extract-document" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/pdf" \ --data-binary @statement.pdf ``` The response (`202 Accepted`) returns the queued review id, the candidate count, and a status that is always `PENDING_REVIEW` on enqueue: ```json theme={null} { "reviewId": "550e8400-e29b-41d4-a716-446655440000", "candidateCount": 12, "status": "PENDING_REVIEW" } ``` ## The review queue *** ### List reviews Cursor-paginated list of extraction reviews for a context, optionally filtered by lifecycle status. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews?status=PENDING_REVIEW&limit=50" \ -H "Authorization: Bearer $TOKEN" ``` Query parameters: `status` (`PENDING_REVIEW`, `APPROVED`, `REJECTED`), `limit` (1–200), and `cursor`. ### Get one review ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}" \ -H "Authorization: Bearer $TOKEN" ``` A review carries its lifecycle, the proposed candidates, provenance, and linkage state: ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "contextId": "550e8400-e29b-41d4-a716-446655440000", "sourceId": "550e8400-e29b-41d4-a716-446655440000", "status": "PENDING_REVIEW", "candidates": [ { "source": "text_layer", "fields": [ { "canonicalKey": "amount", "value": "100.50", "confidence": 0.95, "page": 1 }, { "canonicalKey": "date", "value": "2025-06-01", "confidence": 0.9, "page": 1 } ] } ], "version": 1, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ``` Each candidate declares the lane that produced it: `text_layer` (PDF text, higher trust) or `vision` (OCR/vision model, lower trust). Field values are **verbatim tokens** — money stays a string, never a parsed amount. ## Approve or reject *** ### Approve Approving a `PENDING_REVIEW` review runs the single deterministic handoff into the normal ingestion pipeline (dedup + outbox + match-trigger) and links the resulting job to the review. This is the **only** path from an AI candidate to a reconciled transaction, and it runs only on explicit human approval. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}/approve" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "reviewId": "550e8400-e29b-41d4-a716-446655440000", "ingestionJobId": "550e8400-e29b-41d4-a716-446655440000", "candidateCount": 12 } ``` ### Reject Rejecting discards the candidates — nothing is ingested. The body is optional; an empty body is a valid "reject with no reason". ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}/reject" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reason": "poor scan quality, re-upload" }' ``` The approving/rejecting principal is recorded for audit. ## Mapping proposals *** Before you declare a field map by hand, ask the advisor to inspect a representative sample and propose a **config-only** mapping. It is advisory and side-effect-free: producing a proposal **persists nothing**. You confirm the result through the existing field-map declaration path. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/mapping-proposal" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "sample": "id;value;ccy;posted_at\nA1;10,50;BRL;2025-06-01\n", "format": "csv", "hints": { "locale": "pt-BR", "has_header": "true" } }' ``` The response carries the proposed field map, source dialect, and a per-field breakdown with confidence and rationale: ```json theme={null} { "mapping": { "amount": "value", "external_id": "id" }, "dialect": { "encoding": "utf-8", "delimiter": "semicolon", "decimalStyle": "comma", "dateStyle": "iso" }, "fields": [ { "canonicalKey": "amount", "sourceColumn": "value", "confidence": 0.92, "rationale": "numeric column with comma decimal" } ] } ``` The response never carries parsed values, amounts, or transactions. ## Fetch from an external transport *** Trigger a manual fetch-and-ingest that lists every object matching the supplied transport coordinates (SFTP today) and streams each into the trusted-content ingestion pipeline. The body carries connection coordinates plus an **opaque credential reference — never a secret**. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/fetch" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "kind": "sftp", "host": "sftp.bank.example", "port": 22, "path": "outbound/returns", "glob": "*.ret", "credentialRef": "cred-handle-123", "format": "br/cnab240/febraban-base" }' ``` The response (`202 Accepted`) returns a per-file outcome in fetch order. Per-file intake failures are reported without failing the batch: ```json theme={null} { "files": [ { "name": "statement-2025-06.ret", "ingestionJobId": "550e8400-...", "transactionCount": 42 } ] } ``` A transport-level failure (endpoint unreachable or credential rejected) returns `503`. ## Inspect job errors *** After an import, list the stored per-row parse/normalization errors for a job (capped at 100 per job) to explain failed or partially-failed imports. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/jobs/{jobId}/errors" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "items": [ ... ], "totalErrors": 137, "storedErrors": 100, "errorCap": 100, "truncated": true } ``` `totalErrors` is the uncapped failure total; `truncated` is `true` when it exceeds the stored (capped) set. ## Response codes *** | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------- | | `200` | Review, list, mapping proposal, or job errors returned | | `202` | Document enqueued / fetch accepted | | `400` | Invalid input (empty body, bad status filter, invalid pagination, missing mapping sample) | | `403` | Tenant not opted into document extraction | | `404` | Review or job not found | | `409` | Invalid review state transition | | `422` | No candidates could be extracted / structurally invalid request body | | `503` | Extraction, review, proposal, or fetch not enabled on this deployment | # Formats and templates Source: https://docs.lerian.studio/en/matcher/imports/matcher-import-formats Browse the built-in format catalog Matcher can parse and register per-tenant fixed-width layout templates for operator-specific files. Matcher parses incoming files against a catalog of built-in formats and, when a file does not fit one of them, against per-tenant fixed-width **layout templates** you define. This guide covers browsing the format catalog and managing layout templates. ## The format catalog *** The catalog is the read-only inventory of formats the ingestion engine can parse. It is **global-first and static**: built-in parsers carry no tenant, so the response is identical for every authenticated caller. The catalog is organized as a `region → family → variant` tree, matching the canonical format descriptor axes. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/imports/formats" \ -H "Authorization: Bearer $TOKEN" ``` Each variant carries the canonical namespaced registry key — the identity an upload or source declaration pins: ```json theme={null} { "regions": [ { "region": "BR", "families": [ { "family": "cnab240", "variants": [ { "variant": "febraban-base", "key": "br/cnab240/febraban-base" } ] } ] }, { "region": "XX", "families": [ { "family": "camt", "variants": [ { "variant": "camt053", "key": "xx/camt/camt053" } ] } ] } ] } ``` Regions use the ISO-3166 alpha-2 code (uppercased), or `XX` for region-neutral formats. Even a single-canonical-layout family (for example `camt`) names its layout as the `variant` — `camt053` above. The catalog takes no path, query, or body parameters — the tenant is irrelevant to the built-in catalog. ## Layout templates *** When a file uses an operator- or brand-specific fixed-width layout that no built-in parser covers, register a **layout template**. A template namespaces a positional layout under the `{region, family, variant}` axes and is resolved by the parse path as an additive layout source for your tenant. Every submission and edit runs through a **well-formedness gate** *before* storage. Overrun, overlap, missing-required fields, a zero-field record, or a mis-marked money column all reject with `422` and the template is never stored. Money third rail: a money column **must** declare `kind: "decimal"`. A money field that omits or mis-marks its kind is rejected by the submission gate — it never reaches the parse path. ### Create a template ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/formats/templates" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "region": "BR", "family": "cnab400", "variant": "acme-cobranca", "discriminatorStart": 0, "discriminatorLength": 1, "records": [ { "recordType": "1", "width": 33, "fields": [ { "name": "external_id", "startByte": 1, "length": 10, "kind": "string" }, { "name": "amount", "startByte": 11, "length": 12, "kind": "decimal" }, { "name": "date", "startByte": 23, "length": 10, "kind": "date" } ] } ], "requiredFields": ["external_id", "amount", "date"] }' ``` Field values: * `region` — ISO alpha-2 region (uppercased) or `XX`. * `family` — closed-enum format family the template namespaces under. * `variant` — open operator/brand axis (must be non-blank). * `discriminatorStart` / `discriminatorLength` — the byte range the parser reads to select a record type. * `records[]` — each record type with its fixed `width` (bytes) and ordered positional `fields`. * `fields[].kind` — `string`, `decimal` (money/numeric verbatim token, parsed downstream), or `date`. * `requiredFields` — field names the variant must declare across its record types. A successful create returns `201` with the stored template, including its `formatKey` (for example `br/cnab400/acme-cobranca`), the discriminator, the full positional layout, and `recordWidths`. ### List and get templates ```bash theme={null} # List every active template on the tenant (unpaginated) curl -X GET "https://api.matcher.example.com/v1/imports/formats/templates" \ -H "Authorization: Bearer $TOKEN" # Get one template by id curl -X GET "https://api.matcher.example.com/v1/imports/formats/templates/{templateId}" \ -H "Authorization: Bearer $TOKEN" ``` The list is unpaginated: layout templates are bounded operator config. ### Update and delete a template `PUT` is a **full replace**, not a sparse patch — the byte-range invariants are whole-layout properties. The replacement runs through the same well-formedness gate the create path enforces; a failing layout rejects with `422` and the stored template is left unchanged. ```bash theme={null} curl -X PUT "https://api.matcher.example.com/v1/imports/formats/templates/{templateId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "region": "BR", "family": "cnab400", "variant": "acme-cobranca", "discriminatorStart": 0, "discriminatorLength": 1, "records": [ ... ] }' ``` ```bash theme={null} # Soft-delete, freeing the format/variant key for reuse curl -X DELETE "https://api.matcher.example.com/v1/imports/formats/templates/{templateId}" \ -H "Authorization: Bearer $TOKEN" ``` Delete responds `204`. A missing template returns `404`; a format-key collision with another active template returns `409`. ## Response codes *** | Status | Meaning | | ------ | --------------------------------------------------------------------------------------------------------- | | `200` | Catalog, template list, get, or update returned | | `201` | Template created | | `204` | Template soft-deleted | | `400` | Structurally malformed field/layout | | `404` | Template not found | | `409` | Format/variant key already claimed | | `422` | Layout failed the well-formedness gate (overrun, overlap, missing-required, zero-field, mis-marked money) | | `503` | Format catalog or template store not wired on this deployment | # Aggregator connections Source: https://docs.lerian.studio/en/matcher/integrations/matcher-aggregator-connections Provision Open-Finance data-aggregator connections (Pluggy, Belvo), test them, browse connector types, and mint webhook tokens for inbound pulls. Aggregator connections let Matcher pull transaction data from Open-Finance data aggregators (Pluggy, Belvo). You create a connection with a sealed credential, mint a webhook token bound to it, and the aggregator's webhooks then drive inbound pulls. This guide covers the full lifecycle. Credentials (`clientId`/`secret`) are **inbound-only**: they are sealed before persistence and are never returned in a response, a log, or an error. Every response on this surface is secret-free by construction. The tenant is always resolved from the JWT, never from the request body. ## Create a connection *** ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "vendor": "pluggy", "configName": "pluggy-main", "baseUrl": "https://api.pluggy.ai", "accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "clientId": "...", "secret": "..." }' ``` Field values: * `vendor` — `pluggy` or `belvo`. * `configName` — unique (tenant-scoped) connection identity; a duplicate is a `409`. **The webhook token-mint endpoint binds a token to this name.** * `baseUrl` — vendor API base URL, stored as the connection host. * `accountRef` — opaque vendor account reference (Pluggy `itemId`, Belvo link id) threaded onto the webhook pull. * `clientId` / `secret` — aggregator API credential; sealed and never emitted. A successful create returns `201` with the secret-free connection descriptor: ```json theme={null} { "vendor": "pluggy", "configName": "pluggy-main", "baseUrl": "https://api.pluggy.ai", "accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab" } ``` ## List, get, update, delete *** ```bash theme={null} # List (cursor-paginated, secret-free) curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections?limit=20" \ -H "Authorization: Bearer $TOKEN" # Get by opaque id curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \ -H "Authorization: Bearer $TOKEN" ``` ### Update Edit an existing connection by id so a mistyped `baseUrl` is not permanent. The **vendor is immutable**. The credential is optional: supply **both** `clientId` and `secret` to rotate the sealed credential, or omit **both** to leave the stored secret intact. Supplying exactly one is a `400`. ```bash theme={null} curl -X PUT "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "configName": "pluggy-main", "baseUrl": "https://api.pluggy.ai", "accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab" }' ``` ### Delete ```bash theme={null} curl -X DELETE "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \ -H "Authorization: Bearer $TOKEN" ``` Delete soft-deletes the connection (`204`), freeing its config name for reuse. A non-aggregator connection id returns `404` on any by-id operation — this surface never confirms the existence of a non-aggregator row. ## Test a connection *** Run a live connectivity check for an existing connection using its already-sealed credential, addressed by `(vendor, configName)`. No credential is supplied or returned. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections/test" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "vendor": "pluggy", "configName": "pluggy-main" }' ``` ```json theme={null} { "vendor": "pluggy", "configName": "pluggy-main", "healthy": true } ``` A credentials-don't-work outcome is an **expected** test result surfaced as `"healthy": false` with a `200` — not an error. A missing connection is a `404`. ## Connector types *** List the connector types the engine registry has actually registered for this deployment, each tagged with a backend-derived category (`database` or `rest`). The list reflects the live registry — only connectors registered at boot appear. It drives the connection form's type-select. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/connector-types" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "types": [ { "type": "POSTGRESQL", "category": "database" }, { "type": "STRIPE", "category": "rest" } ] } ``` Aggregator-vendor types (Pluggy/Belvo) are excluded here — they are provisioned through the aggregator-connections surface above. ## Mint a webhook token *** Mint a webhook token bound to an existing aggregator connection. The raw token and its provider-facing webhook URL are returned **once** — only the token's SHA-256 hash is stored. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/webhooks/tokens" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "vendor": "pluggy", "connection_config_name": "pluggy-main" }' ``` ```json theme={null} { "token": "", "webhook_url": "https://api.matcher.example.com/v1/discovery/webhooks/pluggy/", "vendor": "pluggy" } ``` Configure the returned `webhook_url` in the aggregator's dashboard. A missing target connection returns `404`. ## Response codes *** | Status | Meaning | | ------ | -------------------------------------------------------------- | | `200` | Get, list, test, or connector-types returned | | `201` | Connection created / token minted | | `204` | Connection soft-deleted | | `400` | Invalid vendor, partial credential pair, or invalid pagination | | `401` | Tenant could not be resolved | | `404` | Connection not found (or not an aggregator) | | `409` | Connection with that config name already exists | # Discovery Source: https://docs.lerian.studio/en/matcher/integrations/matcher-discovery Use Discovery and Fetcher to detect external data sources, inspect their schemas, and pull transactions into Matcher automatically. Discovery automates data source detection and extraction through Fetcher. Instead of manually uploading files, Discovery connects to external systems, identifies available data, and extracts transactions directly into Matcher. ## What Discovery solves *** Manual file uploads create friction at every step. Teams export files, transfer them, monitor for failures, and re-upload when something goes wrong. This process is time-consuming, error-prone, and breaks when data volume grows. Discovery replaces the manual pipeline. It connects to external systems through Fetcher, detects available data sources automatically, and pulls transactions into Matcher on demand. When a new data source appears — a new bank connection, a new payment processor — Discovery finds it without reconfiguration. ## How Discovery works *** Discovery runs on Fetcher's extraction engine, which Matcher hosts in-process; Fetcher is not a remote service. The embedded engine manages connections to external databases and runs extractions locally. Discovery exposes those connections and coordinates the extraction process, handing results directly to Ingestion. The workflow has seven steps: 1. **Check status** — Confirm Discovery and its embedded engine are available. 2. **Browse connections** — See all data sources the embedded engine has access to. 3. **Inspect a connection** — Review the schema to understand what fields are available. 4. **Test a connection** — Validate the connection before committing to an extraction. 5. **Create an extraction** — Request that Matcher pull data from a specific source. 6. **Monitor progress** — Track extraction status as data flows in. 7. **Refresh connections** — Rescan when new data sources are added. ## Discovery workflow *** ### Check Discovery status Verify that Discovery and the embedded Fetcher engine are operational before starting. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/status" \ -H "Authorization: Bearer $TOKEN" ``` API Reference: [Get Discovery status](/en/reference/matcher/discovery-status) ### Browse connections List all data sources available through the embedded Fetcher engine. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/connections" \ -H "Authorization: Bearer $TOKEN" ``` The response lists each connection with its name, type (database, API, file store), and current status. API Reference: [List connections](/en/reference/matcher/list-discovery-connections) ### Get a connection Retrieve a single discovered Fetcher connection by its internal identifier: ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/connections/{connectionId}" \ -H "Authorization: Bearer $TOKEN" ``` `GET /v1/discovery/connections/{connectionId}` returns the full `ConnectionResponse` (name, type, status, and metadata) for one connection — useful when you already hold a `connectionId` (for example from a source binding's query rail) and want its current details without listing every connection. API Reference: [Get discovery connection](/en/reference/matcher/retrieve-discovery-connection) ### Inspect a connection Review the schema of a specific connection to understand what data fields are available before extracting. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/connections/{connectionId}/schema" \ -H "Authorization: Bearer $TOKEN" ``` Use schema inspection to confirm that required fields — transaction IDs, amounts, dates, references — exist before building field mappings. API Reference: [Get connection schema](/en/reference/matcher/get-connection-schema) ### Test a connection Validate that Matcher can reach and read from a connection before creating an extraction. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/connections/{connectionId}/test" \ -H "Authorization: Bearer $TOKEN" ``` A successful test confirms connectivity and read access. Always test before creating an extraction — especially for new or recently modified connections. API Reference: [Test connection](/en/reference/matcher/test-discovery-connection) ### Create an extraction Request that Matcher pull transaction data from a specific connection into the current context. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/connections/{connectionId}/extractions" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tables": { "transactions": {} }, "startDate": "2026-06-01", "endDate": "2026-06-30" }' ``` The response returns an extraction ID. Use it to monitor progress. API Reference: [Create extraction](/en/reference/matcher/create-extraction) ### Monitor extraction progress Track the status of an active extraction by polling its status with `GET`. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/extractions/{extractionId}" \ -H "Authorization: Bearer $TOKEN" ``` Extraction status transitions from `PENDING` → `SUBMITTED` → `EXTRACTING` → `COMPLETE` (or `FAILED`/`CANCELLED`). The response carries the extraction `status`, an `errorMessage` when it failed, and the linked `ingestionJobId` once the extraction bridges into ingestion. API Reference: [Get extraction](/en/reference/matcher/retrieve-extraction) ### Refresh available connections When new data sources are registered with the embedded engine, trigger a refresh so Discovery picks them up. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/refresh" \ -H "Authorization: Bearer $TOKEN" ``` API Reference: [Refresh connections](/en/reference/matcher/refresh-discovery) ### List connector types List the connector (datasource) types the engine registry has registered for this deployment. Each entry carries a backend-derived `category` (`database` or `rest`). The registry is live—only connectors registered at boot appear. Aggregator vendors (Pluggy/Belvo) are excluded; provision those through the aggregator-connections surface below. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/connector-types" \ -H "Authorization: Bearer $TOKEN" ``` #### Response ```json theme={null} { "types": [ { "type": "POSTGRESQL", "category": "database" }, { "type": "MYSQL", "category": "database" } ] } ``` ## Aggregator connections (Open Finance) *** Open-Finance data-aggregator connections (Pluggy or Belvo) let Matcher pull transactions from bank aggregators. Credential material (`clientId`/`secret`) is **sealed on write and never returned**—every read is secret-free by construction. ### Create an aggregator connection ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "vendor": "pluggy", "configName": "pluggy-main", "baseUrl": "https://api.pluggy.ai", "accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "clientId": "...", "secret": "..." }' ``` All six fields are required. `vendor` is one of `pluggy` or `belvo`. `configName` is the tenant-scoped name the webhook token-mint endpoint binds to. Returns **201** with a secret-free connection. #### Response ```json theme={null} { "vendor": "pluggy", "configName": "pluggy-main", "baseUrl": "https://api.pluggy.ai", "accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab" } ``` ### List, get, update, and delete ```bash theme={null} # List (cursor-paginated, secret-free) curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections" \ -H "Authorization: Bearer $TOKEN" # Get one by id curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \ -H "Authorization: Bearer $TOKEN" # Update (PUT). vendor is immutable. Supply clientId+secret together to rotate # the sealed credential, or omit both to keep the stored secret intact. curl -X PUT "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "configName": "pluggy-main", "baseUrl": "https://api.pluggy.ai", "accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab" }' # Delete (soft-delete; frees the config name for reuse). Returns 204. curl -X DELETE "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \ -H "Authorization: Bearer $TOKEN" ``` ### Test an aggregator connection Run a live connectivity check against an existing connection's already-sealed credential, addressed by `(vendor, configName)`. No credential is supplied or returned—the result is a secret-free boolean health. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections/test" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "vendor": "pluggy", "configName": "pluggy-main" }' ``` #### Response ```json theme={null} { "vendor": "pluggy", "configName": "pluggy-main", "healthy": true } ``` ## Aggregator webhook tokens *** Aggregators push data change signals to Matcher via webhooks. Mint an opaque token bound to an aggregator connection, then configure the returned URL in the vendor dashboard. ### Mint a webhook token The raw token and its provider-facing URL are returned **exactly once**—only the token's SHA-256 hash is stored. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/webhooks/tokens" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "vendor": "pluggy", "connection_config_name": "pluggy-main" }' ``` #### Response ```json theme={null} { "vendor": "pluggy", "token": "", "webhook_url": "https://api.matcher.example.com/v1/discovery/webhooks/pluggy/" } ``` ### Receiving webhooks The vendor calls `POST /v1/discovery/webhooks/{provider}/{webhookToken}` (no operator JWT). It is authenticated by the opaque path token **plus** a per-provider source check: a valid HMAC-SHA256 of the raw body in the `X-Webhook-Signature` header, **or** membership in the provider's source-IP allowlist. Both layers fail closed. A valid first delivery returns **202 Accepted** and the signaled data is pulled asynchronously into the ingestion pipeline; a replay of an already-processed event returns **200 OK**. ## Best practices *** A failed extraction mid-run is harder to recover from than a failed test. Test every connection before creating an extraction — especially when connecting to a new source or after a credential rotation. Field names vary across systems. A bank might call the transaction date `value_date` while your ledger uses `posting_date`. Check the schema before configuring field mappings to avoid silent mismatches. Large extractions take time. Don't assume completion — poll the extraction status and confirm the record count before starting a match run. Starting a run on incomplete data generates incorrect exceptions. Discovery doesn't scan for new connections automatically. When a new payment processor is added or a new database is registered with the embedded engine, trigger a refresh. Otherwise, Discovery won't show the new source. Use date range parameters to extract only the data relevant to the current reconciliation period. Extracting unbounded data increases processing time and may pull records that belong to already-closed contexts. ## Next steps *** Configure the external data sources that Discovery connects to. Map fields from extracted data to Matcher's transaction model. Full API reference for Discovery endpoints. # External sources Source: https://docs.lerian.studio/en/matcher/integrations/matcher-external-sources Connect banks, payment gateways like Stripe and Adyen, ERPs such as SAP or Oracle, and card networks to Matcher via LEDGER, BANK, GATEWAY, or CUSTOM types. External sources provide transaction data from systems outside your organization. This guide covers how to connect banks, payment gateways, and other external systems to Matcher. ## Supported source types *** Matcher supports five source types. Each represents a category of data origin: | Type | Description | Typical use | | --------- | --------------------- | --------------------------------------------- | | `LEDGER` | Internal ledger | Internal accounting systems (including Midaz) | | `BANK` | Bank statement feed | External bank feeds | | `GATEWAY` | Payment gateway | Payment processors (Stripe, Adyen, PayPal) | | `CUSTOM` | Bespoke feed | ERPs, card networks, or any other data source | | `FETCHER` | Discovery-engine pull | Aggregator connections pulled automatically | ## Ingestion methods *** Transaction data reaches Matcher through several paths: | Method | Use Case | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | **File upload** | Manual uploads (CSV, JSON, XML, OFX, camt.053, CNAB, acquirer EDIs) | | **Transport fetch** | Matcher pulls files from a configured transport (for example SFTP) and ingests them | | **Discovery extraction** | [Discovery](/en/matcher/integrations/matcher-discovery) extracts data from Fetcher connections into ingestion | | **Aggregator webhooks** | [Open-Finance aggregators](/en/matcher/integrations/matcher-aggregator-connections) signal new data, pulled asynchronously | ## File-based ingestion *** The most common method for bank statements and ERP exports. ### Manual upload Use the file upload endpoint to import transaction files manually. API Reference: [Upload transaction file](/en/reference/matcher/upload-transaction-file) ## Bank connections *** ### Standard Bank format Most banks provide statements in a format Matcher parses natively — CSV, OFX, camt.053, or the Brazilian CNAB layouts: ```json theme={null} { "name": "Chase Business Account", "type": "BANK", "config": { "bank_name": "Chase", "account_number": "****1234", "currency": "USD", "statement_format": "CSV", "timezone": "America/New_York" } } ``` The `config` object is free-form descriptive metadata — Matcher stores it but does not interpret keys like `bank_name` or `statement_format`. Parsing behavior is driven by the declared format dialect and the pinned configuration keys (error-rate policy, duplicate policy, `blank_external_id`, and camt.053 options), not by these labels. API Reference: [Create source](/en/reference/matcher/create-source) ## ERP and custom connections *** Use the `CUSTOM` source type for ERP systems (SAP, Oracle, NetSuite, etc.) and any other data source that doesn't fit the `BANK`, `LEDGER`, or `GATEWAY` categories. ### Example: ERP source ```json theme={null} { "name": "SAP S/4HANA", "type": "CUSTOM", "config": { "erp_type": "SAP", "company_codes": ["1000", "2000"] } } ``` Export transaction data from your ERP and upload it through Matcher's file upload endpoint. Use [field mapping](/en/matcher/configuration/matcher-field-mapping) to translate ERP-specific fields into Matcher's canonical format. ## Payment processor connections *** ### Stripe ```json theme={null} { "name": "Stripe Payments", "type": "GATEWAY", "config": { "provider": "stripe" } } ``` ### Adyen ```json theme={null} { "name": "Adyen Settlements", "type": "GATEWAY", "config": { "provider": "adyen", "merchant_account": "CompanyECOM" } } ``` Export settlement reports from your payment processor and upload them through Matcher's file upload endpoint. ### Card networks For card network settlement files (Visa, Mastercard, Elo), use the `CUSTOM` source type: ```json theme={null} { "name": "Visa Settlement", "type": "CUSTOM", "config": { "network": "VISA", "file_format": "TC33" } } ``` ## Connection security *** ### Credential storage All credentials should be stored securely in an encrypted vault and referenced by ID in source configurations. ### Ip allowlisting Configure IP allowlisting at the infrastructure level (load balancer, API gateway, or firewall) to restrict which IPs can push data to Matcher. Source entities do not have a `settings.security` configuration. Manage IP restrictions outside the application. ### Webhook signatures Matcher signs outbound webhook payloads with HMAC-SHA256. For inbound data, verify signatures at the infrastructure level before data reaches Matcher. Source entities do not have a `settings.webhook` configuration. ## Data format requirements *** ### Required fields Every transaction must include: Field maps use a **closed** canonical vocabulary: the mapping *keys* are fixed, and the *values* name the source's raw column. These canonical keys are required: | Canonical key | Type | Description | | ------------- | ------- | ------------------------------------ | | `external_id` | String | Source-system transaction identifier | | `amount` | Decimal | Transaction amount | | `currency` | String | ISO 4217 code | | `date` | Date | Transaction date | ### Optional fields | Canonical key | Type | Description | | -------------- | ------- | ----------------------------------------------------------------------- | | `description` | String | Reference/description text (feeds the transaction's description column) | | `fee_amount` | Decimal | Fee slot: source column carrying the fee amount | | `fee_currency` | String | Fee slot: source column carrying the fee currency | No other keys are accepted—keys outside this vocabulary are rejected. ### Field mapping Manage field maps with the dedicated endpoint (not the source's `config` object). The request body is a single `mapping` object of `{ canonicalKey: sourceColumnName }` pairs: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{sourceId}/field-maps" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mapping": { "external_id": "TXN_ID", "amount": "trans_amount", "currency": "CCY", "date": "POST_DATE", "description": "memo", "fee_amount": "mdr_fee", "fee_currency": "fee_ccy" } }' ``` Refer to [Field Mapping](/en/matcher/configuration/matcher-field-mapping) for details. ## Best practices *** Check that uploaded files contain columns for the required canonical fields (external\_id, amount, currency, date) before uploading. This prevents ingestion errors. Standardize on a single format (CSV, JSON, or XML) per source to simplify field mapping and reduce errors. Store all API keys and passwords in the vault. Never include credentials in configuration payloads. Validate field mapping and data quality with sample files before uploading production data. ## Next steps *** Configure how source fields map to Matcher. Manual file upload procedures. # Matcher and Midaz Source: https://docs.lerian.studio/en/matcher/integrations/matcher-midaz-integration See how Matcher works alongside Midaz Ledger — two independent services that share data through exports and imports rather than a live API connection. **There's no direct Matcher-to-Midaz connector.** Matcher is a stand-alone reconciliation service. It doesn't open a live connection to Midaz, and it has no Midaz-specific configuration. This page explains how the two products work together conceptually. Matcher and [Midaz](/en/midaz) are separate Lerian services with distinct jobs: * **Midaz** is the ledger — the system of record for balances and postings. * **Matcher** is the reconciliation engine — it compares two independent datasets and reports what agrees, what doesn't, and why. They are **complementary, not coupled**. You get value from Midaz-plus-Matcher by reconciling ledger data *against* an external dataset (a bank statement, a gateway settlement report), which is exactly the problem Matcher exists to solve. The data moves between them by **export and import**, not through a real-time link. ## How they fit together *** A typical Lerian reconciliation flow: 1. **Midaz records postings.** Midaz books transactions in the ledger as usual. 2. **You export the ledger data** for the period you want to reconcile (for example, a day's cash-account entries). 3. **You import that export into Matcher** as one side of a context — a source of type `LEDGER`. 4. **You import the counterparty data** — the bank statement or gateway report — as the other side. 5. **Matcher matches the two sides** using your match rules and surfaces exceptions for review. Midaz is the origin of one dataset; the bank or gateway is the origin of the other. Matcher never talks to Midaz directly — it works from the exported data you feed it. ## Modeling Midaz as a source *** Inside a Matcher context, ledger data from Midaz is represented by a source with type `LEDGER`. `LEDGER` is simply Matcher's category for "internal ledger / accounting system" data; it's not a Midaz driver. ```json theme={null} { "name": "Midaz Cash Ledger", "type": "LEDGER", "side": "LEFT", "config": {} } ``` API Reference: [Create source](/en/reference/matcher/create-source) The other side of the context is the external dataset you're reconciling against — usually a `BANK` or `GATEWAY` source on the opposite `side`. Source types available in Matcher: | Type | Meaning | | --------- | -------------------------------------------------------- | | `LEDGER` | Internal ledger / accounting system (e.g. Midaz exports) | | `BANK` | Bank statement feed | | `GATEWAY` | Payment gateway report | | `CUSTOM` | Any other bespoke feed | | `FETCHER` | Discovery-engine pull | ## Getting ledger data into Matcher *** Matcher ingests the data you export from Midaz the same way it ingests any other source — there's no Midaz-specific transport. * **File upload.** Export ledger entries (CSV/JSON) and upload the file to the source. This is the most common path. * **Discovery-engine query rail.** For sources bound on the `query` rail, Matcher pulls rows through a discovery-engine connection instead of a file. This is a generic ingestion rail, not a Midaz connector. Either way, you then define a [field map](/en/matcher/configuration/matcher-field-mapping) that renames the exported columns to Matcher's canonical fields (`external_id`, `amount`, `currency`, `date`, and optional `description`, `fee_amount`, `fee_currency`). API Reference: [Upload transaction file](/en/reference/matcher/upload-transaction-file) ### Custom field mapping Most ledger columns map straight across. Field-map lookups are **flat** — a mapping value must name a column that exists at the top level of the exported row, so a value that lives inside transaction metadata must be exported as its own flat column before Matcher can match on it. For example, Brazil's Pix `endToEndId` is stored in Midaz transaction metadata. To reconcile on it, export it as a flat column (here `endToEndId`) and map `external_id` to that column — `external_id` is the reference the matching engine compares across sources, so both sides of the context must map it to a value they share (the `description` key is display-only and never feeds matching): ```json theme={null} { "mapping": { "external_id": "endToEndId", "amount": "amount", "currency": "asset_code", "date": "created_at" } } ``` Every field map must declare all four required canonical keys (`external_id`, `amount`, `currency`, `date`) explicitly — there is no automatic mapping. See [Field mapping](/en/matcher/configuration/matcher-field-mapping) for the full canonical vocabulary. ## Shared platform foundations *** Although there's no live integration, Matcher is built for the same platform as Midaz and mirrors several of its patterns: * **Authentication.** Matcher uses the shared Lerian auth stack, so the same identity provider and tokens used across the platform apply. * **Multi-tenancy.** Matcher follows a pool-per-tenant isolation model (a dedicated database per tenant) in line with the rest of the stack, keeping each tenant's data separate. These are platform-level commonalities, not a Matcher-to-Midaz data channel. ## What this integration is *not* *** To avoid confusion with earlier drafts of this page: * Matcher has **no** `MIDAZ_BASE_URL` or `MIDAZ_GRPC_ADDRESS` setting. * There is **no** real-time sync mode and **no** `account_filter` for a Midaz source. * Matcher **doesn't** subscribe to Midaz events or open a gRPC/HTTP connection to Midaz. Reconciling Midaz data means exporting it and importing it into Matcher like any other source. ## Best practices *** Export only the ledger accounts and period you actually need to reconcile. Smaller, targeted exports keep contexts fast and matching precise. If you reconcile daily, export daily. Match the export schedule to the counterparty data (bank statement) availability. Field maps rename columns but do not transform values. Produce exports whose amounts, dates, and currency codes are already in the shapes your match rules expect. Include stable references (invoice numbers, external IDs) as columns in the export so match rules can key on them for higher match rates. ## Next steps *** Connect the bank or gateway side of the reconciliation. Map exported ledger columns to Matcher's canonical fields. # Webhooks and callbacks Source: https://docs.lerian.studio/en/matcher/integrations/matcher-webhooks-callbacks Emit Matcher lifecycle events to external tools through outbound webhooks, and receive resolution callbacks so JIRA or ServiceNow keep exceptions in sync. Webhooks enable real-time communication between Matcher and external systems. This guide covers outbound event notifications and inbound resolution callbacks. ## Overview *** Matcher supports bidirectional webhook communication, keeping your operational tools in sync with every reconciliation event in real time. This reduces manual intervention, helps maintain SLA compliance, and ensures a continuous audit trail across all connected systems. * **Outbound webhooks**: exception routing dispatches exceptions to external targets — JIRA, ServiceNow, or an HTTP webhook endpoint you configure * **Inbound callbacks**: External systems notify Matcher when actions are taken When an exception is routed to a webhook target, Matcher delivers a signed HTTP request to your endpoint. External systems like JIRA or ServiceNow can then send callbacks to update exception status or close items automatically. This two-way flow keeps your tools in sync without manual intervention. Beyond exception dispatch, Matcher publishes its full lifecycle event catalog on the platform's streaming backbone — those events are consumed as a stream, not delivered as HTTP webhooks. Matcher Webhooks Callbacks ## Outbound events *** Matcher emits events when significant actions occur in the reconciliation process. The catalog below is published on the streaming backbone; exception events additionally reach HTTP webhook endpoints through [exception routing](/en/matcher/configuration/matcher-exception-routing). ### Available events Matcher's event catalog is defined centrally. The most commonly consumed events are grouped by domain below. **Configuration** | Event | Trigger | Typical Use | | -------------------------------- | ---------------------------------- | ---------------------- | | `reconciliation_context.created` | Reconciliation context created | Provisioning sync | | `reconciliation_context.updated` | Context metadata or status changed | Config change tracking | | `reconciliation_context.deleted` | Context deleted | Downstream teardown | | `reconciliation_source.created` | Source created within a context | Source onboarding | | `match_rule.created` | Match rule created | Rule change audit | | `match_rule.reordered` | Rule priorities reordered | Rule change audit | **Discovery (Fetcher)** | Event | Trigger | Typical Use | | ---------------------------------- | ------------------------------------------- | --------------------- | | `fetcher_connection.synced` | Connection and schema snapshot synchronized | Discovery monitoring | | `fetcher_connection.unreachable` | Connection marked unreachable | Connectivity alerting | | `extraction_request.created` | Extraction request created | Extraction monitoring | | `extraction_request.submitted` | Extraction accepted by Fetcher | Extraction monitoring | | `extraction_request.completed` | Extraction completed with artifact | Data readiness | | `extraction_request.failed` | Extraction failed | Error alerting | | `extraction_request.cancelled` | Extraction cancelled | Pipeline monitoring | | `extraction_request.bridged` | Extraction linked to an ingestion job | Pipeline monitoring | | `extraction_request.bridge_failed` | Bridge to ingestion failed | Error alerting | **Ingestion** | Event | Trigger | Typical Use | | --------------------- | ------------------------------------ | ------------------------ | | `ingestion.completed` | File import finished | Data pipeline monitoring | | `ingestion.failed` | File import failed | Error alerting | | `transaction.ignored` | Unmatched transaction marked ignored | Audit trail | **Matching** | Event | Trigger | Typical Use | | ---------------------------- | ------------------------------------ | ------------------------- | | `match_run.completed` | Match job finished | Job monitoring, reporting | | `match_run.failed` | Match job failed | Error alerting | | `match_group.confirmed` | Match group confirmed | Downstream updates | | `match_group.unmatched` | Confirmed match reverted | Correction tracking | | `transaction.matched` | Transaction marked matched | Audit logging | | `transaction.pending_review` | Non-automatic candidate needs review | Review queue triggers | | `fee_variance.created` | Fee variance detected | Fee investigation | **Exceptions and disputes** | Event | Trigger | Typical Use | | --------------------------------- | --------------------------------------- | ------------------ | | `exception.assigned` | Exception assigned to an owner | User notification | | `exception.resolved` | Exception resolved | Status sync | | `exception.dispatched` | Exception sent to an external target | Ticket creation | | `exception.callback_processed` | External callback processed | Status sync | | `exception.force_match_resolved` | Exception resolved via force match | Approval workflows | | `exception.adjust_entry_resolved` | Exception resolved via adjustment entry | Audit trail | | `exception_comment.added` | Comment added to an exception | Collaboration sync | | `exception_comment.deleted` | Exception comment deleted | Collaboration sync | | `dispute.opened` | Dispute opened for an exception | Dispute tracking | | `dispute.won` | Dispute closed as won | Status sync | | `dispute.lost` | Dispute closed as lost | Status sync | | `evidence.submitted` | Evidence submitted to a dispute | Dispute tracking | **Governance and reporting** | Event | Trigger | Typical Use | | -------------------------- | ------------------------------ | --------------------- | | `audit_log.created` | Audit log entry appended | Compliance monitoring | | `archive_metadata.created` | Archive lifecycle started | Archival monitoring | | `archive.uploaded` | Archive object uploaded | Archival monitoring | | `archive.completed` | Archive verified and completed | Archival monitoring | | `actor.pseudonymized` | Actor mapping pseudonymized | Compliance monitoring | | `export_job.created` | Export job queued | Export monitoring | | `export_job.succeeded` | Export job completed | Download readiness | | `export_job.failed` | Export job failed | Error alerting | | `export_job.expired` | Export artifact expired | Export lifecycle | ### Webhook delivery payload Exception dispatches to a webhook target carry a consistent payload — `eventId`, `eventType`, `timestamp`, the exception snapshot under `data`, and routing/tracing information under `metadata`: ```json theme={null} { "eventId": "0e8f1c2a-5b6d-4f3e-9a7b-1c2d3e4f5a6b", "eventType": "exception.dispatched", "timestamp": "2026-01-20T10:30:00Z", "data": { "exceptionId": "9b2f4e6a-1c3d-4e5f-8a9b-0c1d2e3f4a5b", "transactionId": "7a1b3c5d-9e8f-4a2b-b6c7-d8e9f0a1b2c3", "severity": "HIGH", "status": "PENDING", "amount": "15000.00", "currency": "USD", "reason": "No matching ledger entry found", "sourceType": "LEFT", "createdAt": "2026-01-20T10:29:15Z", "dueAt": "2026-01-23T10:29:15Z" }, "metadata": { "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "target": "WEBHOOK", "queue": "ops-review", "ruleName": "high-value-unmatched", "assignee": "ops-team" } } ``` `data.dueAt` and the `metadata` fields `traceId`, `queue`, `ruleName`, and `assignee` are omitted when not set. Streaming catalog events (the tables above) follow their own per-event schemas on the event stream and are not delivered in this HTTP shape. ## Inbound callbacks *** External systems send callbacks to Matcher to update exception status after processing. The callback endpoint accepts status updates, resolution notes, and assignee changes from any external system. ### Process a callback The callback endpoint is authenticated by the `X-Callback-Token` header — an opaque token minted through the [callback credentials](#callback-credentials) surface — not by an operator JWT. All fields shown below are required; `dueAt` and `updatedAt` accept `null`, and `payload` can be an empty object: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/callback" \ -H "X-Callback-Token: ***" \ -H "X-Idempotency-Key: callback-jira-1234" \ -H "Content-Type: application/json" \ -d '{ "callbackType": "status_update", "externalSystem": "JIRA", "externalIssueId": "RECON-1234", "status": "RESOLVED", "resolutionNotes": "Verified: amount difference is expected bank wire fee", "assignee": "john.doe@company.com", "dueAt": null, "updatedAt": "2026-01-20T14:30:00Z", "payload": {} }' ``` The `externalSystem` field identifies the external system that processed the exception. Common values include `"JIRA"`, `"SERVICENOW"`, or `"WEBHOOK"`, but callbacks can report any system identifier. Omitting any of the nine required fields returns a `422`. #### Response ```json theme={null} { "status": "accepted" } ``` API Reference: [Process callback](/en/reference/matcher/process-exception-callback) When Matcher processes a callback, it updates the exception status and records the resolution in the audit trail. Use the `X-Idempotency-Key` header to prevent duplicate processing. ### Automatic retry for failed callbacks If a previous callback for the same idempotency key failed during processing, Matcher automatically attempts to reacquire the idempotency lock and reprocess the callback. This means you don't need to generate a new idempotency key when retrying a failed callback — simply resend the same request and Matcher handles the recovery. The retry behavior applies only to callbacks that were marked as `failed` internally. Callbacks that completed successfully are still deduplicated as expected. ## Callback credentials *** Inbound callbacks are authenticated with an opaque bearer token that the external system sends in the `X-Callback-Token` header. These **callback credentials** are minted, listed, rotated, and revoked through a dedicated CRUD surface under `/v1/exceptions/callbacks/credentials`. Each credential is bound to the caller's tenant, and only the token's SHA-256 hash is stored server-side — the raw token is returned **exactly once** at mint/rotate time. | Action | Method & path | Notes | | ----------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Mint credential | `POST /v1/exceptions/callbacks/credentials` | Creates a credential and returns the raw token once (`201`). | | List credentials | `GET /v1/exceptions/callbacks/credentials` | Lists credential metadata for the tenant (never the raw tokens). | | Rotate credential | `POST /v1/exceptions/callbacks/credentials/{credentialId}/rotate` | Atomically supersedes a live credential with a freshly minted one (same label) and returns the new raw token once; the old one is revoked in the same transaction. | | Revoke credential | `DELETE /v1/exceptions/callbacks/credentials/{credentialId}` | Terminally revokes a credential (`204`); append-only audited. | API Reference: * [Mint callback credential](/en/reference/matcher/mint-callback-credential) * [List callback credentials](/en/reference/matcher/list-callback-credentials) * [Rotate callback credential](/en/reference/matcher/rotate-callback-credential) * [Revoke callback credential](/en/reference/matcher/revoke-callback-credential) ### Mint a credential The request body is optional; provide `externalSystem` as an operator-legible label for the system this token authenticates. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/callbacks/credentials" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "externalSystem": "stripe" }' ``` The `201` response (`CredentialSecretResponse`) returns: | Field | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | | `token` | Raw bearer token, surfaced **once**. Configure it as the `X-Callback-Token` header value in the external system. | | `credentialId` | Surrogate id of the minted credential (used for rotate/revoke). | | `createdAt` | Mint time (RFC 3339, UTC). | | `externalSystem` | The label echoed back for confirmation. | | `webhookUrlHint` | Informational inbound-callback URL shape to configure externally; the `{exceptionId}` placeholder is filled per callback. | The raw `token` is shown only in the mint and rotate responses. Store it securely on receipt — it cannot be retrieved again. If it is lost or leaked, rotate or revoke the credential. ### Rotate a credential ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/callbacks/credentials/{credentialId}/rotate" \ -H "Authorization: Bearer $TOKEN" ``` Rotation returns a new `CredentialSecretResponse` (new raw token) and revokes the previous credential atomically, so external callers experience no gap when you swap the token. ### Revoke a credential ```bash cURL theme={null} curl -X DELETE "https://api.matcher.example.com/v1/exceptions/callbacks/credentials/{credentialId}" \ -H "Authorization: Bearer $TOKEN" ``` Revocation is terminal: the credential can no longer authenticate inbound callbacks. ## Webhook security *** ### Signature verification When a webhook shared secret is configured, Matcher signs each delivery with an HMAC-SHA256 over the **raw request body** and sends it in the `X-Signature-256` header, formatted as `sha256=`: ``` X-Signature-256: sha256=abc123... ``` Each delivery also carries an `X-Idempotency-Key` header so receivers can deduplicate retries. **Verification process:** 1. Compute HMAC-SHA256 of the raw request body using the webhook shared secret 2. Prefix the hex digest with `sha256=` 3. Compare (constant-time) with the `X-Signature-256` header **Example (Node.js):** ```javascript theme={null} const crypto = require('crypto'); function verifyWebhook(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return `sha256=${expectedSignature}` === signature; } ``` **Example (Python):** ```python theme={null} import hmac import hashlib def verify_webhook(payload, signature, secret): expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return f"sha256={expected}" == signature ``` ### Network posture Matcher deploys in your own infrastructure, so webhook deliveries originate from your deployment's egress — there is no fixed Lerian IP range to allowlist. Serve webhook endpoints over HTTPS with a valid certificate. As an SSRF guard, Matcher refuses to deliver to private or loopback IP addresses unless the deployment explicitly allows them (development only). ## Retry logic *** Failed webhook deliveries are retried with exponential backoff. ### Default retry policy A failed delivery is retried up to **3 times** by default. Delays follow exponential backoff from a **1-second** base, with jitter added to spread retries — so the exact spacing varies from attempt to attempt rather than following a fixed ladder. ### Retry conditions Retries occur for: * HTTP 429 responses * HTTP 5xx responses * Transport errors (connection failures, timeouts) No retry for: * Other HTTP 4xx responses ## Best practices *** Always verify the HMAC signature before processing webhook payloads. This prevents spoofed requests. Return a 2xx response within 5 seconds. Process the event asynchronously if needed. Deliveries may arrive more than once. Deduplicate on the `X-Idempotency-Key` header or the payload's `eventId`. Set up alerts for webhook failure rates. Investigate persistent failures promptly. ## Next steps *** Configure how exceptions trigger webhook events. Set up data sources that can push via webhooks. # Architecture Source: https://docs.lerian.studio/en/matcher/matcher-architecture Explore Matcher's modular monolith built on DDD, hexagonal architecture, and CQRS, with seven bounded contexts that evolve independently. Matcher is built as a **modular monolith** using Domain-Driven Design (DDD) and hexagonal architecture. CQRS separates commands (writes) from queries (reads). This keeps operations simple while maintaining clear boundaries. Each module can evolve independently without the complexity of microservices. ## Architecture overview *** Matcher Architecture ## Bounded contexts *** Matcher has seven modules. Each owns its data and exposes clean interfaces to the others. * **Configuration**: What you're reconciling (contexts, sources, field maps, rules) * **Discovery**: External data source connections, schema detection, and extraction orchestration with the embedded Fetcher engine * **Ingestion**: Getting data in (parsing, validation, normalization) * **Matching**: The engine (rule execution, confidence scoring) * **Exception**: Handling unmatched items (workflow, routing, resolution) * **Governance**: Audit trails (immutable logs for compliance) * **Reporting**: Visibility (reports, exports, dashboards) ### Configuration Defines **what** you're reconciling and **how**. **Handles:** * Contexts (what's being reconciled) * Sources (where data comes from) * Field maps (translating external fields) * Rules (how to match) **Key models:** * `ReconciliationContext`: The reconciliation scope * `ReconciliationSource`: Source configuration * `FieldMap`: Field translation rules * `MatchRule`: Matching logic ### Discovery The Discovery bounded context manages external data source connectivity, schema detection, and extraction orchestration with Fetcher's embedded engine. Matcher hosts that engine in-process; Fetcher is not a remote service. **Responsibilities:** * Manage external data source connections * Detect and cache source schemas * Run in-process extractions and hand results directly to Ingestion * Track connection and extraction lifecycles **Key entities:** * `FetcherConnection`: External source connection managed locally by the embedded engine * `ExtractionRequest`: Tracks an extraction lifecycle run by the embedded engine See [Discovery](/en/matcher/integrations/matcher-discovery) for how Discovery connects to external databases with the embedded Fetcher engine. ### Ingestion The Ingestion bounded context handles data intake and normalization. **Responsibilities:** * Parse uploaded files (CSV, JSON, XML) * Validate incoming data against configured schemas * Normalize external data into a canonical representation * Detect and handle duplicate records * Emit domain events when ingestion completes **Key entities:** * `IngestionJob`: Tracks ingestion lifecycle and status * `Transaction`: Normalized canonical transaction record **Events published:** * `ingestion.completed`: Indicates data readiness for matching ### Matching The Matching bounded context contains the reconciliation engine. **Responsibilities:** * Load applicable rules for a reconciliation context * Execute matching strategies (exact, tolerance, date-based) * Calculate confidence scores * Create match groups and allocate transactions * Identify unmatched transactions **Key entities:** * `MatchRun`: Execution of a matching job * `MatchGroup`: Group of reconciled transactions * `MatchItem`: Individual transaction allocation **Events published:** * `match_group.confirmed`: A match group has been finalized * `match_group.unmatched`: A previously confirmed match was reverted * `transaction.pending_review`: A non-automatic candidate needs review ### Exception management The Exception bounded context manages unresolved transactions. **Responsibilities:** * Classify exceptions by severity * Route exceptions to internal teams or external systems * Support manual overrides and adjustments * Track resolution status and SLAs * Integrate with external workflow tools **Key entities:** * `Exception`: An unresolved transaction * `Resolution`: Outcome of exception handling * `RoutingRule`: Routing and escalation logic **Integrations:** * JIRA for issue tracking * Webhooks for custom workflows ServiceNow is an accepted routing target, but its HTTP connector is not implemented. ### Governance The Governance bounded context preserves reconciliation traceability. **Responsibilities:** * Record instrumented auditable mutation workflows in immutable audit logs * Provide queryable audit history * Support regulatory and compliance reporting **Key entities:** * `AuditLog`: Append-only record of instrumented auditable mutation workflows Audit logs are append-only by design. Entries cannot be modified or removed to preserve compliance integrity. ### Reporting The Reporting bounded context provides operational visibility. **Responsibilities:** * Generate reconciliation reports * Expose dashboard metrics * Export reconciliation data in multiple formats **Key entities:** * `Report`: Reconciliation summary * `Dashboard`: Aggregated operational metrics * `ExportJob`: Asynchronous export execution ## Data flow *** Reconciliation follows a deterministic pipeline across bounded contexts: Reconciliation contexts, sources, field mappings, and rules are defined through the API. Discovery connects to external sources, detects their schemas, and runs extractions in-process with the embedded Fetcher engine. Extracted results are handed directly to Ingestion. Uploaded files and data extracted by Discovery are parsed, validated, normalized, and deduplicated. An `ingestion.completed` event is emitted. Matching rules are applied to eligible transactions, producing match groups with confidence scores on an integer scale of 0 to 100. EXACT and TOLERANCE groups with a confidence of at least 90 out of 100 can auto-confirm; FUZZY and DATE\_LAG groups always require manual review. Unmatched items become exceptions. Exceptions are classified, routed, and resolved either manually or via external systems. Resolution updates are propagated back to Matcher. Instrumented auditable mutation workflows across the pipeline are recorded in immutable audit logs. Users access reports and dashboards showing reconciliation status, match rates, and exception aging. ## Infrastructure components *** Matcher relies on the following infrastructure services: | Component | Purpose | Usage | | ----------------------------- | -------------------------- | ------------------------------------------------------------------------------- | | **PostgreSQL** | Primary data store | Domain data; configured multi-tenant deployments resolve a pool for each tenant | | **Valkey (Redis-compatible)** | Cache and coordination | Deduplication, locks, idempotency keys | | **Streaming backbone** | Business-event publication | Domain events published via lib-streaming | | **RabbitMQ** | Infrastructure queues | Internal work queues and dead-letter handling | | **Systemplane** | Runtime configuration | Hot-reload settings without restart via `/system/matcher/:key` admin API | ### Database architecture * **Tenant-specific pool resolution** in configured multi-tenant deployments for data separation * **Strong consistency** for matching and exception state * **Eventual consistency** for reporting views ### Multi-tenancy Matcher enforces strict tenant isolation: * With `AUTH_PROVIDER=plugin-auth`, tenant identity is extracted from `tenant_id` or `tenantId` JWT claims * `workos`, single-tenant, and authentication-disabled deployments use the configured default tenant * Tenant identifiers are never accepted via request parameters * Database access is scoped through the active tenant's connection pool * All queries are automatically constrained to the active tenant This model prevents cross-tenant data access and supports regulatory and audit requirements. ## Design patterns *** ### Hexagonal architecture Each bounded context follows the ports-and-adapters pattern: ``` context/ ├── adapters/ │ ├── http/ │ ├── postgres/ │ └── redis/ ├── ports/ ├── services/ │ ├── command/ │ ├── query/ │ └── worker/ └── domain/ ├── entities/ └── errors/ ``` ### Cqrs-light Write and read paths are separated at the service level: * `*_commands.go` for state mutations * `*_queries.go` for read operations This improves code organization and allows independent optimization of query paths. ### Outbox pattern Matcher uses per-event delivery policies. Outbox-backed events persist an outbox record and are dispatched asynchronously; other events can use direct delivery with an outbox fallback when the circuit is open. ## Next steps *** Explore the architecture through a guided example. Review authentication, authorization, and tenant isolation mechanisms. # Matcher concepts Source: https://docs.lerian.studio/en/matcher/matcher-concepts Learn Matcher's five core concepts — contexts, sources, field maps, match rules, and matches — that shape every reconciliation you build. The five core concepts in Matcher: **contexts**, **sources**, **field maps**, **rules**, and **matches**. Understand these and you'll understand how the entire system works. ## Context *** A **context** defines what you reconcile. It is the configuration container for sources and rules. Matcher creates a new context in `DRAFT`, and its inline sources and rules are optional. A context answers: *what am I matching against what?* You can start with an empty draft. To activate it, configure at least one `LEFT` source and one `RIGHT` source, map every source (or declare valid `camt053` options, which self-map), and add a match rule. If fee normalization is enabled, add a fee rule too. ### Context types | Type | Description | Example | | ------- | --------------------------- | -------------------------------------- | | **1:1** | One-to-one reconciliation | Bank statement vs ERP records | | **1:N** | One-to-many reconciliation | One payment covering multiple invoices | | **N:M** | Many-to-many reconciliation | Netting or aggregation scenarios | ### Example A context named **"Chase Bank vs ERP System"** would: * Define Chase Bank as one reconciliation source * Define your ERP system as another source * Specify the rules used to reconcile transactions between them ## Source *** A **source** is where transactions come from. A draft context can start without sources; an activatable context needs at least one source on each matching side. ### Source types * **LEDGER**: Ledger source category * **BANK**: Bank source category * **GATEWAY**: Payment-gateway source category * **CUSTOM**: Custom source category * **FETCHER**: A Fetcher source category ### Source setup Each source requires: * **Name**: Label it (e.g., "Chase Checking") * **Type**: Category (`LEDGER`, `BANK`, `GATEWAY`, `CUSTOM`, or `FETCHER`) * **Side**: Which matching side it feeds (`LEFT` or `RIGHT`) **Config** is optional. If you omit it, Matcher stores an empty config and uses parser defaults for absent policy keys. Field maps translate each source's fields into Matcher's standard schema. ## Field map *** A **field map** translates external field names into Matcher's standard schema. Every system calls things differently—field maps normalize that. ### Standard fields | Field | Required | Type | Description | | -------------- | -------- | -------- | ----------------------------------------- | | `external_id` | Yes | String | Source-system transaction identifier | | `amount` | Yes | Decimal | Transaction amount (positive or negative) | | `currency` | Yes | String | ISO 4217 currency code | | `date` | Yes | DateTime | Transaction date | | `description` | No | String | External reference or description | | `fee_amount` | No | Decimal | Optional fee amount column | | `fee_currency` | No | String | Optional fee currency column | The canonical vocabulary is closed — a field map that declares any other key is rejected. When a source config declares `camt053` options, Matcher uses its built-in ISO 20022 mapping and ignores a field map; activation treats that source as mapped. ### Example mapping A bank statement exposing `TXN_ID`, `VALUE`, `CCY`, and `POST_DATE` would be mapped as: ```json theme={null} { "external_id": "TXN_ID", "amount": "VALUE", "currency": "CCY", "date": "POST_DATE" } ``` ## Match rule *** A **match rule** tells Matcher how to compare transactions. Rules run in ascending priority; a transaction claimed by an earlier rule is unavailable to later rules, which still evaluate the remaining transactions. ### Rule types * **EXACT**: Compares configured fields exactly. Amount, currency, date (by day), and reference are enabled by default. * **TOLERANCE**: Matches amounts inside the configured absolute and/or percentage tolerance. Omitted amount tolerances and `dateWindowDays` default to `0`, so no drift or date window is allowed until you configure one. * **DATE\_LAG**: Matches within a configured day-difference band. `minDays` and `maxDays` both default to `0` (same day), not ±3. Like FUZZY, DATE\_LAG matches never auto-confirm — they always go to manual review. * **FUZZY**: Grades normalized transaction references. It uses `Reference`, populated from the transaction's `ExternalID`; a field-map `description` is not a FUZZY input. FUZZY only proposes—it never auto-confirms, so a human reviews every fuzzy link. ### Priority order Lower numbers run first. A rule claims its matching transactions; later rules continue with the remaining transactions. | Priority | Rule | Description | | -------- | ------------------ | -------------------------------------- | | 1 | Exact match | Amount, date, and reference must match | | 2 | Same-day tolerance | Same date, amount within 0.5% | | 3 | Week tolerance | Within 7 days, amount within 1% | These priorities and values are illustrative rules, not engine defaults. Configure the values for your reconciliation policy. ### Rule parameters | Rule type | Parameters | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | EXACT | `matchAmount`, `matchCurrency`, `matchDate`, `matchReference`: which fields must match exactly | | TOLERANCE | Top-level non-negative `percentTolerance` and/or `absTolerance` values (numbers or decimal strings); optionally set `dateWindowDays` (`0` by default; maximum `3650`). Do not wrap these values in a `tolerance` object. | | DATE\_LAG | `minDays`, `maxDays`: allowed day-difference band. Both default to `0`, must be from `0` to `3650`, and `maxDays` must be at least `minDays`; `inclusive` defaults to `true` and controls the upper bound. | | FUZZY | `minSimilarity`: normalized-reference threshold from `0` to `1` (defaults to `0.80`), plus optional amount, currency, and date axes. | ## Match *** A **match** is when transactions from different sources are reconciled together. It's the end goal. ### Match status | Status | Description | | ----------- | -------------------------------------------------------------------------------- | | `PROPOSED` | Matcher found it, waiting for confirmation | | `CONFIRMED` | Auto-approved or manually approved | | `REJECTED` | Manually rejected | | `REVOKED` | A previously confirmed match was unmatched, returning its transactions to review | ### Match patterns #### 1:1 match One transaction from each source is reconciled. ``` Bank: $100.00 on Jan 15 → ERP: $100.00 on Jan 15 ``` #### 1:N match One transaction is reconciled against multiple transactions. ``` Bank: $300.00 → ERP: $100.00 + $100.00 + $100.00 ``` #### N:1 match Multiple transactions are reconciled against a single transaction. ``` Bank: $50.00 + $50.00 + $50.00 → ERP: $150.00 ``` #### N:M match Multiple transactions on each side are reconciled together. N:M evaluation only executes `EXACT` and `TOLERANCE` rules; it considers up to four transactions per side in a group and caps each identity bucket at 40 candidates. ``` Bank: $100.00 + $200.00 → ERP: $150.00 + $150.00 ``` ### Match items Each match group contains **match items**, which record transaction participation and allocation. This enables partial reconciliation in split and aggregation scenarios. ## Exception *** An **exception** records a transaction that needs review, including unmatched transactions and matched transactions with residual conditions such as FX-rate variance. ### Exception status | Status | Description | | -------------------- | ------------------------------------------------ | | `OPEN` | Waiting for assignment | | `ASSIGNED` | Someone's investigating | | `PENDING_RESOLUTION` | A resolution is in progress, awaiting completion | | `RESOLVED` | Handled | ### Severity Matcher auto-classifies exceptions so you know what to prioritize. | Severity | Default criteria | | ------------ | --------------------------------------------------------------------------------------- | | **Critical** | Absolute base amount ≥ 100,000, age ≥ 120 hours, or a configured regulatory source type | | **High** | Absolute base amount ≥ 10,000 or age ≥ 72 hours | | **Medium** | Absolute base amount ≥ 1,000 or age ≥ 24 hours | | **Low** | All other cases | The classifier evaluates the criteria from top to bottom. When an exception meets the criteria of more than one severity, the highest matching severity applies. ### Resolution workflows * **Resolve**: Record a resolution label and optional reason to close an exception. * **Force match**: Resolve an exception by forcing a match with an override reason after manual review. * **Adjust entry**: Resolve an exception by creating an adjustment entry with a reason, notes, positive amount, currency, and effective time. ## Confidence score *** A **confidence score** indicates the reliability of an automated match on a 0–100 scale. Higher scores represent stronger alignment between transactions. ### Score calculation | Component | Weight | Description | | -------------- | ------ | ----------------------------------------------------------- | | Amount match | 40% | Degree of amount alignment | | Currency match | 30% | Currency consistency | | Date tolerance | 20% | Proximity of transaction dates | | Reference | 10% | Normalized reference alignment (graded 0–1 for FUZZY rules) | ### Confidence tiers | Tier | Score range | System behavior | | ----------------- | ----------- | -------------------------------------------------------------------------------------------------- | | **Auto-approved** | ≥ 90 | Confirmed automatically (EXACT and TOLERANCE rules only — FUZZY and DATE\_LAG always go to review) | | **Needs review** | 60–89 | Flagged for manual review | | **No match** | \< 60 | Does not create a match proposal | Confidence weights and tier thresholds are fixed by the engine and are not configurable. ## Audit log *** An **audit log** is an immutable, append-only record created by an instrumented workflow. It provides traceability for the actions Matcher records. ### Logged events Only workflows instrumented to emit an audit event create entries. When audit publishing is configured, verified producers include: * Context, source, field-map, and rule mutations * Exception workflows, including force match and adjust entry ### Audit entry contents | Field | Description | | ------------------------- | ----------------------------------------------------------------------------------------- | | `createdAt` | Record creation timestamp (UTC), which can differ from the audited action time | | `actorId` | Actor identifier, when supplied | | `action` | Action performed | | `entityType` | Affected entity type | | `entityId` | Identifier of the affected entity | | `changes` | Structured JSON event data; emitted events include `occurred_at` and any supplied changes | | `tenantSeq` | Per-tenant sequence number | | `prevHash` / `recordHash` | Hash chain linking each entry to the previous one, making tampering detectable | Audit logs are append-only. Entries cannot be modified or removed. ## Next steps *** See how these concepts are implemented across bounded contexts. Apply these concepts in a guided, hands-on flow. # Rule suggestions Source: https://docs.lerian.studio/en/matcher/matching/matcher-rule-suggestions Produce AI-suggested match rules, review them in a human-in-the-loop queue, and approve or reject each candidate before any rule is created. Matcher can propose config-only match rules from a context's history using AI — but **AI output is never authoritative**. Producing a suggestion creates no rule; a rule is created only when a human approves a suggestion. This guide covers the human-in-the-loop (HITL) rule-suggestion queue. The lane is gated by a global advisor kill-switch **and** a per-tenant opt-in (fail-closed: a non-opted-in tenant gets `403`). The egress payload is **aggregates only** — no raw transaction, money, or PII leaves your deployment. ## Produce suggestions *** Build aggregate, privacy-safe history features for a context, ask the AI advisor for candidate rules, and enqueue each surviving candidate in the review queue. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions" \ -H "Authorization: Bearer $TOKEN" ``` The response returns the newly created `PENDING_REVIEW` reviews — producing them creates no rule: ```json theme={null} { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "contextId": "550e8400-e29b-41d4-a716-446655440000", "candidate": { "type": "TOLERANCE", "priority": 10, "config": { "tolerance": { "percentTolerance": "0.01" } }, "rationale": "near-miss amounts cluster under 1%", "expectedImprovement": "+8% auto-match", "confidence": 0.82 }, "status": "PENDING_REVIEW", "version": 1, "createdAt": "2026-06-16T10:30:00Z", "updatedAt": "2026-06-16T10:30:00Z" } ], "count": 1 } ``` Candidate types are drawn from the closed vocabulary: `EXACT` (strict equality), `TOLERANCE` (within an amount band), or `DATE_LAG` (allowing a settlement-date offset). The candidate is **config only** — it never carries a monetary value or a transaction. ## List suggestions *** Cursor-paginated list of rule suggestions for a context, optionally filtered by status. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions?status=PENDING_REVIEW&limit=20" \ -H "Authorization: Bearer $TOKEN" ``` Query parameters: `status` (`PENDING_REVIEW`, `APPROVED`, `REJECTED`), `limit` (1–200), and `cursor`. Reading the queue egresses nothing. ## Approve a suggestion *** Approving a `PENDING_REVIEW` suggestion creates the match rule through the deterministic configuration write path. This is the **only** path from an AI suggestion to an active match rule, and it runs only on explicit human approval. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions/{suggestionId}/approve" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "reviewId": "550e8400-e29b-41d4-a716-446655440000", "createdRuleId": "550e8400-e29b-41d4-a716-446655440000" } ``` ## Reject a suggestion *** Rejecting a `PENDING_REVIEW` suggestion discards it — nothing is created. The request body is required, but its `reason` field is optional (send `{}` to reject without a reason). ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions/{suggestionId}/reject" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reason": "too aggressive" }' ``` The approving/rejecting principal is recorded for audit. ## Lifecycle after approval *** Once a suggestion is `APPROVED`, its `createdRuleId` links to a real match rule that participates in match runs exactly like a hand-authored rule. A review is a one-way state machine: a `PENDING_REVIEW` suggestion transitions to `APPROVED` or `REJECTED` once and cannot be re-decided. Attempting to re-decide, or to approve an already-linked review, returns `409`. An approved candidate that fails validation returns `422`. Preview how a candidate rule would behave before you approve it with the read-only simulate endpoint — see [Simulation](/en/matcher/matching/matcher-simulate). ## Response codes *** | Status | Meaning | | ------ | ---------------------------------------------------------------- | | `200` | Suggestions produced, listed, approved, or rejected | | `400` | Invalid status filter or suggestion id | | `403` | Tenant not opted into rule suggestions | | `404` | Rule suggestion not found | | `409` | Invalid state transition / already linked | | `422` | Approved suggestion failed validation | | `503` | Rule suggestion or advisor not available (author rules manually) | # Simulation Source: https://docs.lerian.studio/en/matcher/matching/matcher-simulate Dry-run a match rule and preview fee-schedule calculations in Matcher — both endpoints are read-only, so you can validate config before committing anything. Matcher exposes two read-only simulation endpoints so you can answer "what would happen?" before you commit configuration: **match simulation** (will this rule actually match?) and **fee simulation** (what fees would this schedule charge?). Neither persists anything. ## Match simulation *** Preview how a single rule — an existing configured rule (`ruleId`) **or** an inline candidate rule (`rule`) — would match a context's unmatched transactions, without committing anything. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/simulate" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contextId": "550e8400-e29b-41d4-a716-446655440000", "rule": { "type": "TOLERANCE", "config": { "tolerance": { "percentTolerance": "0.01" } } }, "sampleLimit": 25 }' ``` Supply **exactly one** of: * `ruleId` — preview an existing configured rule of the context. * `rule` — preview an un-persisted candidate definition (`type` is one of `EXACT`, `TOLERANCE`, `DATE_LAG`, `FUZZY`, plus its `config`). Supplying both, or neither, returns `400`. `sampleLimit` (1–200, default 25) caps the returned would-match pairs. The response reports how many 1:1 groups the rule would form, a bounded sample of would-match pairs (each with a confidence score and per-component rationale), and the per-side unmatched counts: ```json theme={null} { "ruleType": "TOLERANCE", "matchedGroups": 12, "unmatchedLeft": 3, "unmatchedRight": 5, "sampleTruncated": false, "sample": [ { "left": { "id": "...", "amount": "100.00", "currency": "BRL", "date": "2025-06-01T00:00:00Z" }, "right": { "id": "...", "amount": "100.00", "currency": "BRL", "date": "2025-06-01T00:00:00Z" }, "score": 90, "why": { "amountMatch": true, "currencyMatch": true, "dateMatch": true, "referenceScore": 0 }, "amountDelta": "0.00", "dateDeltaDays": 0 } ] } ``` Scope: the simulation scores by the deterministic rule engine over the **raw** transaction amounts. It does **not** apply run-time fee normalization or the FX-variance band, and it previews only 1:1 pairwise grouping (no 1:N/N:M allocation). A pair that only matches after fee normalization, inside the FX band, or via allocation is not counted here. ## Fee simulation *** Calculate fees for a given gross amount using a specific fee schedule. Use it to validate a schedule's rules before you attach it to a context. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/fee-schedules/{scheduleId}/simulate" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "grossAmount": "100.00", "currency": "USD" }' ``` The response returns the net amount, the total fee, and a per-item breakdown: ```json theme={null} { "grossAmount": "100.00", "netAmount": "97.70", "totalFee": "2.30", "currency": "USD", "items": [ { "name": "interchange", "fee": "1.50", "baseUsed": "100.00" } ] } ``` Fee simulation has no transaction metadata, so expression-fee items that require a transaction identifier surface their missing-identifier error as a `4xx`. A missing schedule returns `404`. ## When to use each *** | Use | When | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **Match simulation** (`/matching/simulate`) | You are authoring or reviewing a match rule and want to see how many transactions it would group, and which pairs, before committing it. | | **Fee simulation** (`/fee-schedules/{scheduleId}/simulate`) | You are configuring a fee schedule and want to verify the net/fee breakdown it would produce for a representative gross amount. | Both are strictly read-only: they never persist a run, group, item, rule, or transaction, and the tenant is always taken from the JWT. ## Response codes *** | Status | Meaning | | ------ | ------------------------------------------------------------------------- | | `200` | Simulation returned | | `400` | Invalid input (ambiguous/missing rule, invalid ids, invalid gross amount) | | `404` | Context, rule, or fee schedule not found | | `503` | Match simulation not available | # Confidence scoring Source: https://docs.lerian.studio/en/matcher/reference/matcher-confidence-scoring See how Matcher computes a 0–100 confidence score from amount, currency, date, and reference checks, and how EXACT, TOLERANCE, DATE_LAG, and FUZZY rules score. Confidence scores indicate the reliability of an automated match on a scale of 0-100. Higher scores mean greater certainty that two transactions represent the same financial event. ## Overview *** When Matcher identifies a potential match, it assigns a confidence score based on multiple factors. This score defines how the match is handled: * High scores (90+) are auto-approved * Mid-range (60 – 89) scores require review * Low scores (\<60) are treated as exceptions Matcher Confidence Scoring ## Score components *** Matcher uses a **binary weighted scoring system** with four components. Each component evaluates to either a full match (1.0) or no match (0.0) — there are no partial scores within a component. | Component | Weight | Points (match / no match) | | --------------- | ------ | ------------------------- | | Amount match | 40% | 40 / 0 | | Currency match | 30% | 30 / 0 | | Date proximity | 20% | 20 / 0 | | Reference match | 10% | 10 / 0 | ### Amount match (40%) The amount component has the highest weight because amount discrepancies often indicate different transactions. | Condition | Score | | ------------------------------------- | --------- | | Amounts match (within rule tolerance) | 40 points | | Amounts do not match | 0 points | Amount matching depends on the active rule type. An EXACT rule requires identical amounts; a TOLERANCE rule allows variance within the configured `percentTolerance` and `absTolerance`. ### Currency match (30%) Currency verification is binary — currencies either match or they don't. | Condition | Score | | ------------------ | --------- | | Same currency | 30 points | | Different currency | 0 points | ### Date proximity (20%) Date scoring checks whether the transaction dates fall within the configured window. | Condition | Score | | ---------------------------- | --------- | | Dates within allowed window | 20 points | | Dates outside allowed window | 0 points | The allowed window depends on the rule: an EXACT rule requires the same date (respecting `datePrecision`), while a DATE\_LAG rule accepts a day difference inside its `[minDays, maxDays]` band (the `inclusive` flag controls whether `maxDays` itself counts). ### Reference match (10%) For EXACT and TOLERANCE rules, reference comparison is binary. | Condition | Score | | ---------------------------------------------- | ------------------------------------------------------------------------------------------- | | References match (exact or case-insensitive) | 10 points | | Both references absent | 10 points (unless the rule sets `referenceMustSet`, which scores any absent reference as 0) | | References do not match, or only one is absent | 0 points | **FUZZY rules score the reference on a continuous scale.** For a FUZZY rule, the 10% reference slot carries a graded `ReferenceScore` between `0.0` and `1.0` (a similarity measure) rather than a strict `0`/`1`. A near-identical reference contributes close to the full 10 points while one that barely clears the fuzzy gate contributes proportionally less. As a result, FUZZY matches can produce non-multiple-of-10 scores such as 97 or 99. See [Possible scores](#possible-scores) below. **DATE\_LAG rules do not score references.** For DATE\_LAG rules the `ReferenceScore` is always `0.0`, so the 10% reference component contributes `0` points regardless of the reference values. The maximum achievable DATE\_LAG score is therefore 90 (40 + 30 + 20 + 0), which by design keeps date-lag matches in the manual-review path. ## Calculation formula *** The confidence score formula: ``` confidence = round( (amountMatch × 0.40 + currencyMatch × 0.30 + dateMatch × 0.20 + referenceScore × 0.10) × 100 ) ``` Where each factor is either `1.0` (match) or `0.0` (no match). The weights are hardcoded constants and are not configurable per context. ### Possible scores For **EXACT**, **TOLERANCE**, and **DATE\_LAG** rules, every component is binary, so the confidence score is always one of these values: **0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100** For these rule types, intermediate values (e.g. 87, 72, 55) never occur. (For DATE\_LAG the reference component is always `0`, so its scores never include the final 10 points.) For **FUZZY** rules this does not hold. Because the reference component is a continuous `ReferenceScore` (0.0–1.0), FUZZY matches can produce intermediate scores such as **97** or **99**. Note that regardless of the resulting score, a FUZZY match is **never auto-confirmed** — see [FUZZY matches never auto-confirm](#fuzzy-matches-never-auto-confirm). ## Calculation examples *** ### Exact match (score: 100) Two transactions with identical values on the same day: | Component | Comparison | Points | | --------- | -------------------------- | ------ | | Amount | $1,000.00 vs $1,000.00 ✓ | 40 | | Currency | USD vs USD ✓ | 30 | | Date | 2024-01-15 vs 2024-01-15 ✓ | 20 | | Reference | PAY-001 vs PAY-001 ✓ | 10 | **Final Score: 100** → Auto-confirmed ### High confidence match (score: 90) All fields match except reference: | Component | Comparison | Points | | --------- | -------------------------- | ------ | | Amount | $1,000.00 vs $1,000.00 ✓ | 40 | | Currency | USD vs USD ✓ | 30 | | Date | 2024-01-15 vs 2024-01-15 ✓ | 20 | | Reference | PAY-001 vs — ✗ | 0 | **Final Score: 90** → Auto-confirmed ### Medium confidence (score: 70) Amount and currency match, but date and reference do not: | Component | Comparison | Points | | --------- | -------------------------- | ------ | | Amount | $1,000.00 vs $1,000.00 ✓ | 40 | | Currency | USD vs USD ✓ | 30 | | Date | 2024-01-15 vs 2024-01-25 ✗ | 0 | | Reference | PAY-001 vs REC-999 ✗ | 0 | **Final Score: 70** → Needs review ### Low confidence (score: 40) Only amount matches: | Component | Comparison | Points | | --------- | -------------------------- | ------ | | Amount | $1,000.00 vs $1,000.00 ✓ | 40 | | Currency | USD vs EUR ✗ | 0 | | Date | 2024-01-15 vs 2024-01-25 ✗ | 0 | | Reference | PAY-001 vs REC-999 ✗ | 0 | **Final Score: 40** → Exception (below 60) ## Confidence tiers *** Matcher categorizes matches into tiers based on score: | Tier (score) | System behavior | Typical volume (illustrative) | | ------------------------ | ------------------------ | ----------------------------- | | **Auto-Approved** (≥ 90) | Automatically confirmed | 70-80% | | **Needs Review** (60-89) | Queued for manual review | 15-25% | | **Exception** (\< 60) | Treated as unmatched | 5-10% | ### How confidence tiers are applied When Matcher proposes a match, it evaluates the confidence score and applies the following steps: 1. If the score is **90 or higher**, the match is automatically confirmed for EXACT and TOLERANCE rules; FUZZY and DATE\_LAG matches always require manual review. 2. If the score is **between 60 and 89**, the match is queued for manual review. 3. If the score is **below 60**, no match is created and the transaction becomes an exception. 4. Reviewed matches can be either confirmed or rejected, updating their final status. This ensures high-confidence matches flow automatically while keeping human control where it matters. ### FUZZY matches never auto-confirm The auto-confirm behavior above applies to EXACT and TOLERANCE rules; FUZZY and DATE\_LAG matches always require manual review. Matches produced by **FUZZY** rules are **never auto-confirmed**, regardless of their confidence score — even a FUZZY match scoring 90 or above is always queued for manual review. This is by design: a fuzzy reference only contributes the 10% reference slot, so the financial fields (amount + currency + date) alone can already reach the 90 threshold. Capping FUZZY below auto-confirm guarantees that a human reviews the fuzzed reference before the match is committed — "fuzzy proposes, never commits." ## Confidence thresholds *** Matcher uses fixed thresholds to determine how matches are handled: | Threshold | Score | Behavior | | ------------ | ----- | -------------------------------------------------- | | Auto-confirm | >= 90 | Match is automatically confirmed | | Match | >= 60 | Match is proposed for manual review | | Exception | \< 60 | No match created; transaction becomes an exception | These thresholds are not configurable per context. ## Weights *** The component weights (40/30/20/10) are hardcoded constants. They cannot be adjusted per context or per rule. | Component | Weight | Rationale | | --------- | ------ | --------------------------------------------------------------- | | Amount | 40% | Amount is the strongest indicator of a valid match | | Currency | 30% | Currency mismatch usually means different transactions | | Date | 20% | Date proximity adds confidence but allows for settlement delays | | Reference | 10% | References are helpful but often missing or inconsistent | ## Best practices *** Track the percentage of transactions in each tier. Unusual shifts may indicate data quality issues or rule misconfiguration. Since confidence depends on which rules match, always dry-run rule changes before committing. This prevents unexpected increases in manual review volume. Periodically review matches just above the exception threshold. These often reveal opportunities for rule improvements. For EXACT, TOLERANCE, and DATE\_LAG rules, scoring is binary, so a score of 70 means exactly "amount + currency matched, date + reference did not." Use this to diagnose matching issues. FUZZY rules are the exception: their graded reference component can yield intermediate scores (e.g. 97), so a FUZZY score should be read as "financial fields matched plus a partial reference similarity." ## Next steps *** Configure rules that influence scoring. How FX affects confidence scoring. # Exports & disputes Source: https://docs.lerian.studio/en/matcher/reference/matcher-export-disputes Run asynchronous export jobs to generate downloadable reconciliation reports, and open, evidence, and resolve disputes against exceptions. This guide covers two operator workflows that both end in a downloadable or resolved artifact: **export jobs** (queue a report, poll until it succeeds, download the file) and **disputes** (open a dispute against an exception, attach evidence, then close it won or lost). Both are tenant-scoped from the JWT. ## Export jobs *** Exports are asynchronous. You create a job scoped to a context, poll its status by ID, and download the file once it reaches `SUCCEEDED`. Statuses are `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED`, `EXPIRED`, and `CANCELED`. ### Create an export job `POST` to the context's export-jobs collection. Responds `202 Accepted` with the job ID and a poll URL. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/export-jobs" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reportType": "MATCHED", "format": "CSV", "dateFrom": "2025-01-01", "dateTo": "2025-01-31", "sourceId": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```json theme={null} { "jobId": "550e8400-e29b-41d4-a716-446655440000", "status": "QUEUED", "statusUrl": "/v1/export-jobs/550e8400-e29b-41d4-a716-446655440001" } ``` * `reportType` — one of `MATCHED`, `UNMATCHED`, `VARIANCE`, `EXCEPTIONS` (aliases `MATCHES` and `UNMATCHED_TRANSACTIONS` are normalized). * `format` — `CSV`, `JSON`, or `XML` (normalized to uppercase). * `dateFrom` / `dateTo` — optional `YYYY-MM-DD`; `dateFrom` defaults to 30 days before `dateTo`, and `dateTo` defaults to tomorrow (UTC). * `sourceId` — optional source filter. `SUMMARY` and `PDF` are **not** supported for async export jobs and are rejected with `400`. The date window is also capped at a maximum span (an over-range request is rejected rather than silently clamped). ### Poll job status Read the top-level job route (the `statusUrl` from creation). ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/export-jobs/{jobId}" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "reportType": "MATCHED", "format": "CSV", "status": "SUCCEEDED", "recordsWritten": 4250, "bytesWritten": 524288, "fileName": "matched_report_2025-01-31.csv", "createdAt": "2025-01-15T10:30:00Z", "startedAt": "2025-01-15T10:30:05Z", "finishedAt": "2025-01-15T10:35:00Z", "expiresAt": "2025-01-16T10:30:00Z", "downloadUrl": "https://storage.example.com/exports/matched_report.csv?token=abc" } ``` `error` is present only when `status` is `FAILED`; `downloadUrl` appears only once the job has `SUCCEEDED` and the file is still available. You can list a context's jobs with `GET /v1/contexts/{contextId}/export-jobs`, list all jobs with `GET /v1/export-jobs`, and cancel a queued/running job with `POST /v1/export-jobs/{jobId}/cancel`. ### Download the file Returns a presigned URL, the original file name, a SHA-256 checksum, and the URL's remaining lifetime in seconds. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/export-jobs/{jobId}/download" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "downloadUrl": "https://storage.example.com/exports/report.csv?token=abc", "fileName": "matched_report.csv", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "expiresIn": 3600 } ``` Export files are purged after `expiresAt` (default 7 days). Once a job is `EXPIRED` the file is no longer downloadable — re-run the export to regenerate it. ## Disputes *** A dispute is opened against a specific **exception** when a reconciliation discrepancy needs to be contested. Its lifecycle starts at `DRAFT` → `OPEN`. From `OPEN` a dispute can move to `PENDING_EVIDENCE` (and back to `OPEN`) or close directly as `WON` / `LOST`. Only `WON` is terminal — a `LOST` dispute can be reopened to `OPEN`. ### Open a dispute `POST` to the exception's disputes collection. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/disputes" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "category": "BANK_FEE_ERROR", "description": "Transaction amount differs from invoice" }' ``` ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "exceptionId": "550e8400-e29b-41d4-a716-446655440001", "category": "BANK_FEE_ERROR", "state": "OPEN", "description": "Transaction amount differs from invoice", "openedBy": "user@example.com", "evidence": [], "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ``` `category` is one of `BANK_FEE_ERROR`, `UNRECOGNIZED_CHARGE`, `DUPLICATE_TRANSACTION`, or `OTHER`. The opening principal is recorded in `openedBy`. ### Submit evidence by URL Attach a reference to an already-hosted evidence file plus a describing comment. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/evidence" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "comment": "Attached bank statement showing correct amount", "fileUrl": "https://storage.example.com/evidence/doc123.pdf" }' ``` ### Upload an evidence file Stream the raw file bytes directly to tenant-scoped object storage — the comment travels as a query parameter and the file as the request body. Responds `201 Created` with the updated dispute. Allowed content types are `application/pdf`, `image/png`, `image/jpeg`, and `text/csv`; the body is capped at 10 MiB. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/evidence/upload?comment=Bank%20statement%20showing%20correct%20amount" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/pdf" \ --data-binary @statement.pdf ``` Each stored evidence item is returned on the dispute's `evidence` array: ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "disputeId": "550e8400-e29b-41d4-a716-446655440001", "comment": "Bank statement showing correct amount", "submittedBy": "user@example.com", "fileUrl": "https://storage.example.com/evidence/doc123.pdf", "submittedAt": "2025-01-15T10:30:00Z" } ``` The upload endpoint fails closed with `503` when object storage is not configured, rejects oversize bodies with `413`, and rejects content types outside the allowlist with `415`. The tenant and dispute are always resolved from the JWT and path — never from the body. ### Close a dispute Record the outcome. `won` sets the state to `WON` (terminal) or `LOST` (reopenable), with a required `resolution` note. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/close" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "won": true, "resolution": "Counterparty acknowledged the error and issued correction" }' ``` You can list disputes with `GET /v1/disputes` (filter by `state`, `category`, date range; sort and cursor-paginate) and fetch one with `GET /v1/disputes/{disputeId}`. ## Response codes *** | Status | Meaning | | ------ | -------------------------------------------------------------------------------- | | `200` | Export/dispute data returned | | `201` | Evidence file uploaded | | `202` | Export job accepted | | `400` | Invalid input (unsupported report type/format, bad date range, invalid category) | | `404` | Context, export job, exception, or dispute not found | | `409` | Invalid dispute state transition | | `413` | Evidence file exceeds the 10 MiB cap | | `415` | Evidence content type not in the allowlist | | `422` | Malformed field | | `503` | Export or evidence storage not configured | # Multi-currency matching Source: https://docs.lerian.studio/en/matcher/reference/matcher-multi-currency Reconcile transactions across currencies in Matcher by converting to a base amount using per-transaction FX hints, then apply your existing match rules. Matcher lets you reconcile transactions in different currencies by converting amounts to a common base currency before comparison. This enables matching across international transactions, treasury operations, and multi-entity reconciliations. ## Overview *** Multi-currency matching converts both transaction amounts to a base currency using the appropriate FX rate, then applies standard matching rules. If the converted amounts fall within tolerance, Matcher creates a match. Otherwise, it creates an exception for review. Multi-currency matching flow. ## How it works *** Multi-currency support is built into the existing context types (`1:1`, `1:N`, `N:M`) and match rules — there is no separate "multi-currency" context type. When transactions have different currencies, Matcher uses the `amountBase` and `currencyBase` fields on each transaction to compare converted amounts. Today, these base fields are populated **at match time**: Matcher derives them from per-transaction FX hints carried in the transaction's own metadata (see [FX from transaction metadata](#fx-from-transaction-metadata) below). You cannot supply a base amount directly at file upload — the field-map vocabulary has no base-amount columns. If a transaction already carries a base amount, Matcher honors it and never overwrites it, but the supported way to get base amounts onto your transactions is the FX metadata path. There is no external FX provider or rate-lookup service: the rate always comes from the transaction row itself. ### Key components | Component | Where it lives | Purpose | | ------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------- | | `amountBase` / `currencyBase` | Transaction fields | Base-currency amounts used for comparison, derived at match time from FX metadata | | `matchBaseAmount` / `matchBaseCurrency` | Rule config | Tell a rule to compare base amounts instead of originals | | `fx_rate`, `fx_base_currency`, `fx_notional_expr` | Transaction metadata | Per-transaction FX hints used to derive the base amount at match time | ## Configuring rules for multi-currency *** Enable multi-currency comparison by setting `matchBaseAmount` and `matchBaseCurrency` to `true` in the rule config. ### Exact rule with base amount matching ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "EXACT", "priority": 1, "config": { "matchBaseAmount": true, "matchBaseCurrency": true, "matchDate": true, "matchReference": false, "matchScore": 100, "matchBaseScore": 90 } }' ``` When `matchBaseAmount` is `true`, the rule compares `amountBase` fields instead of `amount`. When `matchBaseCurrency` is `true`, it compares `currencyBase` instead of `currency`. ### Tolerance rule with base amount matching ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "TOLERANCE", "priority": 2, "config": { "matchBaseAmount": true, "matchBaseCurrency": true, "percentTolerance": 0.02, "absTolerance": 10.0, "matchScore": 85, "matchBaseScore": 80 } }' ``` ### Confidence scoring The `matchScore` and `matchBaseScore` fields are **accepted and validated** in the rule config, but they **do not influence the calculated confidence score**. The scoring engine always uses the fixed internal component weights (`DefaultConfidenceWeights`: amount 40, currency 30, date 20, reference 10) to produce a 0–100 score. Values like `matchScore: 100` or `matchBaseScore: 90` are **not** applied directly as the match output. These fields are currently **reserved for future use** (retained for parity across rule configs and for metrics); setting them has no effect on how a match is scored or auto-confirmed today. Do not rely on `matchScore` / `matchBaseScore` to control confidence. Whether a rule matches original or base amounts, the confidence score is computed from the same 40/30/20/10 component weights. To reflect FX uncertainty, tune the matching **rule** itself (for example, use a TOLERANCE rule or adjust date/reference requirements) rather than these score fields. For the full scoring model, see [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring). ## FX from transaction metadata *** When a transaction has no base amount yet, Matcher converts it at match time using FX hints carried in that transaction's `metadata`. Matcher does **not** call any external rate provider — the rate travels with the row. Conversion only runs when `fx_base_currency` is present, and it never overwrites a base amount that is already set on the transaction. The original `amount` and `currency` are never mutated — conversion changes only the comparison. ### Metadata fields | Metadata field | Required | Purpose | | ------------------ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `fx_base_currency` | Yes (to trigger) | The base currency the amount is converted **into**. Becomes `currencyBase`. | | `fx_rate` | Yes, unless `fx_notional_expr` is set | Multiplicative rate. `amountBase = amount * fx_rate`. | | `fx_notional_expr` | No | Expression evaluated against the transaction metadata to derive the base notional directly. When present, it takes precedence over `fx_rate`. | | `fx_rate_source` | No | Optional label identifying where the rate came from, kept for validation and audit. Defaults to `metadata`. | ### Example transaction with FX metadata ```json theme={null} { "external_id": "txn_001", "amount": 1000.00, "currency": "EUR", "date": "2024-01-15", "metadata": { "fx_base_currency": "USD", "fx_rate": "1.085", "fx_rate_source": "ecb" } } ``` With the metadata above, Matcher derives `amountBase = 1000.00 * 1.085 = 1085.00` and `currencyBase = USD`, then compares against the other side using the rule's `matchBaseAmount` / `matchBaseCurrency` settings. If a transaction already carries a base amount, these metadata hints are ignored — Matcher never overwrites an existing base amount. If the hints are missing or malformed (unparseable rate, failed expression), the transaction simply does not participate in base-amount matching — the run is not aborted. ### When base fields are missing When a rule requires base-amount matching (`matchBaseAmount` / `matchBaseCurrency`) and transactions lack a base amount or base currency, Matcher records the condition under the `FX_RATE_UNAVAILABLE` exception reason. You can filter the exceptions list by `reason=FX_RATE_UNAVAILABLE` (along with the related `MISSING_BASE_AMOUNT` and `MISSING_BASE_CURRENCY` reasons) to find transactions that could not join base-amount comparison. ## FX rate-variance band *** Cross-currency amounts often disagree slightly because each side was converted at a different rate or on a different day. The `fxVarianceBand` key on TOLERANCE rules handles this: it defines a **second threshold stacked above the match tolerance**, expressed as a decimal fraction (`0.0001` = 1 basis point). After the strict-tolerance pass, Matcher rescans unmatched cross-currency `1:1` pairs. A pair whose base-amount residual exceeds the match tolerance but stays within the band still **matches** — the pair becomes a proposed group with a fixed confidence of 75, below the auto-confirm threshold, so it always requires human review. Both transactions are flagged with the `FX_RATE_VARIANCE` exception reason so the residual is recorded as a typed exception instead of collapsing to `UNMATCHED`. The band applies only when: * both sides carry a base amount and the same base currency; * the original currencies differ (same-currency drift is a plain mismatch, not an FX case); * all of the rule's other gates (date window, reference, currency, composite fields) still pass. A zero or absent `fxVarianceBand` disables the band. ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer ***" \ -H "Content-Type: application/json" \ -d '{ "type": "TOLERANCE", "priority": 3, "config": { "matchBaseAmount": true, "matchBaseCurrency": true, "percentTolerance": 0.01, "fxVarianceBand": "0.005" } }' ``` ## Transaction fields *** For multi-currency matching, each transaction carries both original and base currency fields. You supply `amount` and `currency` at upload; Matcher derives `amountBase` and `currencyBase` at match time from the FX metadata: | Field | Type | Description | | -------------- | ------- | ------------------------------------------------------------- | | `amount` | Decimal | Original transaction amount (supplied at upload) | | `currency` | String | Original ISO 4217 currency code (supplied at upload) | | `amountBase` | Decimal | Amount converted to base currency (derived from FX metadata) | | `currencyBase` | String | Base currency ISO 4217 code (derived from `fx_base_currency`) | ### Example transaction After FX conversion, a transaction looks like this internally: ```json theme={null} { "external_id": "txn_001", "amount": 1000.00, "currency": "EUR", "amountBase": 1085.00, "currencyBase": "USD", "date": "2024-01-15", "description": "PAY-2024-001" } ``` ## Example: cross-currency reconciliation *** **Source (EUR account):** | ID | Amount | Base amount | | -------- | ------------ | ------------ | | txn\_001 | 1,000.00 EUR | 1,085.00 USD | **Target (USD account):** | ID | Amount | Base amount | | -------- | ------------ | ------------ | | txn\_002 | 1,095.00 USD | 1,095.00 USD | With a TOLERANCE rule (`matchBaseAmount: true`, `percentTolerance: 0.02`): * Base amounts: $1,085.00 vs $1,095.00 * Variance: \$10.00 (0.92%) * Tolerance: 2% * Result: **Match** (0.92% \< 2%) ## Best practices *** Attach `fx_base_currency` and `fx_rate` (or `fx_notional_expr`) to each transaction's metadata at the source, using the rate that applied when the transaction settled. Because the rate travels with the row, results are reproducible across runs — no runtime rate lookups. `matchBaseScore` and `matchScore` are reserved fields and do not change the calculated confidence score — the engine always uses the fixed 40/30/20/10 weights. To flag FX-converted matches for review, design the rule itself (e.g. tighter tolerances or required reference/date checks) instead of relying on these score fields. FX conversions introduce small variances. Use TOLERANCE rules with matchBaseAmount to allow for rounding and rate timing differences. Use a consistent base currency across all contexts. USD is common for international operations; use your reporting currency for domestic + international. ## Next steps *** How match scores work and what thresholds apply. Full reference for rule types and config fields. # Split and aggregate matches Source: https://docs.lerian.studio/en/matcher/reference/matcher-split-aggregate Reconcile 1:1, 1:N (split and aggregate), and N:M transaction patterns using context types and rule allocation flags to control how amounts are distributed. Real-world reconciliation often involves transactions that don't match 1:1. A single payment may cover multiple invoices, or several deposits may consolidate into one bank entry. Matcher handles these complex scenarios through split and aggregate matching. ## Overview *** Matching cardinality is controlled by the **context type**. Matcher supports three context types: | Context type | Description | Example | | ----------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------- | | **1:1** — one to one | One source to one target | Single invoice payment | | **1:N** — one to many / many to one | One source to many targets (**split**) or many sources to one target (**aggregate**) | Bulk payment covering invoices; deposits consolidated at a bank | | **N:M** — many to many | Any combination of sources and targets | Complex netting | There is no separate `N:1` context type. Aggregate matching (many sources to one target) is simply the `1:N` context type applied in the aggregate direction — the same context type covers both split and aggregate. ## How it works *** Split and aggregate behavior is controlled by two mechanisms: 1. **Context type** — determines the matching cardinality (`1:1`, `1:N`, or `N:M`). 2. **Rule allocation flags** — control how amounts are distributed within a match group. There is no separate "split" or "aggregate" setting on the context. The context type defines what patterns are allowed, and the rule config controls allocation behavior. ### Context type mapping | Context type | Allowed patterns | | ------------ | ----------------------------------------------------------------------------- | | `1:1` | One source to one target only | | `1:N` | One source to many targets (split), or many sources to one target (aggregate) | | `N:M` | Any combination of sources and targets | ### Rule allocation settings All rule types accept allocation flags in their `config`: | Field | Type | Description | | -------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowPartial` | Boolean | Allow partial allocation of transaction amounts | | `allocationDirection` | String | Allocation order: `LEFT_TO_RIGHT` or `RIGHT_TO_LEFT` | | `allocationToleranceMode` | String | How tolerance is measured: `ABS` (absolute) or `PERCENT` | | `allocationToleranceValue` | Decimal | Tolerance threshold for allocation residuals | | `allocationUseBaseAmount` | Boolean | Use base (converted) amount for allocation | | `feeAware` | Boolean | Fee-aware 1:N allocation: consume each candidate's gross share (net + fee) instead of net only. Useful for marketplace splits where the payout is net of fees | | `nmDeductionBand` | Decimal | TOLERANCE rules only. Short-pay band for the N:M solver, as a decimal fraction of the short invoice face (`0.05` = 5%). Lets a payment subset under-pay an invoice subset within the band. Zero or absent disables it | ### Example: tolerance rule with allocation ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "TOLERANCE", "priority": 2, "config": { "percentTolerance": 0.01, "absTolerance": 5.0, "matchCurrency": true, "allowPartial": true, "allocationDirection": "LEFT_TO_RIGHT", "allocationToleranceMode": "ABS", "allocationToleranceValue": 10.0, "matchScore": 85, "matchBaseScore": 80 } }' ``` `matchScore` and `matchBaseScore` are accepted and validated but **reserved/inert** — they do not change the calculated confidence score. Confidence is always computed from the fixed internal component weights (amount 40, currency 30, date 20, reference 10). See [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring). ## Creating a 1:N context *** To enable split or aggregate matching, create a context with type `1:N`: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Payment Reconciliation", "type": "1:N", "interval": "daily" }' ``` API Reference: [Create context](/en/reference/matcher/create-context) ## 1:N split matching *** One source transaction matches multiple target transactions. ### Common use cases * **Bulk payment**: Single wire covering multiple invoices * **Payroll**: One bank debit for multiple salary payments * **Settlement**: One gateway payout for multiple orders ### Example: bulk invoice payment **Source (Bank Statement):** | ID | Amount | Reference | | --------- | ----------- | ----------------- | | bank\_001 | \$15,000.00 | BULK-PAY-2024-001 | **Targets (Ledger Entries):** | ID | Amount | Invoice | | -------- | ---------- | ------------ | | inv\_001 | \$5,000.00 | INV-2024-001 | | inv\_002 | \$7,500.00 | INV-2024-002 | | inv\_003 | \$2,500.00 | INV-2024-003 | **Result:** 1:3 match with full allocation ## Aggregate matching (many-to-one) *** Multiple source transactions match one target transaction. This is the aggregate direction of the `1:N` context type — it is not a separate `N:1` type. ### Common use cases * **Bank deposits**: Multiple checks deposited as one credit * **Card settlements**: Daily batch of transactions as one deposit * **Cash consolidation**: Multiple register receipts to one deposit ### Example: consolidated deposit **Sources (Point of Sale):** | ID | Amount | Register | | -------- | ---------- | -------- | | pos\_001 | \$1,250.00 | REG-01 | | pos\_002 | \$980.00 | REG-02 | | pos\_003 | \$1,770.00 | REG-03 | **Target (Bank Statement):** | ID | Amount | Reference | | --------- | ---------- | ---------------- | | bank\_002 | \$4,000.00 | DEPOSIT-20240120 | **Result:** 3:1 match with full allocation ## N:M many-to-many matching *** Multiple source transactions match multiple target transactions. This is the most complex pattern. ### Common use cases * **Intercompany netting**: Multiple invoices netted against multiple payments * **Trade settlements**: Complex clearing with partial fills * **Revenue recognition**: Multiple deliveries against multiple advances ### Example: intercompany netting **Sources (Company A Payables):** | ID | Amount | Reference | | -------- | ----------- | ---------- | | pay\_001 | \$10,000.00 | IC-PAY-001 | | pay\_002 | \$8,000.00 | IC-PAY-002 | **Targets (Company A Receivables):** | ID | Amount | Reference | | -------- | ----------- | ---------- | | rec\_001 | \$12,000.00 | IC-REC-001 | | rec\_002 | \$6,000.00 | IC-REC-002 | **Result:** 2:2 match, \$18,000 total matched To enable N:M matching, create a context with type `N:M`: ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Intercompany Netting", "type": "N:M", "interval": "weekly" }' ``` ## Running and reviewing matches *** After configuring the context and rules, trigger a matching run and review the resulting groups. ### Run matching ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mode": "DRY_RUN" }' ``` ### View match run history ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/matching/contexts/{contextId}/runs" \ -H "Authorization: Bearer $TOKEN" ``` ### View a run's match groups The `contextId` query parameter is required. The response is a cursor-paginated list of match groups, each containing its matched transactions (across all cardinalities) and confidence scores. ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/matching/runs/{runId}/groups?contextId={contextId}" \ -H "Authorization: Bearer $TOKEN" ``` ### Break (unmatch) a match group To reverse an incorrect group, unmatch it. A `PROPOSED` group is rejected with a reason and its transactions return to `UNMATCHED`. For a `CONFIRMED` group, Matcher also reverses the residual/open-item effects that confirmation applied, atomically with revoking the group and returning its transactions. The `contextId` query parameter is required, and a `reason` is sent in the body. ```bash cURL theme={null} curl -X DELETE "https://api.matcher.example.com/v1/matching/groups/{matchGroupId}?contextId={contextId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reason": "incorrect match - amounts do not match" }' ``` If the confirmed group's reversal removes the final live contribution behind an obligation, that open item becomes terminal `WITHDRAWN`: it remains as history but is not nettable or carried into another run. Matcher checks reversibility before changing anything. If a later live entry still stands on the residual, or a newer live obligation would conflict with restoring a terminal item on the same identity, the endpoint returns `409 Conflict` and leaves the group, transactions, and open items unchanged. ## Matching algorithm *** The algorithm depends on the context type. ### 1:N — deterministic sequential allocation For split and aggregate (`1:N`) scenarios, Matcher uses deterministic sequential allocation: 1. **Sort**: Transactions are sorted deterministically to ensure reproducible results across runs. 2. **Iterate**: The engine walks through candidates in priority order. 3. **Allocate**: Amounts are distributed according to the `allocationDirection` setting (`LEFT_TO_RIGHT` or `RIGHT_TO_LEFT`). 4. **Track residuals**: Any remaining unallocated amounts are tracked. If `allowPartial` is `true`, an overshooting leg is capped to the remaining amount; an under-covered split still surfaces a diagnostic exception. ### N:M — set-matching solver For `N:M` scenarios, Matcher does **not** allocate sequentially. It uses a bounded subset-selection solver: candidates are bucketed by the rule's match identity, and the solver searches for a subset of left transactions and a subset of right transactions that reconcile against each other, with cardinality capped per side. Selection is deterministic over the sorted input, each proposed group must clear the fixed confidence gate (minimum score 60), and no transaction lands in two proposed groups within a run. On TOLERANCE rules, the `nmDeductionBand` key lets the solver admit a payment subset that under-pays an invoice subset within the band. ### Exception reasons Transactions that cannot be fully reconciled surface as typed exceptions: * `SPLIT_INCOMPLETE` — allocations exist but do not fully cover the target amount, regardless of `allowPartial`. * `OVER_SETTLED` — a leg over-shot what it was settling; the over-settled remainder is surfaced as a typed break. You can filter the exceptions list by these `reason` values. ## Best practices *** Many-to-many matching is complex. Start with simpler patterns and enable N:M only when necessary. Small rounding differences are common in split payments. Set allocationToleranceValue to a few cents to avoid false exceptions. Only set allowPartial to true when partial matches are expected. This prevents false matches from incomplete data. Always test split and aggregate matching in DRY\_RUN mode first to verify allocation results. Track residual amounts over time. Growing residuals may indicate systematic matching issues. ## Next steps *** Configure rules and allocation settings. Security and access control. # Context rules Source: https://docs.lerian.studio/en/matcher/ui/context-rules Create and manage EXACT, TOLERANCE, DATE_LAG, and FUZZY match rules from the Rules tab of a context's setup page in the Matcher UI. Use the **Rules** tab in a context's setup page to define and manage match rules that control how transactions are paired during reconciliation. Rules specify the criteria the match engine uses to identify corresponding transactions across data sources. ## Accessing the Rules tab *** Navigate to **Configure → Contexts** in the left sidebar. Select a context from the list to open its setup page. Select the **Rules** tab. The global **Context Switcher** in the sidebar lets you change the active reconciliation context at any time. ## Rules list *** The Rules tab lists match rules in priority order. Rules evaluate top to bottom; the first match wins. Each visible row shows the rule's strategy and a short configuration summary, with **Move rule up** / **Move rule down** arrows, an **Edit rule** button, and a **Delete rule** button. For contexts with 100 rules or fewer, priority is assigned automatically when you create a rule — a new rule is appended to the end of the chain. To change precedence, reorder the list with the up/down arrows. There is no priority field in the form. The Rules tab loads only the first 100 rules by priority and provides no pagination. Rules after that cannot be viewed or managed here. **Add rule** derives the new priority from those 100 rules; once a rule at priority 101 is hidden, the next creation attempt conflicts with it and fails. ## Creating a rule *** In the **Rules** tab, click the **Add rule** button. A dialog opens. Select a **Strategy**: | Strategy | Description | | ------------- | -------------------------------- | | **Exact** | Field-for-field equality | | **Tolerance** | Amount/date within a band | | **Date lag** | Settlement delay window | | **Fuzzy** | Approximate reference similarity | Depending on the strategy, different fields appear (see below). Click **Create rule**. Strategy is fixed after creation. To change a rule's strategy, delete the rule and recreate it. ## Rule types *** ### EXACT Matches transactions by comparing fields for exact equality. Top-level toggles: | Field | Description | | ------------------- | --------------------------- | | **Match amount** | Compare transaction amounts | | **Match currency** | Require currency equality | | **Match date** | Compare transaction dates | | **Match reference** | Compare reference fields | The **Advanced** section adds: | Field | Description | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Date precision** | Precision for date comparison: `DAY` or `TIMESTAMP` | | **Case-insensitive reference compare** | Ignore case when comparing references | | **Reference must be present** | When **Match reference** is enabled, require a non-empty reference value | | **Match base amount** | Also match on the base (pre-conversion) amount | | **Match base currency** | Also match on the base currency | | **Sign mode** | How amount signs compare: `same` matches like signs, `opposite` matches a refund against its charge, `ignore` compares magnitudes only | | **Match score** | Accepted configuration value (0–100). It is stored but does not determine awarded confidence, which uses the engine's fixed weighted comparison components | | **Base-match score** | Accepted configuration value (0–100). It is stored but does not determine awarded confidence | | **Allocation (1:N / N:1)** | Partial allocation settings (see below) | | **Match fields** | Composite-key editor for matching on additional named fields, combined with the toggles above | ### TOLERANCE Matches transactions within a numeric or date tolerance band. Top-level fields: | Field | Description | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **Absolute tolerance** | Fixed amount tolerance. The effective amount band uses the larger of this and the percentage-derived amount tolerance | | **Percent tolerance** | Percentage-derived amount tolerance (e.g., `0.005` = 0.5%). The effective amount band uses the larger of this and the absolute tolerance | | **Date window (days)** | Allowed date drift between sides (0–3650) | | **Match currency** | Require currency equality | Zero tolerances are valid — both values default to `0`, which makes the amount band require equality after the default `HALF_UP` rounding at scale `2`. The **Advanced** section adds: | Field | Description | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Rounding mode** | How values round before comparison: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE` | | **Rounding scale** | Decimal places for rounding (0–10) | | **Percentage base** | Which side the percent tolerance is measured against: `MAX`, `MIN`, `AVERAGE`, `LEFT`, or `RIGHT` | | **Match reference** | Compare reference fields | | **Case-insensitive reference compare** | Ignore case when comparing references | | **Reference must be present** | When **Match reference** is enabled, require a non-empty reference value | | **Match base amount** / **Match base currency** | Also match on the base amount/currency | | **Sign mode** | `same`, `opposite`, or `ignore` (as for EXACT) | | **Match score** / **Base-match score** | Accepted configuration values (0–100). They are stored but do not determine awarded confidence, which uses the engine's fixed weighted comparison components | | **FX variance band** | Extra cross-currency tolerance above the match band, as a decimal fraction (`0.0001` = 1 basis point). A residual within it still matches and records an FX rate-variance exception; `0` disables | | **Lockbox deduction band** | Short-pay tolerance for N:M lockbox matching, as a decimal fraction of the invoice face (`0.05` = 5%); `0` disables | | **Business-day & timezone** | Holiday calendar and timezone for date comparison | | **Allocation (1:N / N:1)** | Partial allocation settings (see below) | | **Match fields** | Composite-key editor, as for EXACT | ### DATE\_LAG Matches transactions that occur within a configurable number of days of each other. Top-level fields: | Field | Description | | -------------------- | --------------------------------------------------------------------------------------------------- | | **Min days** | Minimum day lag allowed (0–3650; defaults to `0`) | | **Max days** | Maximum day lag allowed (0–3650; defaults to `0`) | | **Direction** | Which side is expected to be earlier: `ABS` (absolute), `LEFT_BEFORE_RIGHT`, or `RIGHT_BEFORE_LEFT` | | **Inclusive bounds** | Include the min/max day boundaries | **Max days** must be greater than or equal to **Min days**. Exclusive bounds (`Inclusive bounds` off) with **Min days** set to `0` are rejected, because that would exclude same-day transactions. The **Advanced** section adds: | Field | Description | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Fee tolerance** | Maximum absolute difference between the transaction amounts (defaults to `0`) | | **Match currency** | Require currency equality | | **Match reference** | Off by default; enable to require reference equality | | **Case-insensitive reference compare** | Ignore case when comparing references | | **Reference must be present** | When **Match reference** is enabled, require a non-empty reference value | | **Match score** | Accepted configuration value (0–100). It is stored but does not determine awarded confidence, which uses the engine's fixed weighted comparison components | | **Business-day & timezone** | **Holiday calendar** (`US Federal` or `Brazil ANBIMA`), **Timezone** (IANA zone, defaults to UTC), and **Count business days only** — measure the lag in business days, skipping the calendar's weekends and holidays. Requires a calendar | | **Allocation (1:N / N:1)** | Partial allocation settings (see below) | DATE\_LAG rules have no base-amount options (`Match base amount` / `Match base currency` are only available on EXACT and TOLERANCE). ### FUZZY Matches transactions by approximate reference similarity, with configurable financial checks on amount, currency, and date. Fuzzy matches always propose for review — they never auto-confirm. Top-level fields: | Field | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Minimum similarity** | Reference similarity threshold as a ratio from 0 to 1 (`0.80` = 80% similar). References below it are rejected; if they clear it and the enabled financial checks pass, the pair receives a graded confidence score. Default `0.80` | | **Match amount** | Compare transaction amounts | | **Match currency** | Require currency equality | | **Match date** | Compare transaction dates | The **Advanced** section adds: | Field | Description | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Date precision** | `DAY` or `TIMESTAMP` | | **Reference must be present** | Defaults to on — two blank references would otherwise compare as fully similar | | **Sign mode** | `same`, `opposite`, or `ignore` | | **Match score** | Accepted configuration value (0–100; default `70`). It is stored but does not determine awarded confidence; fuzzy confidence uses the engine's fixed weighted components and graded reference similarity | | **Allocation (1:N / N:1)** | Partial allocation settings (see below) | FUZZY rules have no reference-equality toggle and no base-amount options. ## Allocation settings *** EXACT, TOLERANCE, DATE\_LAG, and FUZZY rules include an **Allocation (1:N / N:1)** block inside the **Advanced** section: | Field | Description | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | **Allow partial allocation** | Match a set of items against one counterpart, splitting the amount across them; an unallocated remainder carries forward as an open item | | **Fee-aware split** | Consume each leg's gross share (net + fee) from the counterpart instead of its net amount alone. Off uses net only | | **Allocation direction** | Order legs are consumed in; defaults to left-to-right | | **Allocation tolerance mode** | How the residual is bounded: `ABS` (absolute amount) or `PERCENT` (fraction, `0.01` = 1%). Defaults to `ABS` | | **Allocation tolerance value** | Residual the split may leave; defaults to `0` | | **Allocate on base amount** | Use the base amount instead of the converted amount for allocation | ## Previewing a rule *** The rule dialog includes a **Preview matches** panel. Click **Run preview** to test the rule before saving. The preview is read-only: it uses up to 5,000 extraction-complete unmatched transactions, forms only 1:1 pairs, and does not apply fee normalization, FX variance bands, or allocation. The panel shows how many pairs **Would match**, the unmatched left/right counts, and up to 25 highest-scoring would-match pairs. Nothing is saved. ## Editing a rule *** Click the **Edit rule** button on a rule to open the dialog. The **Strategy** selector is disabled — strategy is fixed after creation. Update the remaining fields and click **Save changes**. ## Reordering rules *** Use the **Move rule up** / **Move rule down** arrows on the list to change rule precedence. Rules evaluate top to bottom; the first match wins. ## Deleting a rule *** Click the **Delete rule** button on a rule and confirm in the **Delete rule?** dialog. The rule is removed from the chain; remaining rules keep their order. # Matcher UI overview Source: https://docs.lerian.studio/en/matcher/ui/matcher-ui-overview Meet Matcher's standalone web app — configure contexts, sources, and rules, upload files, run matches, and resolve exceptions without writing any API calls. The **Matcher UI** is the standalone web application for operating Matcher, Lerian's reconciliation engine. It puts the full reconciliation workflow behind a visual interface: configure contexts, sources, and match rules, upload transaction data, run reconciliations, review matches, and resolve exceptions — all without writing API calls. The Matcher UI is **not part of Lerian Console**. It ships as its own web application, deployed alongside the Matcher API, and does not require a Console installation. If you operate other Lerian products through the Console, Matcher is the exception: it brings its own work surface. Matcher is only available if you've acquired the Matcher product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more. ## Accessing the Matcher UI *** Your platform team deploys the Matcher UI alongside the Matcher API and shares the URL for your environment. Sign in with the credentials your identity provider issues; every action in the UI is authorized against the Matcher API with your identity and tenant — the UI has no separate permission model. Once signed in, the **active context** shown in the sidebar controls which reconciliation context you're working with. The active context applies across Matcher's pages. ## Main work surfaces ***
Reconciliation health at a glance — match rate, cash exposure, SLA compliance, and setup progress for the active context. Create contexts and manage their sources, match rules, and schedules from each context's detail page. Import transaction files and start reconciliation runs, then track their progress. Review confirmed match groups and confidence scores inside each run's detail page, or pair remaining transactions in the Unmatched workbench. Work the queue of transactions that didn't match automatically — resolve, dispatch, or escalate them into formal disputes. Operational history, immutable audit logs, and downloadable archives for compliance.
For a page-by-page directory of every screen in the UI, see [Matcher UI pages](/en/matcher/ui/matcher-module). ## Beyond the UI *** Everything the Matcher UI does runs through the Matcher API, so you can mix surfaces freely: * **API** — automate any workflow programmatically. Start with the [developer quick start](/en/reference/matcher/matcher-developer-quick-start). * **MCP server** — let an AI assistant operate Matcher through the [Model Context Protocol](/en/matcher/mcp/matcher-mcp-overview). # Pix reconciliation Source: https://docs.lerian.studio/en/matcher/use-case-guides/matcher-pix-reconciliation Configure Matcher to reconcile Pix cash-in and cash-out flows between Midaz Ledger and BACEN's SPI settlement extract using endToEndId as the matching key. This guide walks through reconciling Pix transactions between Midaz Ledger and BACEN (Brazil's Central Bank) settlement data using Matcher. It covers both Pix sent (cash-out) and Pix received (cash-in), from configuration to daily operation and exception handling. By the end of this guide, you will have a fully configured Matcher context that automatically reconciles your Pix transactions against BACEN's SPI settlement extracts on a daily basis. ## Pix transaction flows *** Understanding how Pix transactions flow through the system is essential for configuring reconciliation correctly. The two flows below show what Matcher needs to reconcile on each side. ### Pix sent (cash-out) Pix sent cash-out flow 1. **Client initiates Pix** — The end user triggers a Pix payment via the app or API. 2. **Plugin creates initiation** — The Pix plugin creates an initiation record and resolves the destination account via DICT lookup. 3. **Plugin processes payment** — The plugin debits the client account in Midaz (transaction in `pending` status) and sends the payment instruction to SPI. 4. **Settlement confirmed** — SPI sends a webhook confirming settlement. The Midaz transaction is committed. 5. **Matcher reconciles** — Matcher compares the committed Midaz transaction against the corresponding entry in the BACEN SPI settlement extract. ### Pix received (cash-in) Pix received cash-in flow 1. **Inbound Pix arrives** — SPI sends a synchronous webhook containing the inbound Pix data. 2. **Plugin validates** — The Pix plugin validates the payload and approves the transaction. 3. **Credit transaction created** — The plugin creates a CREDIT transaction in Midaz for the recipient account. 4. **Settlement confirmed** — The settlement webhook confirms the transaction is final. 5. **Matcher reconciles** — Matcher compares the Midaz credit transaction against the corresponding entry in the BACEN SPI settlement extract. In both flows, the **endToEndId** is the unique identifier that links the Midaz transaction to the BACEN settlement record. This is the primary key for reconciliation. ## Configuration step-by-step *** Create a reconciliation context for Pix transactions. Use type `1:1` because each Pix transaction has exactly one corresponding BACEN settlement entry. ```json theme={null} { "name": "Pix Daily Reconciliation", "type": "1:1", "interval": "daily", "feeToleranceAbs": "0", "feeTolerancePct": "0", "autoMatchOnUpload": false } ``` Pix transactions have no intermediate fees or partial settlements. A Pix of R$ 150.00 in Midaz must appear as exactly R$ 150.00 in the BACEN extract. Set both tolerance values to zero — they are decimal strings, so pass `"0"`. Setting `autoMatchOnUpload` to `false` gives you control over when matching runs, which is important when you need both sources ingested before executing. See the full request schema at [Create context](/en/reference/matcher/create-context). Each context needs two sources: one for Midaz transactions and one for the BACEN settlement extract. **Source A — Midaz (type `LEDGER`):** ```json theme={null} { "name": "Midaz - Pix Transactions", "type": "LEDGER", "side": "LEFT", "config": {} } ``` `LEDGER` is Matcher's category for internal ledger data — it is not a live Midaz connector. You export the day's Pix transactions from Midaz (including the `endToEndId` metadata as a flat column) and upload the export to this source, manually or via an automated pipeline. See [Matcher and Midaz](/en/matcher/integrations/matcher-midaz-integration) for the export/import flow. **Source B — BACEN SPI extract (type `CUSTOM`):** ```json theme={null} { "name": "BACEN SPI Settlement Extract", "type": "CUSTOM", "side": "RIGHT", "config": { "description": "Daily SPI settlement file from BACEN" } } ``` The BACEN source uses type `CUSTOM` because the SPI settlement file is uploaded manually or via an automated pipeline each day. Each source must declare a `side` (`LEFT` or `RIGHT`). A context reconciles its `LEFT` source against its `RIGHT` source, so assign one side to Midaz and the other to BACEN and keep the assignment consistent across both sources. See the full request schema at [Create source](/en/reference/matcher/create-source). Field maps tell Matcher how to translate fields from each source into the canonical fields used for matching. A field map is a JSON object in the form `{ "": "" }`. The keys come from Matcher's closed canonical vocabulary (`external_id`, `amount`, `currency`, `date`, and optional `description`, `fee_amount`, `fee_currency`); the values are the raw column names in each source. Lookups are **flat** — a mapping value must name a top-level column in the row, so the Midaz export must carry `endToEndId` as its own flat column (see [Matcher and Midaz — custom field mapping](/en/matcher/integrations/matcher-midaz-integration#custom-field-mapping)). `external_id` is the **cross-side matching reference** — the value the engine compares between the two sources when a rule sets `matchReference`. Map it to the `endToEndId` on **both** sides. Do not map side-local row IDs (the Midaz transaction `id`, the BACEN `id_liquidacao`) into `external_id`: those values never agree across sources, so reference matching would never find a counterpart. The following table shows how each canonical field maps to the column in each source: | Canonical field | Midaz export column | BACEN extract column | | --------------- | ------------------- | -------------------- | | `external_id` | `endToEndId` | `end_to_end_id` | | `amount` | `amount` | `valor` | | `currency` | `asset_code` | `moeda` | | `date` | `created_at` | `data_liquidacao` | **Midaz source — field map:** ```json theme={null} { "mapping": { "external_id": "endToEndId", "amount": "amount", "currency": "asset_code", "date": "created_at" } } ``` **BACEN source — field map:** ```json theme={null} { "mapping": { "external_id": "end_to_end_id", "amount": "valor", "currency": "moeda", "date": "data_liquidacao" } } ``` See [Create field map](/en/reference/matcher/create-field-map) for the full request schema and [Field mapping](/en/matcher/configuration/matcher-field-mapping) for the canonical vocabulary. Match rules define how Matcher compares transactions across sources. For Pix reconciliation, two rules cover the vast majority of scenarios. **Rule 1 — Exact match by endToEndId (priority 1):** ```json theme={null} { "type": "EXACT", "priority": 1, "config": { "matchAmount": true, "matchCurrency": true, "matchDate": true, "matchReference": true, "datePrecision": "DAY", "caseInsensitive": false, "referenceMustSet": true, "matchScore": 100 } } ``` This rule resolves approximately 95% of cases. The `endToEndId` is unique per Pix transaction across the entire ecosystem. When the reference, amount, currency, and date all match, it is a confirmed reconciliation with maximum confidence. Note that `caseInsensitive` is set to `false` because `endToEndId` values are case-sensitive, and `referenceMustSet` is `true` to ensure both sides carry the `endToEndId` before comparing — this prevents false positives on amount and date alone. **Rule 2 — Date tolerance fallback (priority 51):** ```json theme={null} { "type": "DATE_LAG", "priority": 51, "config": { "maxDays": 1, "minDays": 0, "inclusive": true, "direction": "ABS", "feeTolerance": 0, "matchScore": 85, "matchCurrency": true } } ``` A Pix initiated at 23:58 may settle in BACEN on the following calendar day. This rule allows a 1-day window to cover D+1 settlement scenarios. Note that this rule relies on amount and currency matching only — the `endToEndId` comparison is handled by Rule 1. See [Create match rule](/en/reference/matcher/create-match-rule) for the full request schema and all available rule types. Once all configuration is in place, activate the context and create a daily schedule. **Activate the context:** ```bash theme={null} curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "ACTIVE" }' ``` **Create a schedule to run daily at 07:00 UTC:** ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/schedules" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "cronExpression": "0 7 * * *", "enabled": true }' ``` Running at 07:00 UTC provides enough margin for D+1 settlements to appear in the BACEN extract and for the daily file to be uploaded before the matching run executes. See [Update context](/en/reference/matcher/update-context) and [Create schedule](/en/reference/matcher/create-schedule) for the full request schemas. ## Daily operation *** Once configured, the daily reconciliation workflow follows five steps. Upload the previous day's SPI settlement file to the BACEN source. Matcher parses CSV, JSON, XML, and other formats from its [format catalog](/en/matcher/imports/matcher-import-formats). ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/upload" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: multipart/form-data" \ -F "file=@bacen_spi_2026-03-17.csv" \ -F "format=csv" ``` This step can be automated via a pipeline that fetches the SPI file and uploads it before the scheduled matching run. Export the previous day's committed Pix transactions from Midaz — including the `endToEndId` metadata as a flat column — and upload the export to the Midaz source the same way. This step is usually automated by the same pipeline. See [Matcher and Midaz](/en/matcher/integrations/matcher-midaz-integration) for the export/import flow. The scheduled run executes automatically at 07:00 UTC. To run matching manually, use the run endpoint. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mode": "DRY_RUN" }' ``` Use `DRY_RUN` first to preview results without committing them. When satisfied, run again with `COMMIT`: ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mode": "COMMIT" }' ``` After the run completes, retrieve the matched groups to see the results. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/matching/runs/{runId}/groups" \ -H "Authorization: Bearer $TOKEN" ``` Each group shows the matched Midaz transaction and its corresponding BACEN settlement entry, along with the rule that matched them and the confidence score. Unmatched transactions appear as exceptions. These require investigation — a transaction present in one source but not the other, or a mismatch in amount or date beyond the configured tolerance. Review exceptions, determine the root cause, and resolve them by force matching, ignoring, or correcting the underlying data. Always run a `DRY_RUN` first when testing new rules or after configuration changes. This prevents unintended matches from being committed. ## Practical example — one day of data *** The following example illustrates a complete reconciliation run for March 17, 2026. ### Midaz transactions (Source A) | ID (type) | endToEndId | Amount · date | | ----------------- | ---------------------- | -------------------------- | | txn-001 (Pix OUT) | E123456789202603170001 | 150.00 · 2026-03-17 10:15 | | txn-002 (Pix IN) | E987654321202603170042 | 3200.50 · 2026-03-17 11:30 | | txn-003 (Pix OUT) | E555666777202603170099 | 89.90 · 2026-03-17 23:58 | | txn-004 (Pix IN) | E111222333202603170007 | 500.00 · 2026-03-17 14:00 | ### BACEN SPI extract (Source B) | id\_liquidacao | end\_to\_end\_id | valor / data\_liquidacao | | -------------- | ---------------------- | ------------------------ | | liq-8801 | E123456789202603170001 | 150.00 / 2026-03-17 | | liq-8802 | E987654321202603170042 | 3200.50 / 2026-03-17 | | liq-8803 | E555666777202603170099 | 89.90 / 2026-03-18 | | liq-8804 | E444555666202603170055 | 750.00 / 2026-03-17 | ### Match results | endToEndId | Midaz ↔ BACEN | Result (rule) | | ------------- | ------------------ | ----------------------------------- | | E12345...0001 | txn-001 ↔ liq-8801 | Match — EXACT (score: 100) | | E98765...0042 | txn-002 ↔ liq-8802 | Match — EXACT (score: 100) | | E55566...0099 | txn-003 ↔ liq-8803 | Match (D+1) — DATE\_LAG (score: 85) | | E11122...0007 | txn-004 ↔ — | Exception — — | | E44455...0055 | — ↔ liq-8804 | Exception — — | ### Analysis * **txn-001 and txn-002:** Exact match on endToEndId, amount, currency, and date. Rule 1 resolved these with confidence score 100. * **txn-003:** Pix initiated at 23:58, settled in BACEN on 2026-03-18. Rule 2 (DATE\_LAG with 1-day window) paired this with confidence score 85. Date-lag matches never auto-confirm — the pair lands in the review queue for a human to confirm. * **txn-004:** Present in Midaz but absent from BACEN. Possible settlement failure or SPI timeout. Investigate the transaction status via the Pix plugin. * **liq-8804:** Present in BACEN but absent from Midaz. An inbound Pix that was not processed. Check webhook delivery or reprocess the message. ## Handling Pix exceptions *** The following table covers the most common Pix exception scenarios and recommended actions. | Scenario | Probable cause | Recommended action | | ---------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------- | | In Midaz, not in BACEN | Settlement failure, SPI timeout, rejected transaction | Check transaction status in the Pix plugin. If rejected, revert in Midaz. | | In BACEN, not in Midaz | Inbound Pix not processed, webhook failure | Reprocess the message. Create a manual transaction if needed. | | Amount mismatch | Rare in Pix (no intermediate fees). Possible rounding error. | Investigate original records. Force match if the difference is acceptable. | | Date mismatch (>1 day) | Held transaction, reprocessing | Verify it is the same Pix. Force match or ignore. | ### Force match When you have confirmed that two records represent the same Pix transaction but Matcher could not match them automatically, use force match. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/force-match" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "notes": "Confirmed same Pix via endToEndId lookup in SPI", "overrideReason": "D+2 settlement delay confirmed with BACEN" }' ``` ### Ignore transaction When a transaction should be excluded from reconciliation (for example, a duplicate entry or an already-reversed Pix), mark it as ignored. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/transactions/{transactionId}/ignore" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reason": "Pix reversal already processed — duplicate entry" }' ``` See [Force match](/en/reference/matcher/force-match-exception) and [Ignore transaction](/en/reference/matcher/ignore-transaction) for the full request schemas. ## Pix refunds (devoluções) *** Pix refunds generate reverse transactions that also need reconciliation. When a refund is processed, the Pix plugin creates a new transaction in Midaz with: * The `originalEndToEndId` linking back to the original Pix transaction * A new `returnIdentification` (rtrId) that uniquely identifies the refund in SPI BACEN's settlement extract includes refund entries with both identifiers, allowing Matcher to reconcile them against the corresponding Midaz refund transactions. For low refund volumes, these can be reconciled within the same Pix Daily Reconciliation context. For high volumes, create a separate context dedicated to refund reconciliation. This simplifies exception triage and keeps refund metrics isolated from standard Pix flow metrics. Refund initiation via `POST /v1/transfers/{id}/refunds` is a **Pix plugin** endpoint, **not** a Matcher endpoint. Matcher does not initiate transfers or refunds — it only reconciles the resulting transactions. Each refund carries the `originalEndToEndId` and a new `returnIdentification` for end-to-end tracking, which Matcher then uses to match the refund against BACEN's settlement extract. For refund initiation, see the [Pix plugin documentation](/en/midaz/plugins/pix/pix-overview). ## Best practices *** The `endToEndId` is the unique Pix identifier across the entire ecosystem — from the initiating institution through SPI to the receiving institution. Ensure it is stored in Midaz transaction metadata and present in the BACEN extract. Without it, reconciliation falls back to amount and date matching, which is far less reliable. Pix transactions near end of day may settle in BACEN on D+1. Scheduling Matcher for 07:00 UTC ensures all settlements from the previous day are included in the BACEN extract before matching runs. This eliminates false exceptions caused by timing. When processing high Pix volumes, create two separate contexts — one for cash-out and one for cash-in. This simplifies exception triage, provides more granular metrics per flow, and allows independent scheduling if needed. A healthy Pix reconciliation achieves greater than 99% automatic match rate. If the rate drops below 95%, investigate systemic issues such as plugin failures, BACEN format changes, or missing metadata in Midaz transactions. Pix has no intermediate fees, partial settlements, or processing charges. If amounts diverge between Midaz and BACEN, it indicates a real problem — not rounding. Keep both `feeToleranceAbs` and `feeTolerancePct` at zero. Always run a `DRY_RUN` before `COMMIT`, especially after rule or field map changes. This lets you review match results and catch configuration errors before they affect production data. ## Key metrics *** Track these metrics to monitor the health of your Pix reconciliation process. | Metric | Healthy value | Alert threshold | | -------------------------- | -------------------- | --------------- | | Automatic match rate | Above 99% | Below 95% | | Daily exceptions | Below 0.5% of volume | Above 2% | | Average resolution time | Under 4 hours | Over 24 hours | | Unresolved exceptions 48h+ | 0 | More than 5 | Use Matcher's dashboard endpoints to monitor these metrics in real time. See [Dashboard metrics](/en/reference/matcher/get-dashboard-metrics). ## Next steps *** Learn how to configure and manage reconciliation contexts and data sources. Explore all available rule types and advanced matching configurations. Deep dive into automatic field mapping and real-time sync with Midaz Ledger. Detailed guide on investigating, force matching, and managing reconciliation exceptions. # What is Matcher? Source: https://docs.lerian.studio/en/matcher/what-is-matcher Discover Matcher, Lerian's reconciliation engine that automates transaction matching, confidence scoring, exception routing, and audit trails for compliance. **Matcher** is Lerian's reconciliation engine that automates transaction reconciliation between any data sources—internal systems, banks, payment processors, ERPs, or payment gateways. Matcher is available to licensed customers; its repository is maintained internally. You configure matching rules once. Matcher handles the repetitive work—matching transactions, flagging discrepancies, and maintaining audit trails for compliance. ## Why use Matcher *** Every financial institution reconciles transactions — comparing internal records against bank statements, payment processors, and ERPs. Most do it manually in spreadsheets. This does not scale: matching takes hours, errors go unnoticed, and month-end close becomes stressful. Matcher solves this: * **Automate matching**: Configure rules once, let the system handle routine matches * **Know what's reliable**: Confidence scores tell you which matches are solid and which need review * **Route exceptions**: Send unmatched items to JIRA, ServiceNow, or your own tools via webhooks * **Stay compliant**: Every match and resolution is logged immutably for audits ## What Matcher does *** * **Match automatically**: Apply exact or tolerance-based rules. Get confidence scores (0-100) for every match * **Handle multiple sources**: Reconcile between banks, ERPs, payment processors, ledgers—all at once if needed * **Manage exceptions**: Unmatched items get classified, routed to your team, and tracked until resolved * **Audit everything**: Immutable logs and SOX-ready reports for compliance — a complete audit trail ### Matching features * **Exact matching**: Match transactions by ID, amount, date, and custom attributes * **Tolerance matching**: Allow controlled variance in amounts, defined as absolute values or percentages * **Date range matching**: Handle timing differences using configurable date windows * **Multi-currency support**: Reconcile transactions across currencies with FX rate handling * **Split and aggregate matching**: Support 1:N, N:1, and N:M transaction relationships * **Confidence scoring**: Assign a 0–100 score indicating match reliability * **Dry run mode**: Preview matching results before committing changes ### Data ingestion * Import data in **CSV**, **JSON**, **XML**, or **ISO 20022 camt.053** format — plus a catalog of regional layouts (CNAB, OFX, acquirer EDI files). See [Formats and templates](/en/matcher/imports/matcher-import-formats) * Automatic validation and duplicate detection * Flexible field mapping for heterogeneous data sources * Optional: reconcile Midaz Ledger exports through the `LEDGER` source type ### Workflow integration * Route exceptions to **JIRA** or **ServiceNow** * Emit events through **webhooks** for custom integrations * Receive resolution callbacks from external systems * Track SLAs and automatically escalate breaches ## Common use cases *** Match your internal records against daily bank statements. Upload CSVs, map fields, run matching. Review exceptions where the bank and your systems don't align. Reconcile Stripe or Adyen settlements against your ledger or ERP. Handle splits (one payment, many invoices) and aggregates (many payments, one deposit) automatically. Match Visa, Mastercard, or Elo settlement files against your transaction records. Tolerance rules handle fee variances and timing differences between authorization and settlement. Reconcile acquiring transactions with complex rate structures. Handle MCC-based rates, transaction type variations, and Interchange Plus models where rates vary by card type, merchant segment, and transaction method. Verify SAP, Oracle, or NetSuite transactions against your other systems. Catch discrepancies before month-end close or audits. ## How Matcher works *** Define what you're reconciling (contexts), where data comes from (sources), and how to match (rules). Upload files or pull data automatically. Matcher validates, normalizes, and deduplicates everything. Run a matching job. Matcher applies your rules and assigns confidence scores to each proposed match. High-confidence matches (score 90 or above) are confirmed automatically — except fuzzy and date-lag matches, which always go to review. Lower scores and unmatched items go to your review queue. Handle exceptions: force a match, create an adjustment, split transactions, or route to JIRA/ServiceNow. Export reconciliation reports. Every action is logged immutably for audits. ## Integrations with the lerian ecosystem *** Matcher is a stand-alone product that can optionally integrate with other components of the Lerian ecosystem. | Product | Integration | | ----------------------------------------- | -------------------------------------------------------------------------------------------- | | [Midaz](/en/midaz/about-midaz) | Optional: Reconcile exported Ledger data through the `LEDGER` source type | | [Reporter](/en/reporter/what-is-reporter) | Optional: Reconciliation reports and audit summaries | | [Flowker](/en/flowker/what-is-flowker) | Optional: Event-driven reconciliation workflows | Matcher works independently or as part of the Lerian Studio ecosystem. Use it alone or connect it to Midaz and other financial components as needed. All Matcher write endpoints (POST, PUT, PATCH) include built-in idempotency through the `X-Idempotency-Key` request header. For details, see [Retries and idempotency](/en/reference/retries-idempotency). ## Next steps *** Understand the key concepts: contexts, sources, rules, matches, and exceptions. Explore Matcher's architecture and data flow. Run your first reconciliation, then review matches and exceptions with confidence. Learn how to configure exact, tolerance, and date-based matching rules. [Explore the Matcher API](/en/reference/matcher/matcher-developer-quick-start). Browse endpoints for reconciliation contexts, sources, and matching rules. # APIX 001 Source: https://docs.lerian.studio/en/reporter/apix-001 Generate the APIX 001 XML report for BACEN — monthly Pix statistics including transactions, refunds, cautionary blocks, revenue, and availability metrics. Reporter lets you generate XML-based reports that follow the official APIX structure, as required by the Brazilian Central Bank (BACEN). This guide walks you through the structure and logic used to generate the **APIX 001** report (Document 1201) in XML. APIX 001 reports must strictly follow the XSD schema defined by BACEN (version 2.5). Reporter automates the XML generation, but you remain responsible for validating the output and ensuring compliance with regulatory requirements. ## What is APIX 001? *** The **APIX 001** is a monthly regulatory report that Pix participants — direct or indirect — must submit to the Brazilian Central Bank. It consolidates **operational statistics** for the institution's Pix ecosystem during a given month. ### What BACEN expects to receive The report covers ten data sections: | Section | Description | | ----------------------- | ---------------------------------------------------------------------- | | **Transacoes** | Transaction volumes and values, broken down by detail type and purpose | | **Devolucoes** | Refund volumes and values (MED and other reasons) | | **BloqueiosCautelares** | Cautionary block counts and values | | **Receitas** | Revenue from Pix operations by source and person type | | **TemposTransacoes** | Transaction processing time percentiles (P50, P99) | | **TemposDict** | DICT operation time percentiles (queries, registrations, portability) | | **ConsultasDict** | Total DICT queries in the period | | **Disponibilidade** | System availability index (percentage) | | **TempoAutorizacoes** | Authorization processing time (P95) | | **Autorizacoes** | Pix Automático authorization counts and stock by payer type | ### Submission requirements | Document | Frequency | Deadline | Reference | | ------------ | --------- | ---------------------------------------- | ------------------------------ | | **APIX 001** | Monthly | 15th business day of the following month | Document 1201, XSD version 2.5 | ### Header attributes The root `` element requires these attributes: | Attribute | Description | Format | | ----------- | ------------------------------------------------ | ------------------------------------ | | `DtArquivo` | File generation date | `YYYY-MM-DD` | | `Ano` | Reference year | `YYYY` | | `Mes` | Reference month | `1`–`12` | | `ISPB` | Institution's ISPB code (first 8 digits of CNPJ) | 8 numeric digits | | `NomeResp` | Responsible person's name | Text (max 200 chars) | | `EmailResp` | Responsible person's email | Valid email | | `TelResp` | Responsible person's phone | Text (max 14 chars) | | `TipoEnvio` | Submission type | `I` (Inclusion) or `S` (Replacement) | ## Understanding the data structure *** Before building the template, it's important to understand how the Pix plugin data maps to each APIX 001 section. ### Pix plugin data sources The template queries data from the **Pix plugin** tables registered as Reporter data sources. The main entities used are: | Data Source | Table | Content | | ------------------- | ------------- | ------------------------------------------------------- | | `payment.transfers` | Pix transfers | Transaction records with status, amount, type, and fees | | `payment.refunds` | Pix refunds | Refund records with status, amount, and reason code | | `dict.entries` | DICT entries | Pix key registrations | | `dict.claims` | DICT claims | Portability and ownership claims | The Pix plugin does not currently track DICT query events. The `dict.entries` table stores key registrations, not consultations. For `ConsultasDict`, source this metric from your infrastructure monitoring or API gateway logs. The template placeholder is included for structural reference only. The data source prefix (e.g. `pix_btg`) depends on how you register the Pix plugin in Reporter. Replace it with your actual data source name. ### Transfers table structure | Field | Type | Description | | ------------------------- | --------- | ------------------------------------------------------ | | `id` | UUID | Unique transfer identifier | | `status` | String | Transaction status: `COMPLETED`, `REJECTED`, `PENDING` | | `amount` | Decimal | Transaction value in BRL | | `transfer_type` | String | `CASHIN` (receiving) or `CASHOUT` (sending) | | `destination_person_type` | String | `NATURAL_PERSON` or `LEGAL_PERSON` | | `fee_charge` | JSONB | Fee structure (populated only for CASHIN transactions) | | `failed_reason` | String | Rejection reason code (when status is REJECTED) | | `created_at` | Timestamp | Creation date and time | ### Refunds table structure | Field | Type | Description | | ------------ | --------- | ------------------------------------------- | | `id` | UUID | Unique refund identifier | | `status` | String | Refund status: `COMPLETED`, `PENDING` | | `amount` | Decimal | Refund value in BRL | | `reason` | String | Reason code: `FR01`, `BE08`, `MD06`, `SL02` | | `created_at` | Timestamp | Creation date and time | ### Refund reason codes | Code | Meaning | APIX detail type | | ------ | --------------------------------------------- | ---------------- | | `FR01` | Fraud (MED — Mecanismo Especial de Devolução) | 1 | | `BE08` | Bank error | 2 | | `MD06` | Customer request | 2 | | `SL02` | Creditor-specific service (Pix Saque/Troco) | 2 | ### Fee charge structure (JSONB) The `fee_charge` field is a JSONB column populated **only for CASHIN** (receiving) transactions: ```json theme={null} { "applied": true, "calculationType": "FIXED", "totalAmount": "1.50", "netAmount": "98.50", "fees": [ { "type": "TRANSACTION_FEE", "amount": "1.50" } ] } ``` CASHOUT transactions do not have fee charges in the current product model. Revenue from CASHOUT sources should be reported as zero unless your implementation charges fees on outgoing transfers. ## Data mapping *** ### Transaction detail types | Code | Description | Mapping | | ---- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | 5 | Indirect participant (settled via direct participant) | All completed transactions via the settlement partner | | 6 | Direct participant (own settlement) | Zero for indirect participants | | 7 | Rejected due to fraud indication | Transactions with `status == "REJECTED"` filtered by fraud-indication reason (see warning below) | Detail type 7 should only count rejections due to fraud indication — not all rejected transactions. The current template uses `status == "REJECTED"` as a simplified filter. In production, cross-reference with `dict.infraction_reports` (filtering by fraud-related `situation_type`) or apply specific `failed_reason` codes. Validate this logic with your engineering team before submitting to BACEN. ### Transaction purposes | Code | Description | Mapping | | ---- | ------------------- | ------------------------------------------------ | | 1 | Transfer / Purchase | Completed transactions (`status == "COMPLETED"`) | | 2 | Pix Saque | Zero if not implemented | | 3 | Pix Troco | Zero if not implemented | | 4 | Pix Automático | Zero if not implemented | The report requires exactly *12 transaction entries* — one for each combination of 3 detail types × 4 purposes. Entries with no matching data must still be present with zero values. ### Revenue sources | Source | Description | Mapping | | ------ | -------------------------------------- | -------------------------------------------------------- | | 1 | Initiation by legal person (CASHOUT) | `fee_charge.totalAmount` from CASHOUT + `LEGAL_PERSON` | | 2 | Receiving by legal person (CASHIN) | `fee_charge.totalAmount` from CASHIN + `LEGAL_PERSON` | | 3 | Receiving by natural person (CASHIN) | `fee_charge.totalAmount` from CASHIN + `NATURAL_PERSON` | | 4 | Initiation by natural person (CASHOUT) | `fee_charge.totalAmount` from CASHOUT + `NATURAL_PERSON` | ### Refund detail types | Code | Description | Mapping | | ---- | ------------------ | ------------------------------------- | | 1 | Fraud refund (MED) | `reason == "FR01"` | | 2 | Other reasons | `reason != "FR01"` (BE08, MD06, SL02) | ### Cautionary block detail types | Code | Description | | ---- | --------------------- | | 1 | Requested | | 2 | Received | | 3 | Canceled by requester | | 4 | Canceled by recipient | ### Authorization payer types (Pix Automático) | Code | Description | | ---- | -------------- | | 1 | Natural person | | 2 | Legal person | ## Using Reporter *** Here is the complete template for generating APIX 001 in Reporter. This example uses `pix_btg` as the data source prefix — replace it with the name configured in your Reporter data source setup. Reporter's report request (`POST /v1/reports`) accepts only `templateId` and `filters` — there is no input for arbitrary template parameters. Set the reference year and month (`Ano`, `Mes`) and the responsible person's attributes (`NomeResp`, `EmailResp`, `TelResp`) directly in the template before each submission, the same way `TipoEnvio` is set. ```tpl Template theme={null} {% count_by pix_btg:payment.transfers if status == "COMPLETED" %} {% sum_by pix_btg:payment.transfers by "amount" if status == "COMPLETED" %} 0.00 5 1 0 0.00 0.00 5 2 0 0.00 0.00 5 3 0 0.00 0.00 5 4 0 0.00 0.00 6 1 0 0.00 0.00 6 2 0 0.00 0.00 6 3 0 0.00 0.00 6 4 {% count_by pix_btg:payment.transfers if status == "REJECTED" %} {% sum_by pix_btg:payment.transfers by "amount" if status == "REJECTED" %} 0.00 7 1 0 0.00 0.00 7 2 0 0.00 0.00 7 3 0 0.00 0.00 7 4 {% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason == "FR01" %} {% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason == "FR01" %} 1 {% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason != "FR01" %} {% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason != "FR01" %} 2 0 0.00 1 0 0.00 2 0 0.00 3 0 0.00 4 0.00 1 {% sum_by pix_btg:payment.transfers by "fee_charge.totalAmount" if transfer_type == "CASHIN" and destination_person_type == "LEGAL_PERSON" and status == "COMPLETED" %} 2 {% sum_by pix_btg:payment.transfers by "fee_charge.totalAmount" if transfer_type == "CASHIN" and destination_person_type == "NATURAL_PERSON" and status == "COMPLETED" %} 3 0.00 4 0 0 1 0 0 2 ``` ## Code breakdown *** ### Root element ```tpl theme={null} ``` * `DtArquivo`: File generation date, dynamically inserted via `date_time` * `Ano` and `Mes`: Reference year and month — static values you update in the template for each submission (the report request carries no template parameters) * `ISPB`: First 8 digits of the institution's CNPJ, extracted using the `slice` filter from Midaz onboarding data * `TipoEnvio`: `I` for inclusion, `S` for replacement of previously approved data ### Transactions section The template explicitly declares all 12 required entries (3 detail types × 4 purposes). Dynamic queries are used where data exists: ```tpl theme={null} {% count_by pix_btg:payment.transfers if status == "COMPLETED" %} {% sum_by pix_btg:payment.transfers by "amount" if status == "COMPLETED" %} 0.00 5 1 ``` * `count_by` counts records matching the filter condition * `sum_by ... by "field"` sums a specific field across matching records * `ValorEspecie` is `0.00` for standard transfers (only non-zero for Pix Saque/Troco) Entries for purposes 2, 3, and 4 (Pix Saque, Troco, Automático) use hardcoded zeros when those features are not implemented. Detail type 6 (direct participant settlement) also uses zeros for indirect participants. ### Refunds section Refunds are split by reason code using the `reason` field: ```tpl theme={null} {% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason == "FR01" %} {% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason == "FR01" %} 1 {% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason != "FR01" %} {% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason != "FR01" %} 2 ``` * `FR01` maps to BACEN detail type 1 (fraud via MED) * All other reason codes (`BE08`, `MD06`, `SL02`) map to detail type 2 ### Revenue section Revenue is extracted from the `fee_charge.totalAmount` JSONB field, filtered by transfer type and person type: ```tpl theme={null} {% sum_by pix_btg:payment.transfers by "fee_charge.totalAmount" if transfer_type == "CASHIN" and destination_person_type == "LEGAL_PERSON" and status == "COMPLETED" %} 2 ``` The `fee_charge` JSONB field uses nested field path syntax (`fee_charge.totalAmount`). Reporter's Pongo2 engine navigates the JSON structure to access the nested value. ### Time metrics and availability Time metrics (transaction processing times, DICT operation times) and availability index must be sourced from your **infrastructure monitoring system** — they are not derived from transactional data. Populate these values from your SPI logs and uptime monitoring. ### DICT queries ```tpl theme={null} ``` This placeholder counts DICT entries (key registrations) as a structural reference. In production, replace `dict.entries` with your actual DICT query log data source or populate `QtdConsultas` manually from infrastructure metrics. ## Rendered example *** ```xml theme={null} 150000 75000000.00 0.00 5 1 0 0.00 0.00 5 2 0 0.00 0.00 5 3 0 0.00 0.00 5 4 0 0.00 0.00 6 1 0 0.00 0.00 6 2 0 0.00 0.00 6 3 0 0.00 0.00 6 4 320 160000.00 0.00 7 1 0 0.00 0.00 7 2 0 0.00 0.00 7 3 0 0.00 0.00 7 4 45 22500.00 1 230 115000.00 2 0 0.00 1 0 0.00 2 0 0.00 3 0 0.00 4 0.00 1 85000.00 2 12000.00 3 0.00 4 0 0 1 0 0 2 ``` ## Request example with date filter *** To generate the APIX 001 for a specific month, send a `POST /v1/reports` request with the following body: ```json Request theme={null} { "templateId": "APIX_001_TEMPLATE_ID", "filters": { "pix_btg": { "payment.transfers": { "created_at": { "between": ["2026-03-01T00:00:00Z", "2026-03-31T23:59:59Z"] } }, "payment.refunds": { "created_at": { "between": ["2026-03-01T00:00:00Z", "2026-03-31T23:59:59Z"] } } } } } ``` | Field | Description | | ----------------------------------- | --------------------------------------------------- | | `templateId` | APIX 001 template identifier registered in Reporter | | `filters.pix_btg.payment.transfers` | Date filter for the transfers collection | | `filters.pix_btg.payment.refunds` | Date filter for the refunds collection | | `created_at.between` | Filters records created within the specified month | Dates must be in ISO 8601 format with UTC timezone (`Z`). Make sure to cover the entire reference month — from the first second to the last. ## XSD validation rules *** Key constraints from the official APIX 001 XSD (version 2.5) that your data must satisfy: | Field | Constraint | | ------------------------ | --------------------------------------------------------- | | Monetary values | Up to 15 digits total, 2 decimal places, minimum 0 | | Quantities | Integer, minimum 0, max 999,999,999,999 | | Time values | Up to 8 digits total, 2 decimal places, 0–999,999 seconds | | Availability | Percentage, 0.00–100.00 | | ISPB | Exactly 8 numeric digits | | Transaction entries | Exactly 12 (3 detail types × 4 purposes) | | Refund entries | Exactly 2 | | Cautionary block entries | Exactly 4 | | Revenue entries | Exactly 4 | | Authorization entries | Exactly 2 | BACEN validates both structure and cardinality. If your report has fewer or more entries than expected in any section, the submission will be rejected. ## Best practices *** ### Zero-value entries Even when there are no transactions for a given combination, the entry must still appear in the report with zero values. BACEN requires all 12 transaction entries, all 4 revenue entries, all 4 cautionary block entries, and all 2 authorization entries — regardless of whether data exists. ### Replacement submissions Use `TipoEnvio="S"` only to replace a previously **approved** submission. If your first submission was rejected, resubmit with `TipoEnvio="I"`. ### Value precision All monetary values must have exactly 2 decimal places. Use Reporter's formatting capabilities or ensure your data source provides pre-formatted values. ### Time metric sourcing Transaction time percentiles and DICT operation times must be sourced from your **infrastructure monitoring** — not from transactional data. These metrics reflect the actual user experience from payment initiation to settlement confirmation. BACEN may audit these values against SPI logs. ### JSONB nested fields Revenue calculations use nested field paths (e.g. `fee_charge.totalAmount`) to access values inside JSONB columns. Make sure your Reporter version supports nested field parsing in aggregation functions. Always validate the rendered XML against BACEN's official XSD before submission. You can download the schema from BACEN's regulatory documents portal. # APIX 002 Source: https://docs.lerian.studio/en/reporter/apix-002 On-demand Pix data submission to BACEN (Document 1202) — 12 months of transaction dimensions kept on hand, with format defined per request. Pix participants must deliver detailed transaction data to BACEN whenever the regulator requests it. Reporter helps you keep that data structured and ready for ad-hoc extraction. This guide covers the **APIX 002** report (Document 1202) — what to store, how to organize it, and how to respond when BACEN sends a request. Unlike APIX 001, APIX 002 has no fixed XML format or XSD schema. Each request defines its own format, dimensions, and deadline. The institution remains responsible for extracting accurate data and meeting the deadline set by the regulator. ## What is APIX 002? *** The **APIX 002** is an on-demand regulatory submission to the Brazilian Central Bank, defined by Normative Instruction BCB nº 32/2020. Unlike the monthly APIX 001, it is sent only when BACEN explicitly requests it, with the format specified per request. ### APIX 001 vs APIX 002 | Aspect | APIX 001 | APIX 002 | | ----------------- | ---------------------------------------- | -------------------- | | **Frequency** | Monthly (mandatory) | On-demand | | **Deadline** | 15th business day of the following month | Defined per request | | **Format** | XML (fixed XSD) | Defined per request | | **Layout / XSD** | Available (version 2.5) | Not applicable | | **Validation** | Pre + post submission | Post-submission only | | **Document code** | 1201 | 1202 | ### Who must report | Participant type | Must report? | | ------------------------------------------------------- | ------------------------------------------- | | Direct participant (transactional account provider) | Yes | | Special settler | Yes | | Indirect participant (settled via BTG, Itaú, or others) | No — consolidated by the direct participant | Indirect participants do not report APIX 002 directly. The direct participant consolidates the data on their behalf. However, indirect participants must still *keep the underlying data available* for the direct participant to aggregate when BACEN makes a request. ### Submission attributes | Field | Value | | ------------------ | ---------------------------------------------------------- | | Document name | On-demand Pix participant data | | Document code | 1202 | | STA code | APIX002 | | Frequency | Non-periodic (on-demand) | | Format | Defined per request | | Validation | Post-submission | | Autran transaction | SPIXP001 | | Reference window | Monthly data covering up to 12 months prior to the request | ## Why APIX 002 has no fixed format *** The normative explicitly defines the submission format as *"to be defined per demand"* ("Formato para Remessa: a definir para cada demanda"). This has four direct implications: 1. **No standard XSD** is published for APIX 002 — you cannot pre-build a single template. 2. **BACEN sets the format** at the moment of the request, including dimensions, granularity, and file structure. 3. **The institution extracts data** matching whatever dimensions are specified. 4. **The deadline is set per request**, with no standard turnaround time. To stay ready, your data store needs to support flexible queries across all dimensions BACEN may ask for. ## Data dimensions to store *** Participants must keep the data described in **Annex II of IN 32/2020** for at least 12 months. The data is organized across three groups: internally settled transactions, all transactions (internal and interbank), and cross-dimensional combinations. ### Internally settled transactions These dimensions apply to transactions where both payer and receiver belong to the same institution. | Field | Description | Values | | ------------------------ | --------------------------------------------------------- | ------------------------------------------------------------- | | **Scheduled** | Whether the transaction was scheduled | Yes / No | | **Suspected fraud** | Transactions analyzed past the fraud detection time limit | Yes / No | | **Not settled in time** | Transactions that exceeded 40 seconds | Yes / No | | **Rejected** | Whether the transaction was rejected | Yes / No | | **Rejection reason** | Per arts. 38 and 39 of Resolution BCB nº 1/2020 | Specific codes | | **Initiation procedure** | How the transaction was initiated | 1) Manual; 2) Pre-stored values | | **Initiation mechanism** | Mechanism used to start the transaction | 1) Pix key; 2) Dynamic QR; 3) Static QR; 4) Payment initiator | | **Payer nature** | Person type | 1) Natural person; 2) Legal person | | **Receiver nature** | Person type | 1) Natural person; 2) Legal person | | **Initiator** | ISPB of the payment initiator | ISPB code (8 digits) | ### All transactions (internal and interbank) These dimensions apply to the full transaction set, including interbank settlements via SPI. | Field | Description | Example | | ---------------------- | ----------------------------- | -------------------------------------------------------------- | | **Year** | Reference year | 2026 | | **Period** | Reference month | 01–12 | | **Transaction count** | Total count | 1,250,000 | | **Financial amount** | Sum of values | R\$ 985,000,000.00 | | **Initiation channel** | Where the transaction started | 1) App; 2) Internet banking; 3) Call center; 4) ATM; 5) Branch | ### Cross-dimensional queries BACEN may request combinations of multiple dimensions. Examples: * Count of transactions that are **scheduled** AND **suspected fraud** * Value of **rejected** transactions by **specific reason** AND **legal person** * Transactions by **initiation channel** AND **initiation mechanism** Structure your storage so any combination of these dimensions can be aggregated efficiently. A wide, denormalized table or a columnar store works better than highly normalized models for this type of regulatory query. ## Submission process *** ### System and transaction | Item | Value | | ------------------ | ------------------------------------------ | | System | STA (Sistema de Transferência de Arquivos) | | File code | APIX002 | | Autran transaction | SPIXP001 | | Document number | 1202 | ### Six-step flow 1. **Wait for the BACEN request** — there is no proactive submission. 2. **Review the format and deadline** specified by the regulator. 3. **Extract the data** matching the requested dimensions. 4. **Generate the file** in the specified format. 5. **Submit through STA** using the `APIX002` file code. 6. **BACEN runs post-submission validation** on the received data. There is no pre-submission validation for APIX 002. BACEN validates the data only after you submit it. Make sure your extraction is accurate before sending. ## Implementation recommendations *** ### Data model A flat, dimension-rich table simplifies on-demand extraction. The minimum recommended schema: ```sql theme={null} CREATE TABLE pix_transactions ( id UUID PRIMARY KEY, occurred_at TIMESTAMP NOT NULL, year SMALLINT NOT NULL, month SMALLINT NOT NULL, amount NUMERIC(18, 2) NOT NULL, settlement_type VARCHAR(16) NOT NULL, -- INTERNAL / SPI scheduled BOOLEAN NOT NULL DEFAULT FALSE, suspected_fraud BOOLEAN NOT NULL DEFAULT FALSE, not_settled_in_time BOOLEAN NOT NULL DEFAULT FALSE, rejected BOOLEAN NOT NULL DEFAULT FALSE, rejection_reason VARCHAR(8), initiation_procedure SMALLINT NOT NULL, -- 1=Manual, 2=Pre-stored initiation_mechanism SMALLINT NOT NULL, -- 1=Key, 2=Dynamic QR, 3=Static QR, 4=Initiator payer_nature SMALLINT NOT NULL, -- 1=Natural, 2=Legal receiver_nature SMALLINT NOT NULL, -- 1=Natural, 2=Legal initiation_channel SMALLINT NOT NULL, -- 1=App, 2=Web, 3=Call, 4=ATM, 5=Branch initiator_ispb VARCHAR(8), created_at TIMESTAMP NOT NULL DEFAULT NOW() ); ``` The `settlement_type` column distinguishes internally settled transactions (both parties at the same institution) from interbank transactions settled via SPI. This distinction matters because some dimensions only apply to internal settlements. ### Required capabilities * [ ] Store transaction data for at least 12 months prior to any request * [ ] Query by any single dimension or combination of dimensions * [ ] Aggregate count and financial amount over arbitrary periods * [ ] Export to multiple formats (CSV, XML, JSON) as BACEN may specify any * [ ] Generate cross-dimensional reports efficiently ## Regulatory references *** ### Normatives | Document | Description | | ------------------------ | ------------------------------------------------- | | IN BCB nº 32/2020 | Main normative establishing APIX 001 and APIX 002 | | IN BCB nº 174/2021 | Updates to Annexes I and III of IN 32 | | IN BCB nº 126/2021 | Earlier updates to IN 32 | | IN BCB nº 383/2023 | Updates effective from 2024-02-01 | | Resolution BCB nº 1/2020 | Pix regulation (arts. 38, 39, and 99) | ### Supporting documents | Document | Description | | --------------------------- | ------------------------------------------------------- | | APIX 002 instructions (PDF) | Guidance on preparing and submitting on-demand requests | | Pix Time Manual | Reference for Pix time limits | | BACEN submission portal | Instructions and XSD for APIX 001 | | STA system | File Transfer System access | | STA instructions | How to use the system | | Sisbacen / Autran | Transaction lookup | ### BACEN contacts | Topic | Email | | ------------------- | ----------------------------------------------------------------- | | Submission delivery | [suporte.ti@bcb.gov.br](mailto:suporte.ti@bcb.gov.br) | | Document content | [estatisticas.spb@bcb.gov.br](mailto:estatisticas.spb@bcb.gov.br) | This page is based on regulation in force as of January 2026. Always check the latest BACEN publications for current requirements. Unlike APIX 001 which can be fully automated with a Reporter template, APIX 002 requires manual intervention — you must wait for BACEN's specific request, understand the format they define, and build the extraction accordingly. Reporter can help generate the output file, but the template must be created per request. # CADOC 4010 and 4016 Source: https://docs.lerian.studio/en/reporter/cadoc-4010-and-4016 Generate CADOC 4010 and 4016 XML reports for BACEN with Reporter, following the COSIF standard, required regulatory layout, and validation rules. Reporter lets you generate XML-based reports that follow the official CADOC structure, exactly as required by the Brazilian Central Bank (BACEN). This guide walks you through the structure and logic used to generate **CADOC 4010** and **CADOC 4016** reports in XML. These reports follow the **COSIF** standard and must match the XML structure defined by BACEN. You can adapt the logic to your own data model, but the XML format must be respected. ## What are CADOC 4010 and 4016? *** The **4010** and **4016** are Analytical Balance Sheet reports used to submit financial reports to the **BACEN** (Central Bank of Brazil). ### What BACEN expects to receive * Closing balances by COSIF code * Report base date (reference month) * Institution's CNPJ (first 8 digits) * Submission type (`I` = Inclusion, `S` = Replacement) ### Difference between CADOC 4010 and 4016 | Document | Periodicity | Base Date | Expected Accounts | | -------- | -------------------- | --------- | ------------------------------------------ | | **4010** | Monthly | YYYY-MM | All accounts | | **4016** | Semiannual (Jun/Dec) | YYYY-MM | Without groups 7 and 8 (Revenues/Expenses) | The **4016** represents the entity's accounting position after the income statement closing. At this point, Revenue accounts (group 7) and Expense accounts (group 8) have already been closed and their balances transferred to Equity. ### Submission requirements | Document | Deadline | STA Code | | -------- | -------------------------------------------------------- | -------- | | **4010** | Day 18 of the following month (or the next business day) | ACOS010 | | **4016** | Last business day of the following month | ACOS016 | The STA code identifies the document type in BACEN’s transmission system. Use `ACOS010` when submitting CADOC 4010 and `ACOS016` when submitting CADOC 4016. ## Understanding the data structure *** ### Operation routes Operation routes work as accounting classifiers. Each route has: * **Unique identifier (`id`)**: Used internally to relate operations * **COSIF code (`code`)**: The 10-digit accounting code that will be reported to BACEN Think of routes as "accounting labels" attached to each operation, indicating which chart of accounts item that movement should be classified under. ### Operations Operations represent financial movements in the ledger. Each operation contains: * **Associated account (`account_id`)**: Which account was moved * **Route (`route`)**: ID of the applied accounting route/classification * **Balance after operation (`available_balance_after`)**: The account balance immediately after this operation * **Date and time (`created_at`)**: When the operation occurred ### Relationship between entities Relationship between the ledger entities and operations used to build the CADOC 4010 and 4016 reports The `balance` entity represents the **current** account balance, not the history. For regulatory reports that need balances from a specific period, use the `operation` entity with date filters. ## CADOC structure *** ### Base format The CADOC report **must** be an XML file and **must** follow the structure defined by **BACEN**: ```xml XML theme={null} ``` ### Mandatory fields **``** Always starts the file. It defines the XML version and encoding so the system knows how to read the content. **`` tag** Wraps the entire CADOC structure and includes: | Field | Description | Format | | ----------------- | ---------------------- | -------------------- | | `codigoDocumento` | Fixed identifier | `"4010"` or `"4016"` | | `cnpj` | First 8 digits of CNPJ | Numeric, 8 positions | | `dataBase` | Reference month | YYYY-MM | | `tipoRemessa` | Submission type | `"I"` or `"S"` | If your first submission was rejected due to errors, you still need to use `"I"` on your next attempt. Only use `"S"` for replacing previously approved data. **`` tag** Groups all account entries for the reporting period. **`` tag** * `codigoConta`: Account code, following COSIF format (10 numeric digits). * `saldo`: Account balance in decimal format (two decimal places). ## Template construction logic *** ### General structure The template follows a two-level aggregation logic: 1. **First level**: Iterate through all available operation routes 2. **Second level**: For each route, aggregate the balances of linked operations ### Iterating over routes The template must iterate through all registered operation routes. For each route: 1. **Check if it has a COSIF code**: Only routes with a filled code generate lines 2. **Filter operations**: Select operations that belong to that route 3. **Calculate the balance**: Aggregate the balances of filtered operations ### Variable naming caution When building the template, avoid using the same name for the iteration variable and the filter field: | Approach | Example | Result | | ------------- | --------------------------------------------- | --------------- | | **Incorrect** | `for route in ... if route == route.id` | Name conflict | | **Correct** | `for op_route in ... if route == op_route.id` | Works correctly | ## CADOC 4010 template *** Here's the complete template for generating CADOC 4010 in Reporter: ```xml tpl theme={null} {%- for op_route in midaz_transaction.operation_route %} {%- if op_route.code %} {%- endif %} {%- endfor %} ``` ### Line-by-line explanation **Line 1 - XML declaration** Standard XML header with UTF-8 encoding. **Line 2 - Root element ``** * `codigoDocumento="4010"`: Fixed identifier for the Analytical Balance Sheet * `cnpj`: Extracts the first 8 digits from the organization's legal document * `dataBase`: Generates the date in YYYY-MM format (reference month) * `tipoRemessa="I"`: Indicates data inclusion **Line 4 - Start of `for` loop** * `op_route`: Variable that receives each route during iteration * `midaz_transaction.operation_route`: Collection of all operation routes **Line 5 - `if` condition** Checks if the route has a filled COSIF code. **Line 6 - `` element** * `codigoConta`: Displays the current route's COSIF code * `saldo`: Uses the `sum_by` tag to aggregate balances, filtering operations whose route matches the current route's identifier (`op_route.id`) **Lines 7-8 - Block closing** Close the condition and loop respectively. ### Tags and filters used | Element | Type | Function | | ---------------------- | ---------- | --------------------------------------------- | | `{{ variable }}` | Expression | Displays a variable's value | | `{% tag %}` | Tag | Executes logic (loop, condition, aggregation) | | `\|slice:':8'` | Filter | Cuts the first 8 characters | | `for ... in` | Tag | Iterates through a collection | | `if` | Tag | Condition for execution | | `sum_by ... by ... if` | Tag | Sums values with conditional filter | | `date_time` | Tag | Generates formatted date | ## CADOC 4016 template *** The CADOC 4016 is the Analytical Balance Sheet, similar to 4010, but with semiannual periodicity and an important restriction: **it must not contain accounts from groups 7 (Revenues) and 8 (Expenses)**. ### Why exclude groups 7 and 8? The 4016 document represents the entity's accounting position **after the income statement closing**. At this point, Revenue accounts (group 7) and Expense accounts (group 8) have already been closed and their balances transferred to Equity. ### Template example ```xml tpl theme={null} {%- for op_route in midaz_transaction.operation_route %} {%- if op_route.code and op_route.code|slice:":1" != "7" and op_route.code|slice:":1" != "8" %} {%- endif %} {%- endfor %} ``` ### Difference from 4010 The only difference in the template is the additional condition in the `if`: | Template | Condition | | -------- | -------------------------------------------------------------------------------------------- | | **4010** | `if op_route.code` | | **4016** | `if op_route.code and op_route.code\|slice:":1" != "7" and op_route.code\|slice:":1" != "8"` | ### How the exclusion works The `slice:":1"` filter extracts the first character of the COSIF code: | COSIF Code | First Digit | Group | Included in 4016? | | ---------- | ----------- | ----------- | ----------------- | | 1000000009 | 1 | Assets | Yes | | 2100000003 | 2 | Liabilities | Yes | | 7100000001 | 7 | Revenues | **No** | | 8200000005 | 8 | Expenses | **No** | ## Generating the report with date filter *** ### Request example for 4010 To generate the CADOC 4010 for a specific month, send a `POST /v1/reports` request with the following body: ```json JSON theme={null} { "templateId": "CADOC_4010_TEMPLATE_ID", "filters": { "midaz_transaction": { "operation": { "created_at": { "between": ["2025-11-01T00:00:00Z", "2025-11-30T23:59:59Z"] } } } } } ``` ### Field explanation | Field | Description | | ------------------------------------- | ----------------------------------------------------------------- | | `templateId` | Identifier of the CADOC 4010 template registered in Reporter | | `filters.midaz_transaction.operation` | Indicates the filter will be applied to the operations collection | | `created_at.between` | Filters operations created within the specified interval | | `between[0]` | First day of the month at 00:00:00 UTC | | `between[1]` | Last day of the month at 23:59:59 UTC | **Date format**: ISO 8601 with UTC timezone (`Z`). Adjust the last day according to the month (28, 29, 30, or 31 days). ### Request example for 4016 To generate the CADOC 4016 for the first semester: ```json JSON theme={null} { "templateId": "CADOC_4016_TEMPLATE_ID", "filters": { "midaz_transaction": { "operation": { "created_at": { "between": ["2025-01-01T00:00:00Z", "2025-06-30T23:59:59Z"] } } } } } ``` For the second semester, adjust the dates to July through December. ## Template evolution and balance extraction *** We are working on evolving our main template to support balance aggregation in compliance with BACEN CADOC 4010/4016, which requires the final balance from the last business day of the month. While this feature is being developed, we provide an alternative version for extracting these balances using the following template. ### Extraction template This auxiliary template is designed to correctly calculate balances, ensuring compliance of your reports through the following steps: 1. Group operations by account 2. Sort entries by date and time 3. Select the last record from each account to obtain the final balance 4. Sum final balances by COSIF code With this option, you can extract the information required for CADOC 4010/4016 and transfer it to the layout format required by BACEN. This template can help if you already have a provider that builds CADOC files. ### Extraction template example ```xml tpl theme={null} account_id;account_alias;codigo_cosif;created_at;saldo_disponivel {%- for op_route in midaz_transaction.operation_route %} {%- if op_route.code %} {%- for op in filter(midaz_transaction.operation, "route", op_route.id) %} {{ op.account_id }};{{ op.account_alias }};{{ op_route.code }};{{ op.created_at }};{{ op.available_balance_after }} {%- endfor %} {%- endif %} {%- endfor %} ``` ### Extraction output The template exports all operations in CSV format, containing: * Account identifier * Account alias * COSIF code * Operation date and time * Available balance You can import this CSV into a spreadsheet or your existing reconciliation system to process the final balances. ## Comparison with CADOC 4111 *** CADOC 4010 and CADOC 4111 share the same template structure, with differences only in the parameters: | Aspect | CADOC 4111 | CADOC 4010 | | ----------------- | --------------- | --------------------- | | Document | Daily Balances | Monthly Balance Sheet | | Periodicity | Daily | Monthly | | `codigoDocumento` | `"4111"` | `"4010"` | | `dataBase` | YYYY-MM | YYYY-MM | | Date filter | 1 day | 1 month | | Expected balance | Last of the day | Last of the month | **Identical template**: The logic for iterating over routes and aggregating balances is the same. ## Best practices for building templates *** ### Variable naming Use descriptive and unique names for iteration variables, avoiding conflicts with entity field names. ### Field validation Always check if optional fields have values before using them. Empty fields can generate unwanted lines in the report. ### Date format BACEN requires dates in YYYY-MM format for 4010. Make sure to configure the format correctly in the template. ### CNPJ handling The CNPJ must be presented with only the first 8 digits, without formatting (dots, slashes, or dashes). ### COSIF account code The COSIF chart of accounts is structured with 6 hierarchical levels and a check digit. The `codigoConta` field must have 10 numeric digits, without dots or dashes. ## Component summary *** | Component | Data Source | Usage in Template | | ---------- | ----------------------------- | ---------------------- | | CNPJ | Organization (Onboarding) | Document header | | COSIF Code | Operation Route (Transaction) | Account identifier | | Balance | Operation (Transaction) | Value to be aggregated | | Base Date | Current date function | Document header | Always validate the rendered XML against BACEN's schema before submitting. The structure alone isn't enough — the data must reflect your institution's actual ledger. # CADOC 4111 Source: https://docs.lerian.studio/en/reporter/cadoc-4111 Generate the CADOC 4111 XML report for BACEN with Reporter — daily account balances grouped by COSIF code, in the required regulatory layout. Reporter lets you generate XML-based reports that follow the official CADOC structure, exactly as required by the Brazilian Central Bank (BACEN). This guide walks you through the structure and logic used to generate the **CADOC 4111** report in XML. These reports follow the **COSIF** standard and must match the XML structure defined by BACEN. You can adapt the logic to your own data model, but the XML format must be respected. ## What is CADOC 4111? *** The **CADOC 4111** is a regulatory document required by the Brazilian Central Bank (BACEN) that reports daily balances of accounting accounts grouped by COSIF code (Chart of Accounts for Institutions of the National Financial System). ### What BACEN expects to receive * Consolidated balances by COSIF code * Report base date * Institution's CNPJ (first 8 digits) * Submission type (`I` = Inclusion, `S` = Replacement) ### Submission requirements | Document | Deadline | STA Code | | -------- | ------------------------------------------------------- | -------- | | **4111** | Day following the reference date (or next business day) | ACOS011 | The STA code identifies the document type in BACEN's transmission system. Use `ACOS011` when submitting CADOC 4111. ## Understanding the data structure *** Before building the template, it's essential to understand how data is organized in the Midaz ledger. ### Operation routes Operation routes function as accounting classifiers. Each route has: * **Unique identifier (`id`)**: Used internally to relate operations * **COSIF code (`code`)**: The 10-digit accounting code that will be reported to BACEN When an operation is recorded in Midaz, it's associated with a route. This route carries the corresponding COSIF code. Think of routes as "accounting labels" attached to each operation, indicating which chart of accounts category that movement should be classified under. ### Operations Operations represent financial movements in the ledger. Each operation contains: * **Associated account (`account_id`)**: Which account was affected * **Route (`route`)**: ID of the applied accounting route/classification * **Balance after operation (`available_balance_after`)**: The account balance immediately after this operation * **Date and time (`created_at`)**: When the operation occurred The balance after operation field represents the accumulated account balance at that moment, not the value of the operation itself. ### Relationship between routes and operations Relationship between accounting routes and operations used to build the CADOC 4111 report Each route can have multiple operations linked to it throughout the day. ## CADOC structure *** ### Base format The CADOC report **must** be an XML file and **must** follow the structure defined by **BACEN**: ```xml XML theme={null} ``` ### Mandatory fields These fields are required and must be included: **``** Always starts the file. It defines the XML version and encoding so the system knows how to read the content. **`` tag** Wraps the entire CADOC structure and includes: | Field | Description | Format | | ----------------- | ---------------------- | -------------------- | | `codigoDocumento` | Fixed identifier | `"4111"` | | `cnpj` | First 8 digits of CNPJ | Numeric, 8 positions | | `dataBase` | Reference date | YYYY-MM | | `tipoRemessa` | Submission type | `"I"` or `"S"` | If your first submission was rejected due to errors, you still need to use `"I"` on your next attempt. Only use `"S"` for replacing previously approved data. **`` tag** Groups all account entries for the reporting period. **`` tag** * `codigoConta`: COSIF code from the operation route (10 numeric digits) * `saldoDia`: Consolidated balance in decimal format (two decimal places) ## Template construction logic *** ### General structure The template follows an aggregation logic at two levels: 1. **First level**: Iterate through all available operation routes 2. **Second level**: For each route, sum the balances of linked operations ### Document header The XML header must contain: * **Document code**: Fixed identifier `4111` * **CNPJ**: Extracted from organization data, limited to first 8 digits * **Base date**: Report generation date in `YYYY-MM` format * **Submission type**: `I` for inclusion Organization data is obtained from the Midaz onboarding data source, specifically from the organization entity. ### Iterating over routes The template must iterate through all registered operation routes. For each route: 1. **Check for COSIF code**: Only routes with a valid COSIF code should generate lines in the report 2. **Filter operations**: Select only operations belonging to that specific route 3. **Calculate balance**: Sum the balances of filtered operations Not all routes have a COSIF code filled in. Routes without a code are used for internal controls and should not appear in the regulatory report. ### Filtering operations by route The association between operations and routes is made through the route identifier. The template uses this identifier to: 1. Access a specific route 2. Find all operations that reference this route 3. Process only these operations in the aggregation When building the template, avoid using the same name for the iteration variable and the filter field, as this can cause conflicts in template interpretation. ### Summing balances For each set of operations from a route, the template sums the available balances. The field used is the available balance after each operation. The aggregation function iterates through all operations that meet the filter criteria (same route) and accumulates the balance field values. ## Using Reporter *** Here's the complete template for generating CADOC 4111 in Reporter: ```tpl Template theme={null} {%- for op_route in midaz_transaction.operation_route %} {%- if op_route.code %} {%- endif %} {%- endfor %} ``` ## Code breakdown *** ### Line 1 - XML declaration ```xml theme={null} ``` Standard XML header with UTF-8 encoding. ### Line 2 - Root element `` ```tpl theme={null} ``` * `codigoDocumento="4111"`: Fixed document type identifier * `cnpj`: Accesses the organization's legal document and extracts the first 8 characters using the `slice` filter * `dataBase`: Generates the current date in BACEN's required format using the `date_time` tag * `tipoRemessa="I"`: Indicates data inclusion ### Line 4 - Start of `for` loop ```tpl theme={null} {%- for op_route in midaz_transaction.operation_route %} ``` * `op_route`: Variable that receives each route during iteration (name chosen to avoid conflict with the `route` field in operations) * `midaz_transaction.operation_route`: Collection of all operation routes ### Line 5 - `if` condition ```tpl theme={null} {%- if op_route.code %} ``` Checks if the route has a COSIF code filled in. Routes without a code are ignored. ### Line 6 - `` element ```tpl theme={null} ``` * `codigoConta`: Displays the current route's COSIF code * `saldoDia`: Uses the `sum_by` tag to sum balances, filtering only operations whose route matches the current route identifier (`op_route.id`) ### Lines 7-8 - Closing blocks ```tpl theme={null} {%- endif %} {%- endfor %} ``` Close the condition and loop respectively. ### Tags and filters reference | Element | Type | Function | | ---------------------- | ---------- | --------------------------------------------- | | `{{ variable }}` | Expression | Displays a variable's value | | `{% tag %}` | Tag | Executes logic (loop, condition, aggregation) | | `\|slice:":8"` | Filter | Extracts the first 8 characters | | `for ... in` | Tag | Iterates through a collection | | `if` | Tag | Execution condition | | `sum_by ... by ... if` | Tag | Sums values with conditional filter | | `date_time` | Tag | Generates formatted date | ## Date filter considerations *** ### Request example with date filter To generate CADOC 4111 for a specific day, send a `POST /v1/reports` request with the following body: ```json Request theme={null} { "templateId": "CADOC_4111_TEMPLATE_ID", "filters": { "midaz_transaction": { "operation": { "created_at": { "between": ["2025-11-11T00:00:00Z", "2025-11-11T23:59:59Z"] } } } } } ``` ### Field explanation | Field | Description | | ------------------------------------- | ----------------------------------------------------------------- | | `templateId` | CADOC 4111 template identifier registered in Reporter | | `filters.midaz_transaction.operation` | Indicates the filter will be applied to the operations collection | | `created_at.between` | Filters operations created within the specified interval | | `between[0]` | Start date and time (midnight of desired day) | | `between[1]` | End date and time (last second of desired day) | Dates must be in ISO 8601 format with UTC timezone (`Z`). ## Template evolution and balance extraction *** We are working on evolving our main template to support balance aggregation in compliance with BACEN CADOC 4111, which requires the final balance from the last business day. While this feature is being developed, we provide an alternative version for extracting these balances using the following template. ### Extraction template This auxiliary template is designed to correctly calculate balances, ensuring compliance of your reports through the following steps: 1. Group operations by account 2. Sort entries by date and time 3. Select the last record from each account to obtain the final balance 4. Sum final balances by COSIF code With this option, you can extract the information required for CADOC 4111 and transfer it to the layout format required by BACEN. This template can help if you already have a provider that builds CADOC files. ### Extraction template example ```tpl CSV Extraction theme={null} account_id;account_alias;codigo_cosif;created_at;saldo_disponivel {%- for op_route in midaz_transaction.operation_route %} {%- if op_route.code %} {%- for op in filter(midaz_transaction.operation, "route", op_route.id) %} {{ op.account_id }};{{ op.account_alias }};{{ op_route.code }};{{ op.created_at }};{{ op.available_balance_after }} {%- endfor %} {%- endif %} {%- endfor %} ``` ### Extraction output The template exports all operations in CSV format, containing: * Account identifier * Account alias * COSIF code * Operation date and time * Available balance You can import this CSV into a spreadsheet or your existing reconciliation system to process the final balances. ## Best practices for template construction *** ### Variable naming Use descriptive and unique names for iteration variables, avoiding conflicts with entity field names. ### Field validation Always check if optional fields have values before using them. Empty fields can generate unwanted lines in the report. ### Date format BACEN requires dates in `YYYY-MM` format. Make sure to configure the format correctly in the template. ### CNPJ handling The CNPJ must be presented with only the first 8 digits, without formatting (dots, slashes, or dashes). ## Component summary *** | Component | Data Source | Usage in Template | | ---------- | ----------------------------- | ---------------------- | | CNPJ | Organization (Onboarding) | Document header | | COSIF Code | Operation Route (Transaction) | Account identifier | | Balance | Operation (Transaction) | Value to be aggregated | | Base Date | Current date function | Document header | Always validate the rendered XML against BACEN's schema before submitting. The structure alone isn't enough — the data must reflect your institution's actual ledger. # CCS Reports Source: https://docs.lerian.studio/en/reporter/ccs-reports Build CCS XML files (ACCS001, ACCS005, ACCS010) for BACEN by combining Lerian CRM and Midaz data through Reporter templates and scheduled generation. Reporter enables you to generate XML-based CCS (Cadastro de Clientes do Sistema Financeiro Nacional) files as defined by the Central Bank of Brazil (BACEN). This guide shows how to create automated CCS reports using Reporter — including ACCS001, ACCS005, and ACCS010 — by combining CRM data (holders and aliases) with Midaz structures. CCS reports must strictly follow the layouts defined by BACEN. Reporter lets you automate the XML generation, but you remain responsible for validating the output and ensuring compliance with regulatory requirements. ## What is CCS? *** The **CCS (Cadastro de Clientes do Sistema Financeiro Nacional)** is a registry that maps **relationships between customers and financial institutions**. Its purpose is to allow regulators and other authorized entities to identify **where a customer holds financial relationships**. Institutions must submit CCS reports in XML format, following BACEN’s ACCS schemas. Each file type has a specific function — from daily updates to relationship transfers. Reporter makes this process automated and standardized, ensuring that CCS submissions are both **technically valid** and **business-aligned**. ## ACCS001 — Daily update file *** ### What it is The ACCS001 is the **daily update file**. It reports inclusions, changes, or exclusions of customer relationships (individual or corporate) with the institution. This is the most frequent CCS file, keeping the registry in sync with daily operations. ### Full template ``` 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), enabling accurate association between account data and their respective owners. * The `` field is conditional: * F for **natural persons** * J for **legal entities** * **Fixed values in this example:** * `` = I (Inclusion) Although set to I in this scenario, this field must reflect the actual operation being reported.\ Valid values according to BACEN are: * I = **Inclusion** (new accounts in the institution's database) * A = **Update** (changes to existing account information) * E = **Exclusion** (the original record must be disregarded) * `` = N (Normal) Similarly, this field should represent the correct qualifier for the account.\ Valid values include: * N = **Normal** * P = **Popular** * C = **Conglomerate** * **Functions used:** * count\_by plugin\_crm.aliases counts alias records per account. * date\_time "YYYY-MM-dd" dynamically inserts the current date in the specified format. ### Rendered example ``` 12345678 00000000 ACCS001 12233444 I N F 12345678900 2025-02-01 1 2025-02-01 ``` ## ACCS005 — Relationship detail file *** ### What it is The ACCS005 is generated when BACEN requests **detailed information on client relationships**. It lists all the links between a customer and financial assets (BDVs — Bens, Direitos e Valores), and may include **related persons** such as partners or legal representatives. ### Full template ``` 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 The logic applies nested loops to iterate through **accounts**, **aliases**, and **holders**, enabling structured extraction of all relevant entities. * The `` field uses conditional logic to determine the account type: * 1 = deposit * 2 = savings * 3 = payment * 6 = others * For CNPJ extraction, slice:"0:8" is applied to retrieve only the first 8 digits (base of the CNPJ). * The `` tag is rendered only when the account holder is a natural person **and** has associated individuals (e.g., an attorney or legal representative). To illustrate this, the example bellow includes the **mother’s name** as a placeholder for a vinculated person — not because that tag refers to the mother per se, but to demonstrate the feature in action. * The `date_time` function dynamically generates timestamps at runtime, ensuring the report reflects the current execution context. ### Rendered example ``` 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 The ACCS010 is generated when **client relationships are transferred** between institutions — common in portfolio sales or mergers. It lists the relationships being transferred and identifies the **new responsible institution**. ### Full template ``` 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 ``` While Reporter provides the technical automation, institutions must ensure the **accuracy and consistency of the data** submitted to BACEN. Always validate rendered XML against the official schema before submission, and involve accounting or compliance teams in the review process. # Connect Reporter to Midaz Source: https://docs.lerian.studio/en/reporter/connecting-reporter-to-midaz Connect Reporter to Midaz through a named data source, protect the source database with least-privilege credentials, and verify the connection end to end. Reporter reads Midaz data through a named **data source**. A report template references that name, and Reporter turns its placeholders and filters into queries against the source database. For example, a template can reference an account from the `midaz_onboarding` data source: ```text theme={null} {{ midaz_onboarding.account.name }} ``` Reporter builds `SELECT` queries for PostgreSQL sources and `Find` operations for MongoDB sources. It then renders the returned data through the template. Reporter does not write back to the source databases or persist report results in Midaz. ## Before you start *** You need: * Network access from Reporter to the Midaz database. * A database user with read-only permissions. * Preferably, a read replica so report workloads do not compete with ledger workloads. The reference setup uses the host `midaz-postgres-replica` as an example. * The database, schemas, and tables that your templates need. Reporter generates read operations, but this behavior does not enforce database permissions. Use read-only database credentials. Do not give Reporter a user that can insert, update, delete, or change schemas. ## Choose who manages the connection *** Your deployment model determines who configures the data source: * **Managed (Lerian-hosted):** the connection to your Lerian product data is set up during onboarding. Confirm the data source name and availability with your Lerian contact before you build templates. * **Self-hosted:** your operator configures the connection at deployment time and owns its network access, credentials, certificates, and database permissions. In both models, report authors reference the configured data source by its `configName`. ## Configure a named data source *** In a self-hosted deployment, define the connection with `DATASOURCE__*` environment variables. Set `DATASOURCE__CONFIG_NAME` to the stable name that templates and filters will use. For example, if the `configName` is `midaz_onboarding`, templates reference `midaz_onboarding` even if the database host or credentials change later. For PostgreSQL sources that expose more than one schema, derive the schema variable from the `configName`. A `configName` of `external_db` uses: ```env theme={null} DATASOURCE_EXTERNAL_DB_SCHEMAS=sales,inventory,reporting ``` See [Configure external data sources](/en/reporter/using-reporter#configure-external-data-sources) for the connection variables and complete examples. ## Protect the Midaz database *** Use a dedicated database role that can connect and select only the required tables and schemas. Point Reporter to a read replica when your topology supports one. This keeps report queries away from the primary database, but the replica is not a substitute for read-only credentials: apply both controls. Rotate the credential through your normal secret-management process. Keep the `configName` stable so existing templates continue to resolve the same data source. ## Verify the connection *** 1. [List data sources](/en/reference/reporter/list-data-sources) and confirm that the expected `configName` appears. 2. Copy the data source ID from the response. 3. [Retrieve the data source](/en/reference/reporter/retrieve-data-source) and confirm that Reporter discovers the expected schemas, tables, and fields. 4. Create a small template that references one known field, then generate a report to verify the complete query and rendering path. The connection is ready when the named source is available, its discovered structure is correct, and the test report returns the expected value. ## Troubleshooting *** ### The data source is unavailable Check network routing, DNS, TLS settings, credentials, database permissions, and replica health. During template validation, Reporter can return a `DATA_SOURCE_UNAVAILABLE` warning instead of blocking template creation. Fix the connection before relying on the report output. ### The source name is ambiguous Use one descriptive `configName` per database role, such as `midaz_onboarding` and `midaz_transaction`. Avoid generic names such as `database` or `postgres`, especially when different sources contain tables with the same names. Update the template to reference the intended source explicitly. ## Next steps *** * [Create your first report](/en/reporter/reporter-quick-start) * [Build templates and filters](/en/reporter/using-reporter) * [See Reporter in a complete core banking architecture](/en/building-a-complete-core-banking) # Add a Data Source Source: https://docs.lerian.studio/en/reporter/console/adding-a-data-source Configure an external database connection when Reporter needs a data source that is not already available in the Lerian Console for report generation. Use this page to create a Data Source when Reporter needs to read from an external database or from a connection that is not already configured. If the report uses internal Lerian product data, such as Midaz or CRM, the Data Source may already exist. ## Objective *** Configure an external connection that Templates and Reports can use. For example, create a Data Source named `production-postgres` that points to an external reporting database, or `compliance-readonly` for a read-only database user used by reporting operators. ## When to use *** Use this task when: * the required Data Source is not already available in Reporter; * a new database must feed reports; * an existing report process needs a separate read-only connection; * operators need schemas and fields available in Template Builder or report filters. Before adding one, check whether the required internal Data Source already exists. Creating a duplicate external connection is unnecessary when Reporter already has the source configured. Do not use this task to fix an existing connection. Use [Edit a Data Source](/en/reporter/console/editing-a-data-source) and [Test a Data Source Connection](/en/reporter/console/testing-a-data-source). ## Before you start *** Have these details ready: * host, port, and database name; * username and password; * schema names, when the database uses schemas; * SSL requirements and CA certificate, when required by infrastructure; * a clear connection name, such as `production-postgres` or `compliance-readonly`. Use credentials intended for reporting access. A Data Source gives Reporter access to read data used in Templates and Reports. ## Step by step *** Go to **Data Sources** and click **Add Data Source**. In **Add Data Source**, enter **Connection Name**, **Database Type**, **Host**, **Port**, **Database Name**, **Username**, and **Password**. For database types where the Console shows **Schemas** (currently PostgreSQL in the selector), add the schemas that Reporter should inspect. Type a schema name and click **Add**, or press **Enter**. Expand **SSL Configuration**, turn on **Enable SSL**, select **SSL Mode**, and paste the **CA Certificate** if your infrastructure requires it. Expand **Metadata** to add key-value fields that help operators classify the connection. Click **Create**. The new Data Source appears with **Disconnected** status. Run **Test Connection** before using it in Templates or Reports. ## Field guide *** | Field | What to enter | Example | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | **Connection Name** | Required operator-facing name. Technical field: `configName`. Console validation: 3 to 100 characters. | `production-postgres` | | **Database Type** | Required database engine. Console currently offers `POSTGRESQL` and `MONGODB`. Technical field: `type`. | `POSTGRESQL` | | **Host** | Required database server address, not an application URL. Technical field: `host`. | `db.example.com` | | **Port** | Required database port. Technical field: `port`; valid range is 1 to 65535. | `5432` | | **Database Name** | Required database name Reporter will query. Technical field: `databaseName`. | `ledger` | | **Username** | Required database user. Technical field: `username`. | `reporter_readonly` | | **Password** | Required secret when creating a Data Source. Technical field: `password`. | Stored through the form | | **Schemas** | Optional schema list for supported database types. Stored as `metadata.schemas`; entries are trimmed, lowercased, and deduplicated. | `public`, `pix`, `transactions` | | **Enable SSL** | Optional SSL toggle. When enabled, the connection payload includes `ssl`. | Enabled | | **SSL Mode** | Required when SSL is enabled. Use `disable`, `require`, `verify-ca`, or `verify-full`. Technical field: `ssl.mode`. | `verify-ca` | | **CA Certificate** | Required for `verify-ca` and `verify-full`. Technical field: `ssl.ca`. | PEM CA certificate | | **Metadata** key/value fields | Optional operator classification data. Only rows with a nonblank key are submitted. Technical field: `metadata`. | `environment = production` | ## Expected result *** The new external Data Source appears on the **Data Sources** page with **Disconnected** status until tested. After a successful test, operators can use its fields in Templates and Reports. ## Common errors and care points *** **Host** should be the database server address, such as `db.example.com`, not the URL of an application or admin panel. If the database uses schemas and the required schema is not listed, operators may not see the expected tables and fields. If infrastructure requires SSL or a CA certificate, the connection can fail until **SSL Configuration** matches that requirement. Names like `db1` are hard to use safely. Prefer names that show environment and purpose, such as `production-compliance-readonly`. ## Next steps *** * Use [Test a Data Source Connection](/en/reporter/console/testing-a-data-source) immediately after creation. * Use [Template Builder](/en/reporter/console/template-builder) to use fields from the Data Source in a Template. * Use [Generate a Report](/en/reporter/console/generating-a-report) after the Template and Data Source are ready. [Create connection endpoint](/en/reference/reporter/connections/create-connection) # Add a Template Source: https://docs.lerian.studio/en/reporter/console/adding-template Create a Template in Reporter using Template Builder or by uploading a prepared .tpl file, then map its data sources for regulatory report generation. Use this page to create the Template that Reporter will use to generate a Report. A Template is the reusable structure of the output file: it defines the layout, fields, conditions, loops, calculations, and final format. ## Objective *** Add a Template that operators can select when generating Reports. For example, a monthly tax Template can define a CSV header, one row per transaction, and a total amount at the end of the file. A regulatory XML Template can define the required XML tags and fill them with values from a Data Source. ## When to use *** Use this task when: * you are setting up the first Template for a report whose Data Source already exists; * a new report obligation requires a new output structure; * your team already has a reviewed `.tpl` file and needs to make it available in the Console; * an operator needs to build the Template visually instead of editing `.tpl` code. Do not use this task to fix a Template that already exists. Use [Update a Template](/en/reporter/console/updating-a-template) instead. ## Before you start *** Confirm: * the Data Source that will feed the Template is available when the Template needs database fields. It may be an internal source already configured for the environment or an external source added from **Data Sources**; * the expected output format is known: CSV, XML, HTML, TXT, or PDF; * the Template will be created in **Template Builder** or uploaded as a `.tpl` file; * the report owner has confirmed the fields that must appear in the output. A Template is not a generated Report. It is the reusable definition that Reporter applies later during report generation. ## Option 1: Build in Template Builder *** Use Template Builder when an operator needs to assemble the Template visually from blocks. Go to the **Templates** page and click **Template Builder**. Use a name operators can recognize later, such as `Monthly tax CSV` or `Daily transaction PDF`. Select the format that the generated Report must produce: XML, HTML, CSV, TXT, or PDF. Add blocks such as **Text**, **Variable**, **Loop**, **Conditional**, or **Aggregation**. In practice, blocks represent the pieces of the final file: fixed text, database values, repeated rows, conditional sections, or totals. Use the Data Sources sidebar to select fields from the configured database connection. For example, use a **Variable** block to place `transactions.amount` in the output. Click **Save** to validate the blocks and save the generated `.tpl` file. ## Option 2: Upload a `.tpl` file *** Use upload when a technical team already maintains the Template outside the Console. Go to the **Templates** page and click **New Template**. In the **File** step, drag the `.tpl` file to **Drag file here**, or click **to select** to browse your computer. In the **Config** step, fill in **Template Name** and **Output Format**. In the **Review** step, confirm the file, name, and output format. Use the edit buttons if any field is wrong. Click **Save Template**. **API equivalent**: [Upload template endpoint](/en/reference/reporter/upload-template) ## Field guide *** ### Upload wizard fields | Field | What to enter | Example | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | **Template File (.tpl)** | Required `.tpl` file used as the Template source. It must be larger than 0 bytes and no larger than 5 MB. API field: `template`. | `monthly-tax.tpl` | | **Template Name** | Required operator-facing name. It maps to `name` in the Console and to `description` in the Reporter API. | `Monthly tax CSV` | | **Output Format** | Required output type. Use one of `csv`, `xml`, `html`, `txt`, or `pdf`. | `csv` | ### Template Builder fields | Field | What to enter | Example | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | **Template name** | Required name in the builder header. It becomes the saved Template name and generated `.tpl` filename prefix. | `Daily transaction PDF` | | Block toolbar | Add at least one block before saving. Blocks become the generated `.tpl` code. | **Text** header + **Loop** over transactions + **Aggregation** total | | Data Sources sidebar | Use configured Data Sources to insert fields into **Variable** blocks. Technical fields include `dataSource`, `table`, and `field`. | `production-postgres.transactions.amount` | ## Expected result *** After saving, the Template appears on the **Templates** page and can be selected in the **Generate Report** wizard. ## Common errors and care points *** Upload only accepts `.tpl` files. If the Template is still being designed, use **Template Builder** first or prepare the `.tpl` file outside the Console. The output format controls what kind of file operators will download after generating a Report. Confirm the required format before saving. Templates can reference fields from Data Sources. If the required source is missing or untested, operators may not know which fields are safe to use. ## Next steps *** * Use [Generate a Report](/en/reporter/console/generating-a-report) to create a Report from the Template. * Use [Update a Template](/en/reporter/console/updating-a-template) if the structure, file, name, or output format needs correction. * Use [Template Builder](/en/reporter/console/template-builder) when operators need to maintain the Template visually. # BC Correios in Lerian Console Source: https://docs.lerian.studio/en/reporter/console/bc-correios Operate the BC Correios provider in the Reporter Module — receive, send, search, and audit regulatory mail from the Banco Central do Brasil (BCB). **BC Correios** is a provider in the [Reporter Module](/en/reporter/console/reporter-module) that automates regulatory communication with the **Banco Central do Brasil (BCB)** through its electronic mail system, *BC Correio*. Institutions supervised by the BCB must monitor this mailbox daily for official directives, circulars, and resolutions. BC Correios brings that workflow into Lerian Console: it receives incoming mail, lets you transmit replies and new communications to the BCB, and keeps an immutable record for compliance evidence — without anyone needing to handle the BCB's underlying SOAP/XML protocol. BC Correios is only available if your organization has acquired the provider and it is enabled for your environment. If you don't see it, [get in touch with our team](https://lerian.studio/contact). ## What BC Correios covers *** Once enabled, BC Correios adds a section to the Reporter sidebar and covers the following areas: * **Dashboard** — KPIs for received and pending mail, a chart of mail categories, sync status, and recent activity. * **Inbox** — searchable, filterable list of incoming mail, with a detail view that shows attachments and an optional **AI analysis** panel. * **New message** — send a new communication to the BCB (destination, subject, message, and keywords) via the BCB `TransmitirCorreio` operation. * **Sent** — communications transmitted to the BCB. * **Cancelled** — mail in a cancelled state. This folder is **read-only** because the BCB reports the state upstream. * **Archived** — mail you have archived locally for organization. * **Audit Trail** — an immutable audit trail of events, with CSV export for compliance evidence. * **Settings** — BCB credentials, sync interval, AI provider, webhook, and retention configuration. Archiving and restoring are **local-only** organizational actions — they update the BC Correios database and never change anything on the BCB side. Regulatory mail is retained to meet the minimum retention requirement; there is no user-facing delete. ## Mail types *** BC Correios categorizes incoming mail to help you triage regulatory communications: | Type | Meaning | | ------------- | ----------------------------------------------------------- | | Circular | A BCB circular. | | Resolution | A BCB resolution. | | Communication | A general BCB communication. | | Other | Any communication that does not match the categories above. | ## How availability is controlled *** BC Correios has two levels of activation: ### 1. Infrastructure flag (set by your DevOps/deploy team) The flag `NEXT_PUBLIC_BC_CORREIOS_ENABLED` controls whether BC Correios is visible at all in the Console. It is **disabled by default** and must be set at the Console container level — via Helm values or environment variables — before anything appears in the UI. This is a **deploy-time setting**, not something changed inside the Console itself. See [Lerian Console via Helm](/en/platform/helm/console/console-helm) for where to set it. Because the flag is read at runtime, toggling it requires restarting the Console container — no rebuild needed. When the flag is off, all BC Correios pages are hidden. ### 2. Organization toggle (set by a Console admin) Once the infrastructure flag is `true`, a **BC Correios card** appears in **Reporter → Settings → Plugins**. An administrator can use the toggle on that card to enable or disable BC Correios for the organization without touching the deployment. ### Where BC Correios appears When both levels are active, BC Correios shows up as a collapsible section under **PLUGINS** in the Reporter sidebar. The sidebar labels the items **Dashboard**, **Caixa de Entrada**, **Enviados**, **Arquivados**, **Cancelados**, **Trilha de Auditoria**, and **Configurações**. If you expect BC Correios but do not see it in the Reporter sidebar, check in order: (1) confirm the infrastructure flag is `true` for your environment; (2) confirm the plugin is toggled on in Reporter → Settings → Plugins. If both are set and it still does not appear, contact the Lerian team. ## Console vs API *** Use the **Console** when an operator needs to read, send, search, and audit BCB mail through the visual interface. Use the BCB integration's **API** when a system needs to automate reception or transmission programmatically. The same concepts apply in both places — the Console abstracts the BCB's SOAP/XML mailbox behind a clean operator workflow. # Audit Trail Source: https://docs.lerian.studio/en/reporter/console/bc-correios-audit Review the immutable chronological record of every action performed on BCB mail, and export it as CSV for compliance evidence and BACEN audit reviews. The **Audit Trail** is a chronological, immutable record of every action performed on Banco Central do Brasil (BCB) mail in BC Correios. It provides traceability and regulatory compliance evidence, and it can be exported as CSV. ## Accessing the Audit Trail *** Open **Reporter** > **BC Correios** > **Trilha de Auditoria** from the left-side menu. The screen is headed **Audit Trail**. The Audit Trail requires the `audit:get` permission, which also covers CSV export. Without it, you see a "not authorized" page. Audit events are retained for 5 years. ## Reading the audit table *** Each row records one event: | Column | Description | | :------------ | :------------------------------------------------------------------- | | **Action** | The type of event (see the list below). | | **User** | The user who performed the action, or `System` for automated events. | | **Mail** | The related mail, when applicable. Click to open its detail view. | | **Date/Time** | When the event occurred. | ### Tracked actions | Action | When it is recorded | | :---------------------- | :----------------------------------------- | | `Mail received` | New mail arrives from the BCB. | | `Viewed mail` | A mail is opened. | | `Archived mail` | A mail is archived locally. | | `Restored mail` | An archived mail is restored. | | `Mail cancelled` | The BCB reports a mail as cancelled. | | `Sent mail` | A communication is transmitted to the BCB. | | `Downloaded attachment` | An attachment is downloaded. | | `Performed search` | An operator runs a search. | | `Updated settings` | Configuration is changed. | | `Tested BCB connection` | A connection test is run. | ## Filtering events *** Use the filter bar to focus the trail: * **From date** and **To date** — restrict to a date range. The default range is the last 7 days. * **All actions** — the default; pick one action type to show only that one. * **Items per page** — control how many rows are shown. ## Exporting to CSV *** Click **Export CSV** to download the currently filtered events. The export honors the active action and date-range filters, so you can produce a targeted compliance report. Export after setting a date range and action filter to generate evidence for a specific event type or reporting period. # Composing a message Source: https://docs.lerian.studio/en/reporter/console/bc-correios-compose Send a new communication or reply to the Banco Central do Brasil through the BCB TransmitirCorreio operation, with attachments and metadata support. The **New message** page lets you transmit a new communication — or a reply — to the Banco Central do Brasil (BCB). Behind the scenes, BC Correios sends the message through the BCB `TransmitirCorreio` operation, so you never need to handle the underlying SOAP/XML protocol. ## Accessing New message *** Open **New message** from the [Inbox](/en/reporter/console/bc-correios-inbox) by clicking the **New message** button, or from **Reply** on a mail detail page. The page requires the `mails:post` permission. With read-only access, you see the form with a read-only banner and a disabled **Send** button. ## Message fields *** | Field | Required | Description | | :-------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Subject** | Yes | The subject line of the communication. | | **Destination** | No | The destination unit at the BCB. Leave blank to use the tenant default. Common units are suggested as you type (for example DEINF, SECRE, GABIN, DEPIN, DESIG, DEORF), but the field accepts free text. | | **Message** | Yes | The message body (up to 10,000 characters). A live character counter is shown. | | **Keywords** | No | Comma-separated tags to classify the message. Maximum of 10 keywords, 100 characters each. | ## Replying to a message *** When you start **New message** from a mail's **Reply** action, the subject is pre-filled with `Re: ` and the page shows which message you are replying to. The reply is linked to the original mail so the conversation stays traceable. ## Sending *** 1. Fill in at least the **Subject** and **Message**. 2. Optionally set a **Destination** and **Keywords**. 3. Click **Send**. On success, BC Correios confirms the transmission and returns you to the Inbox. The sent communication then appears in the [Sent](/en/reporter/console/bc-correios-folders) folder. If you navigate away with unsaved changes, BC Correios asks you to confirm before discarding the draft. Every transmission is recorded in the [Audit Trail](/en/reporter/console/bc-correios-audit) as a `Sent mail` event for compliance evidence. # Dashboard Source: https://docs.lerian.studio/en/reporter/console/bc-correios-dashboard Monitor incoming BCB mail at a glance — KPIs, mail categories, sync status, and recent activity in the BC Correios dashboard for regulatory oversight. The **Dashboard** is the landing page of the **BC Correios** plugin. It gives you a single view of the mailbox health: how much mail has arrived, what still needs attention, how it breaks down by category, whether synchronization with the Banco Central do Brasil (BCB) is healthy, and what happened most recently. ## Accessing the Dashboard *** Open **Reporter** > **BC Correios** > **Dashboard** from the left-side menu. The Dashboard is at the root of the BC Correios section. The Dashboard requires the `dashboard:get` permission. Without it, you see a "not authorized" page instead of a partial view. ## KPI cards *** Four cards summarize the current state of the mailbox: | KPI | Meaning | | :---------------- | :------------------------------------------------------------------------------------------ | | **Total mail** | All mail received from the BCB. | | **Unread** | Mail not yet opened. Links directly to the [Inbox](/en/reporter/console/bc-correios-inbox). | | **Urgent** | Mail flagged as requiring prompt attention. | | **AI categories** | Number of AI-derived categories detected across the mailbox. | ## Sync status bar *** The sync status bar shows the connection to the BCB and lets you trigger a manual fetch: * **Status badge** — `Pending`, `Syncing`, `Synced`, or `Sync failed`. * **Last sync** — relative time of the most recent synchronization (or "Never synced"). * **Interval** — how often BC Correios checks for new mail (configured in [Settings](/en/reporter/console/bc-correios-settings)). * **Credentials** — whether the BCB (Sisbacen) credentials are valid. If invalid, the badge links to Settings. * **Sync now** — triggers an immediate synchronization without waiting for the next polling cycle. If consecutive synchronization failures occur, a counter appears in the bar so you can react before mail is missed. ### First-time setup prompt If credentials have not been validated and no mail has been received yet, the Dashboard shows an onboarding alert prompting you to configure your **Sisbacen credentials** in Settings before mail can start arriving. ## Activity and category charts *** * **Recent activity** — monthly volume of received mail, so you can spot spikes or quiet periods. * **Distribution by type** — a proportional bar with a legend showing the share of each [mail type](/en/reporter/console/bc-correios): Circular, Resolution, Communication, and Other, each with a count and percentage. ## Recent mail *** The recent mail table lists the latest items received, with their type, subject, sender, and received time. Click a subject to open the [mail detail](/en/reporter/console/bc-correios-inbox#mail-detail) view. The Dashboard refreshes automatically while the tab is in focus — sync status roughly every 30 seconds and KPIs every minute. You don't need to reload the page. # Mail folders Source: https://docs.lerian.studio/en/reporter/console/bc-correios-folders Understand the Sent, Archived, and Cancelled folders in BC Correios, including which actions sync with the BCB and which are local-only. Besides the [Inbox](/en/reporter/console/bc-correios-inbox), BC Correios organizes mail into three additional folders: **Sent**, **Archived**, and **Cancelled**. They share the same list layout — type, subject, sender, received date, and a per-row actions menu — but differ in which actions are available and whether those actions affect the Banco Central do Brasil (BCB). All folder views require the `mails:get` permission, the same as the Inbox. ## Sent *** The **Sent** folder lists communications you have transmitted to the BCB through [New message](/en/reporter/console/bc-correios-compose). It is a reference view — there are no row actions other than opening the message detail. ## Archived *** The **Archived** folder holds mail you have archived for organization. From the row actions menu you can **Restore** an item back to its previous folder. **Archive and Restore are local-only.** They update the BC Correios database for organizational purposes and never change anything on the BCB side. Restore requires the `mails:put` permission; without it the action is hidden. ## Cancelled *** The **Cancelled** folder mirrors mail that the BCB has marked as cancelled. This view is **read-only**: there are no restore, archive, or delete actions. Cancellations always originate upstream at the BCB. The BCB integration does not expose a cancellation operation, so BC Correios only reflects the cancelled state — it cannot change it. To act on a cancellation, use the BCB web interface. ## Why there's no delete *** Regulatory mail must be retained to meet the minimum retention requirement configured in [Settings](/en/reporter/console/bc-correios-settings). For that reason there's no user-facing delete in any folder; archiving is the closest organizational action, and it keeps the mail fully retained. # Inbox Source: https://docs.lerian.studio/en/reporter/console/bc-correios-inbox Browse, search, and read regulatory mail received from the Banco Central do Brasil, including attachments and AI analysis, from the BC Correios inbox. The **Inbox** lists mail received from the Banco Central do Brasil (BCB). From here you can search and filter incoming regulatory communications, open a message to read its full content, download attachments, and review an optional AI analysis. ## Accessing the Inbox *** Open **Reporter** > **BC Correios** > **Caixa de Entrada** from the left-side menu. The screen is headed **Inbox**. The Inbox requires the `mails:get` permission. The same permission also covers the mail detail view, attachments, and the implicit mark-as-read action. ## The mail list *** Each row shows: | Column | Description | | :-------------- | :----------------------------------------------------------------------------------------------------------- | | **Type** | The [mail category](/en/reporter/console/bc-correios) badge — Circular, Resolution, Communication, or Other. | | **Subject** | The message subject. Unread mail is shown in bold. Click to open the detail view. | | **Sender** | The originating BCB unit or address. | | **Received at** | When the mail arrived. | | **Actions** | The menu with **View details**. | ## Searching and filtering *** Use the toolbar above the list to narrow the results: * **Search mail** — free-text search by subject and content. * **All types** — filters by mail category: Circular, Resolution, Communication, or Other. * **All statuses** — filters by read state: Read or Unread. * **Items per page** — control how many rows are shown. Use the button to clear all filters and the search box at once. The Inbox refreshes automatically about once a minute, so new mail appears without reloading. If you have permission to send messages, a **New message** button opens the [New message](/en/reporter/console/bc-correios-compose) page. ## Mail detail *** Click a subject to open the full message. Opening a mail automatically marks it as read. The detail view shows: * **Header** — the subject, mail type badge, sender, and received date. * **Content** — the rendered message content. * **Attachments** — a list of files with name and size; use the button to download each one. * **AI analysis** — an optional AI-generated summary of the regulatory mail (see below). ### Actions | Action | Behavior | | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Back to inbox** | Returns to the list. | | **Archive** | Moves the mail to the [Archived](/en/reporter/console/bc-correios-folders) folder. This is a **local-only** action and requires the `mails:put` permission. | | **Reply** | Opens [New message](/en/reporter/console/bc-correios-compose) pre-filled with the subject, to send a reply to the BCB. Requires the `mails:post` permission. | ### AI analysis panel When an AI provider is configured in [Settings](/en/reporter/console/bc-correios-settings), BC Correios can analyze each mail and present a summary. The panel reflects the analysis state: | State | What you see | | :---------- | :------------------------------------------------------------------------------------- | | `None` | AI analysis not available. | | `Pending` | Analysis is in progress. | | `Completed` | The AI summary, rendered from sanitized Markdown. | | `Failed` | An error message, with a **Retry AI analysis** option for users who can send messages. | AI analysis content is sanitized before rendering, so only safe formatting is displayed — never executable markup. # Settings Source: https://docs.lerian.studio/en/reporter/console/bc-correios-settings Configure BCB credentials, polling interval, retention, webhook notifications, and the AI provider for BC Correios inside the Reporter Console. The **Settings** page is where an administrator configures how BC Correios connects to the Banco Central do Brasil (BCB) and how it processes mail. It groups configuration into collapsible sections: BCB credentials, polling, retention, webhook, and AI. ## Accessing Settings *** Open **Reporter** > **BC Correios** > **Configurações** from the left-side menu. The screen is headed **Settings**. Viewing Settings requires the `settings:get` permission. Saving changes and testing connections require write permissions (`settings:patch` / `settings:post`). With read-only access, you see the current configuration with a read-only banner. ## BCB credentials *** The Sisbacen credentials used to access the BC Correio mailbox. * **BCB username** — your Sisbacen username. * **BCB password** — your Sisbacen password. A badge shows whether the credentials are **Configured** or **Not configured**. For security, stored secrets are never displayed back — leave a field blank to keep the existing value. Without valid BCB credentials, BC Correios cannot synchronize mail. If the [Dashboard](/en/reporter/console/bc-correios-dashboard) sync status shows credentials as invalid, update them here first. ## Polling *** Controls how often BC Correios fetches new mail from the BCB. * **Sync interval** — choose 5, 15, 30, or 60 minutes. ## Retention *** The minimum period mail is kept. * **Retention days** — minimum 5 years (1,825 days), maximum 100 years. The equivalent in years is shown next to the field. Retention enforces the regulatory minimum. This is why BC Correios has no user-facing delete — see [mail folders](/en/reporter/console/bc-correios-folders). ## Webhook *** Optional outbound notification when new mail is received. * **Webhook URL** — the endpoint to notify. * **HMAC secret** — used to sign webhook payloads so your receiver can verify authenticity. A badge shows whether the webhook is **Enabled** or **Disabled** based on whether a URL is set. ## AI provider *** Optional automatic analysis of regulatory mail, on a **bring-your-own-key** basis. When configured, summaries appear in the [mail detail](/en/reporter/console/bc-correios-inbox#mail-detail) **AI analysis** panel. | Field | Description | | :------------------ | :-------------------------------------------------------------- | | **AI provider** | None, OpenAI, Anthropic, or Google Gemini. | | **API key** | Your provider API key. | | **Model** | The model to use (for example `gpt-4o-mini`). | | **Custom endpoint** | Optional custom endpoint, for self-hosted or proxied providers. | | **System prompt** | Optional instructions that shape the AI analysis. | Use **Test connection** to verify the AI configuration before saving; on success the response latency is reported. ## Saving changes *** When you edit any field, a save bar appears with **Discard** and **Save** actions. BC Correios sends only the fields you changed, and shows a confirmation when the save succeeds. Configuration changes are recorded in the [Audit Trail](/en/reporter/console/bc-correios-audit) as `Updated settings` and `Tested BCB connection` events. # Deadlines in the Console Source: https://docs.lerian.studio/en/reporter/console/deadlines-overview Track upcoming and overdue reporting deadlines from the Reporter Overview in Lerian Console — regulatory calendar, delivery status, and notifications. In [Lerian Console](/en/platform/console/about-lerian-console), **Deadlines** are not a separate menu item. They live inside the [Reporter module](/en/reporter/console/reporter-module), on the **Overview** page. This keeps delivery tracking next to the reports and data sources it relates to, rather than in an isolated area. Use this page to understand how reporting deadlines surface visually in the Console. For creating, updating, or delivering deadlines programmatically, see [Managing deadlines](/en/reporter/managing-deadlines). Reporter is only available if you've acquired the Reporter product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more. ## Where to find deadlines *** When you open Reporter, you land on **Overview**. Deadlines appear here as dedicated widgets alongside the module's KPIs: * **Reporter** from the left-side menu opens the module. * **Overview** is the default landing page and the home for all deadline tracking. There is no separate "Deadlines" navigation entry — everything related to delivery tracking is consolidated on this dashboard. ## Overview widgets *** Two widgets on the Overview page give operators a complete view of their reporting obligations: * **Upcoming Deadlines**: Lists the next reporting reminders with their due dates and current status, so operators can see what's coming up at a glance. * **Delivery Calendar**: Shows recurring and one-off reporting deadlines by date, giving a calendar view of the obligation landscape across the period. Together, the `UpcomingDeadlines` and `RegulatoryCalendar` widgets turn the underlying deadline records into a visual delivery layer on top of the reporting engine. ## What you can see *** From the Overview, operators can monitor each obligation without leaving the Console: * **Deadline status** — whether each obligation is `pending`, `overdue`, or `delivered`. * **Notifications** — reminders that begin a configured number of days before the due date, so a deadline doesn't slip unnoticed. * **Delivery state** — when a report behind a deadline has been filed or sent, the deadline reflects that it was delivered. Deadlines in the Console are a tracking view only. They help operators follow reporting routines — they do not generate or submit reports automatically. Report generation still happens from the [Reports](/en/reporter/console/managing-reports) area. ## Console vs API *** Use the **Console Overview** when an operator needs to *see* upcoming and overdue deadlines, follow the regulatory calendar, and monitor delivery status visually. Use the **API** when a system needs to create, update, delete, or mark deadlines delivered programmatically — for example, to sync obligations from another system or automate delivery tracking. Full request and response reference for creating, listing, updating, deleting, and delivering deadlines. # Delete a Data Source Source: https://docs.lerian.studio/en/reporter/console/deleting-a-data-source Permanently remove a Data Source from Reporter when no active reports depend on its database connection, keeping your Console configuration clean. Use this page to remove a Data Source that should no longer be available for future Templates or Reports. ## Objective *** Permanently delete a Data Source connection from Reporter. For example, delete a Data Source when an external reporting database was retired, a temporary migration connection is no longer needed, or operators have moved to a replacement connection. ## When to use *** Use this task when: * the database connection is retired; * no current Template or reporting process should use it; * a replacement Data Source exists, if the process continues; * the connection was created by mistake and should not remain available. Do not delete a Data Source just because it is not part of the current operator journey. Internal product Data Sources may be preconfigured for other Templates or future Reports. Do not delete a Data Source just because a connection test failed. If the connection is still needed, edit and test it instead. ## Before you start *** Confirm: * no active reporting process depends on the connection; * current Templates do not rely on fields from this Data Source; * operators know which replacement Data Source to use; * the connection is not only failing temporarily because of credentials, network, schema, or SSL settings. Deleting a Data Source is permanent. Future Report generation can fail if a Template or process still depends on this connection. ## Step by step *** Go to **Data Sources**. Locate the card for the Data Source you want to delete. Click the three vertical dots () on the card and select **Delete**. In the **Delete Connection** dialog, review the connection name and confirm the deletion. Confirm that the Data Source no longer appears on the **Data Sources** page. ## Field guide *** | Field or label | What to check | Example | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | Connection name | Confirms the Data Source that will be removed. The technical field is `configName`; check that it is the retired connection, not a replacement. | `migration-postgres-temp` | | Connection ID | Technical UUID used by the API as the `connectionId` path parameter. The Console selects it when the operator chooses **Delete**. | `018f2f4c-...` | | **Delete** | Action in the Data Source card menu. It starts the destructive flow and opens the confirmation dialog. | Select **Delete** on `migration-postgres-temp`. | | **Delete Connection** | Final confirmation action. After this, the Data Source is permanently removed from the **Data Sources** list. | Confirm the temporary connection removal. | ## Expected result *** The Data Source is removed from the list and can no longer be selected or used by future Reporter workflows. ## Common errors and care points *** Future Reports can fail if the Template expects data from a deleted connection. Confirm dependencies first. If the connection is still operationally needed, edit credentials or SSL settings and run **Test Connection**. If the reporting process continues, create and test the replacement Data Source before deleting the old one. ## Next steps *** * Use [Add a Data Source](/en/reporter/console/adding-a-data-source) if a replacement connection is needed. * Use [Test a Data Source Connection](/en/reporter/console/testing-a-data-source) to validate replacement connections. * Use [Managing Data Sources](/en/reporter/console/managing-data-sources) to review the remaining active connections. [Delete connection endpoint](/en/reference/reporter/connections/delete-connection) # Delete a Template Source: https://docs.lerian.studio/en/reporter/console/deleting-a-template Remove a Template from Reporter when it is no longer needed for any regulatory report generation, keeping the Console library focused and easy to navigate. Use this page to remove a Template that should no longer be used for future Report generation. ## Objective *** Delete a retired Template from the **Templates** page. For example, delete a Template when a reporting layout was replaced by a new one, the report obligation no longer exists, or the Template was created by mistake and has no operational use. ## When to use *** Use this task when: * the Template is retired; * operators should no longer select it when generating Reports; * a replacement Template already exists, if the reporting process continues; * the Template was created incorrectly and should not remain available. Do not delete a Template just because one generated Report was wrong. Update the Template or the Report inputs, then generate a new Report. ## Before you start *** Confirm: * no active reporting process depends on this Template; * operators are not using it for the current reporting cycle; * a replacement Template exists when the report is still required; * any generated Reports that need external storage were downloaded or archived. Deleting a Template is permanent. Reports already generated with that Template remain available on the **Reports** page, but new Reports cannot use the deleted Template. ## Step by step *** Go to **Templates**. Locate the Template you want to remove. Check its name carefully if multiple Templates have similar names. Click the three dots () in the **Actions** column and select **Delete**. In the **Confirm Deletion** dialog, review the Template name and confirm the deletion. ## Field guide *** | Field or label | What to check | Example | | -------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | Template name | Confirm this is the retired Template, not the replacement. API field: `description`; Console field: `name`. | `Monthly tax CSV - legacy` | | Template ID | Technical identifier used by the delete API. The Console selects it when you choose **Delete**. | `018f2f4c-...` | | **Delete** | Opens the destructive confirmation flow from the row **Actions** menu. | Select **Delete** for the retired Template | | **Confirm Deletion** | Final confirmation before the Template is removed from future **Generate Report** selection. | Confirm `Monthly tax CSV - legacy` | ## Expected result *** The Template is removed from the **Templates** page and is no longer available in the **Generate Report** wizard. ## Common errors and care points *** Future Reports that depend on the deleted Template cannot be generated from it. Confirm the replacement workflow before deletion. If the Template is still needed but has the wrong layout, output format, or name, update it instead of deleting it. Generated Reports remain available on the **Reports** page. Deletion only removes the Template from future use. ## Next steps *** * Use [Add a Template](/en/reporter/console/adding-template) if you need a replacement Template. * Use [Update a Template](/en/reporter/console/updating-a-template) if the Template should be corrected instead of removed. * Use [Managing Templates](/en/reporter/console/managing-templates) to review the active Template list. [Delete template endpoint](/en/reference/reporter/delete-template) # Download a Report Source: https://docs.lerian.studio/en/reporter/console/downloading-a-report Download a finished Report from the Console once Reporter has completed generation, ready for submission to regulators or long-term archiving. Use this page to download a generated Report after Reporter finishes creating the file. ## Objective *** Save the generated Report file outside the Console so it can be submitted, archived, reviewed, or shared through the appropriate internal workflow. For example, download a `Finished` XML file for regulatory submission or a CSV file for operational reconciliation. ## When to use *** Use this task when: * the Report status is `Finished`; * the operator needs the generated file outside the Console; * the file must be stored, sent through another process, or checked locally. Do not use download while the Report is still `Processing`. Wait for completion first. ## Before you start *** Confirm: * the Report status is `Finished`; * the Template and filters used for generation are the intended ones; * the output format is the one expected by the receiving process; * `Error` Reports have been corrected and regenerated. Preview is for checking the Report inside the Console. Download is the action that saves the generated file for use outside the Console. ## Step by step *** Go to **Reports**. Locate the Report you need to download. Check its name, Template, generation date, and status. Make sure the status is `Finished`. Click the three dots () in the **Actions** column. Select **Download Report**. ## Field guide *** | Field or label | What to check | Example | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | Report status | Download only after the Report reaches `Finished`. Technical field: `status`. | `Finished` | | Template | Check which Template and output format produced the file. Technical fields include `templateId`, `templateDescription`, and `templateOutputFormat`. | `Monthly tax CSV`, `csv` | | Filters | Review the filters used during generation. Generated Reports cannot be edited before download. | `transactions.created_at between 2026-05-01 and 2026-05-31` | | **Download Report** | Action that downloads the generated file for the selected `reportId`. | Download report `018f2f4c-...` | ## Expected result *** The file downloads in the output format configured by the Template, such as CSV, XML, HTML, TXT, or PDF. ## Common errors and care points *** Wait until the status changes to `Finished`. A Report in `Processing` is not ready yet. Download does not change Report content. If the filters were wrong, generate a new Report with corrected inputs. Preview helps operators inspect the Report in the Console. Use **Download Report** for the final file. ## Next steps *** * Store or deliver the downloaded file through the required operational process. * Use [Correct a Report](/en/reporter/console/editing-a-report) if the downloaded file has wrong data, layout, or format. * Use [Generate a Report](/en/reporter/console/generating-a-report) to create a corrected file. [Download report endpoint](/en/reference/reporter/download-report) # Edit a Data Source Source: https://docs.lerian.studio/en/reporter/console/editing-a-data-source Update an existing Data Source — change credentials, switch the host, or adjust SSL settings — without recreating the connection or reconfiguring reports. Use this page to update an existing editable Data Source when its database access details change. ## Objective *** Keep a Data Source usable without deleting and recreating it. For example, edit a Data Source when an external database password rotates, the host changes after an infrastructure migration, a new schema must be visible, or SSL settings are updated. ## When to use *** Use this task when: * credentials changed; * host, port, or database name changed; * schema access changed; * SSL mode or CA certificate changed; * metadata needs better operator context. If the Data Source was automatically configured for an internal Lerian product, confirm whether it is meant to be edited from the Console before changing it. If the connection is retired and should no longer be used, delete it instead. ## Before you start *** Confirm what changed: * credentials: update **Username** or **Password**; * database location: update **Host**, **Port**, or **Database Name**; * schema visibility: update the schema list; * security: update **SSL Mode** or **CA Certificate**. A Data Source update affects future Report generation. Reports already generated stay unchanged. ## Step by step *** Go to **Data Sources**. Locate the card for the Data Source you need to edit. Click the three vertical dots () on the card and select **Edit**. In **Edit Connection**, change the fields that need correction. Click **Save Changes**. Run **Test Connection** to confirm the updated settings work. The **Password** field appears empty for security reasons. Enter a new value only when changing the password. Leaving it blank keeps the current password. ## Field guide *** | Field | What to change | Example | | ----------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------- | | **Connection Name** | Required operator-facing name. Technical field: `configName`; Console validation is 3 to 100 characters. | `production-postgres-readonly` | | **Database Type** | Database engine used by the connection. Console currently offers `POSTGRESQL` and `MONGODB`. | `POSTGRESQL` | | **Host** | Database server address. Changing it changes where Reporter reads future data from. | `db-new.example.com` | | **Port** | Database network port. Valid range is 1 to 65535. | `5432` | | **Database Name** | Database Reporter queries through this Data Source. | `ledger_reporting` | | **Username** | Database user used by Reporter. | `reporter_readonly` | | **Password (optional)** | Leave blank to keep the current password. Enter a value only when replacing the stored credential. | New rotated password | | **Schemas** | Optional schema list stored as `metadata.schemas`; entries are trimmed, lowercased, and deduplicated. | `public`, `pix` | | **Enable SSL** | Toggle SSL settings. Disabling SSL sends `ssl.mode = disable`. | Disabled after network migration | | **SSL Mode** | SSL behavior. Use `disable`, `require`, `verify-ca`, or `verify-full`. | `verify-full` | | **CA Certificate** | Required when changing SSL to `verify-ca` or `verify-full`. Leave blank unless replacing SSL material. | PEM CA certificate | | **Metadata** key/value fields | Optional classification data. Only rows with a nonblank key are submitted. | `owner = compliance` | ## Expected result *** The Data Source keeps the updated configuration. After a successful test, operators can rely on it for Templates and Reports. ## Common errors and care points *** Leaving **Password** blank preserves the existing password. Enter a new value only when the credential changed. Host and port changes can point Reporter to a different database. Always run **Test Connection** after saving. If a Template or filter depends on fields from a schema, removing that schema can break future generation. If the database is no longer used by any reporting process, delete the Data Source instead of editing it. ## Next steps *** * Use [Test a Data Source Connection](/en/reporter/console/testing-a-data-source) after every connection change. * Use [Generate a Report](/en/reporter/console/generating-a-report) only after the connection is healthy. * Use [Delete a Data Source](/en/reporter/console/deleting-a-data-source) when the connection is retired. [Update connection endpoint](/en/reference/reporter/connections/update-connection) # Correct a Report Source: https://docs.lerian.studio/en/reporter/console/editing-a-report Understand why generated Reports cannot be edited, and how to correct the source setup before generating a new Report from the same Reporter template. A generated Report is a historical file. Reporter does not edit the generated output after generation finishes. ## Objective *** Correct the source setup that produced a wrong Report, then generate a new Report. For example, if a CSV file is missing rows, review the filters used during generation. If the layout is wrong, update the Template. If values are wrong, test the Data Source and confirm it points to the expected database and schema. ## When to use *** Use this page when: * an operator asks why a generated Report cannot be changed directly; * a generated Report has wrong data, layout, or output format; * an `Error` or `Partial` Report needs investigation before trying again; * the team needs a corrected file while preserving the original generated Report for history. Do not use this task to overwrite a generated file. Generate a new Report after correction. ## Before you start *** Identify what is wrong: | Problem | Likely source to check | | ---------------------------------------- | ------------------------------------------------------ | | File layout is wrong | Template structure or `.tpl` file | | Output format is wrong | Template **Output Format** | | Rows are missing | Report filters or Data Source schema | | Values are wrong | Data Source connection, schema, fields, or source data | | Generation returned `Error` or `Partial` | Report details, Template, Data Source, filters | The original Report remains available for audit history. The corrected file is a new generated Report. ## Step by step *** Go to **Reports** and open the generated Report details. Check the Template, filters, output format, status, and any error details. Update the Template, test or edit the Data Source, or adjust the filters you will use during the next generation. Use the corrected Template, Data Source, and filters to generate a new Report. Wait until the new Report reaches `Finished`, then download it. ## Field guide *** | Field or status | What it helps diagnose | Example | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | Template | Whether the correct Template and output format were used. Technical fields include `templateId`, `templateDescription`, and `templateOutputFormat`. | `Monthly tax CSV`, `csv` | | Filters | Whether the generated Report included the intended data subset. Technical field: `filters`. | `status eq active` | | Status | Whether the Report finished, is still running, covers only some of its sections, or returned an error. Technical field: `status`. | `Error` | | Error details | Message returned by Reporter when generation fails. Technical field: `error`. | `connection timeout` | | Data Source | Whether the Report came from the expected database and schema. Check the filter `database` and the Template field mapping. | `production-postgres` | ## Expected result *** The incorrect Report stays unchanged, and a new corrected Report is generated from the fixed setup. ## Common errors and care points *** Reporter keeps generated Reports as historical outputs. Correct the Template, Data Source, or filters, then generate again. Missing rows often come from filters, not from the Template. Review the generation inputs before changing the Template. If values are wrong or schema access changed, test the Data Source before generating a corrected Report. ## Next steps *** * Use [Update a Template](/en/reporter/console/updating-a-template) when layout or output format is wrong. * Use [Testing a Data Source](/en/reporter/console/testing-a-data-source) when values or schema access are suspect. * Use [Generate a Report](/en/reporter/console/generating-a-report) after fixing the source setup. Generate the corrected Report. There is no direct edit API for historical outputs. Monitor the corrected Report before download. # Generate a Report Source: https://docs.lerian.studio/en/reporter/console/generating-a-report Generate a regulatory Report through a four-step wizard that guides you from Template selection to the final compiled output ready for submission. Use this page to generate a Report from an existing Template. Generation is the moment Reporter applies a Template to data and creates a file that can be monitored, viewed, and downloaded. ## Objective *** Create a generated Report for a specific operational need, period, entity, status, or submission. For example, generate a monthly CSV for May transactions, an XML file for a regulatory submission, or a PDF summary filtered to one customer segment. ## When to use *** Use this task when: * the Template is ready; * the Data Source used by the Template is available. It may be an internal source already configured for the environment or an external source added from **Data Sources**; * an operator needs to produce a Report file; * the Report needs optional filters, such as date range, status, type, or entity. Do not use this task to change the Template structure. Update the Template first, then generate a new Report. ## Before you start *** Confirm: * the selected Template is the correct one for the obligation; * the Template output format matches the file you need to deliver; * the Data Source used by the Template is available and tested when the report depends on a connection that may have changed; * you know whether the Report should include all data or a filtered subset; * the filter values are precise enough for the reporting period. Reports are generated asynchronously. A new Report starts in `Processing` and only becomes available for download when it reaches `Finished`. ## Step by step *** Go to **Reports** and click **New Report**. In the **Template** step, choose the Template that defines the output structure and format. Click **Next**. In the **Filters** step, add one or more filters to narrow the data. This step is optional. If the Report should include all available data, click **Next**. In the **Review** step, check the selected Template and applied filters. Click **Generate Report**. The wizard moves to the **Generate** step and tracks progress while Reporter creates the file. When generation succeeds, the wizard offers **Download** and **View in List** actions. If you close the wizard earlier, the Report appears in the list with `Processing` status; wait until it reaches `Finished` before downloading. ## Field guide *** | Field | What to enter | Example | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | **Template** | Required Template selected in the **Template** step. The technical value sent to the API is `templateId`. | `Monthly tax CSV` | | Search templates | Optional UI search. It filters the loaded Template list by name or file name and is not sent in the generation payload. | `tax` | | **Database** | Required only when a filter row is used. Select the Data Source connection; the payload uses this value as `database`. | `production-postgres` | | **Table** | Required only when a filter row is used. Select a table from the connection schema. | `transactions` | | **Field** | Required only when a filter row is used. Select the field to compare. | `created_at` | | **Operator** | Required only when a filter row is used. Use `eq`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, or `nin`. | **Between** / `between` | | **Values** | Required only when a filter row is used. `between` needs exactly two values; `in` and `nin` accept comma-separated values; other operators use one value. | `2026-05-01, 2026-05-31` | ## Filter examples *** | Filter | Result | | ------------------------------------------------- | ---------------------------------------- | | `status` **Equal** `active` | Includes only active records. | | `created_at` **Between** `2026-05-01, 2026-05-31` | Includes records from May 2026. | | `type` **In** `credit,debit` | Includes either transaction type. | | `amount` **Greater Than** `1000` | Includes records with amount above 1000. | ## Status and next action *** | Status | What it means | Operator action | | ------------ | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `Processing` | Reporter is generating the file. | Wait. The list refreshes automatically. | | `Finished` | The file is ready. | View, preview, or download the Report. | | `Partial` | Some data sections failed. The Report covers only the sections that answered. | Review the failed sections in the Report details, check Data Source health, and generate again after correction. | | `Error` | Reporter found an error during generation. | Review the message and generate again after correction. | ## Expected result *** A new Report appears on the **Reports** page. When it reaches `Finished`, operators can preview or download the generated file. ## Common errors and care points *** The Template controls layout and output format. If the wrong Template is selected, generate a new Report with the correct Template. Failed or outdated connections can cause generation errors. Test the Data Source before generating operational Reports when connection health is uncertain, especially for external databases or recently changed credentials. Empty filters can include all available data. Confirm whether the Report should cover all records or only a specific period, status, or entity. Download is only available after the Report reaches `Finished`. ## Next steps *** * Use [Download a Report](/en/reporter/console/downloading-a-report) when the status is `Finished`. * Use [Correct a Report](/en/reporter/console/editing-a-report) if the generated file has wrong data, layout, or format. * Use [Testing a Data Source](/en/reporter/console/testing-a-data-source) if generation fails because of connection or schema issues. Generate Reports programmatically. Poll generation status before download. # Managing Data Sources Source: https://docs.lerian.studio/en/reporter/console/managing-data-sources Review, test, and manage the database connections that supply data to Reporter, keeping every regulatory report grounded in the correct source of truth. Data Sources are connections that Reporter uses to fetch the data needed for report generation. Some Data Sources can already be configured for internal Lerian products, such as Midaz or CRM. Use this page to review those connections, test them, or manage external database connections through the [Lerian Console](/en/platform/console/about-lerian-console). ## What is a Data Source? *** A Data Source represents a configured connection between Reporter and a data store. To generate reports, Reporter needs access to source data. For internal product data, the connection may already exist. For external databases, operators can add and manage the connection from this page. Each Data Source includes: * **Connection name** — a human-readable identifier for easy management. * **Database type** — the database engine being used (PostgreSQL, MySQL, MongoDB, Oracle, or SQL Server). * **Access configuration** — host, port, database name, credentials, and optionally SSL configuration, when the Data Source is an external database connection. * **Schemas** — for databases that support schemas (PostgreSQL, Oracle, SQL Server), you can specify which schemas to access. * **Metadata** — additional key-value fields for organization. Operators usually use this page to answer three questions: * Is the connection available? * Which schemas, tables, and fields can Reporter read? * Is this the right Data Source for the Template or Report being prepared? ## Supported databases *** | Database | Default port | Schema support | | ---------- | :----------: | :------------: | | PostgreSQL | 5432 | ✅ | | MySQL | 3306 | ❌ | | MongoDB | 27017 | ❌ | | Oracle | 1521 | ✅ | | SQL Server | 1433 | ✅ | ## Accessing the Data Sources page *** To open the **Data Sources** page, click **Data Sources** from the left-side menu on the **Reporter** page. You do not need to start here in every Reporter journey. Start here when the needed Data Source is missing, when an external database must be added, or when a report fails because data is unreachable. The page displays all configured connections, each in a card showing: * Connection name and database type. * Connection status (**Connected**, **Disconnected**, or **Connecting**). * Configured schemas (if any). * Latency from the last check. * Host, database, and user details. You can expand each card to view the database schema, including available tables and columns. ### Available actions From the Data Sources page, you can: Create a reusable external database connection for Reporter. Update credentials, host, schemas, SSL, or metadata. Confirm Reporter can reach the database and load schema information. Remove retired connections that are no longer used. If a Report fails because data is missing or unreachable, start by testing the Data Source and checking whether the expected schema and table appear in the expanded card. # Managing Reports Source: https://docs.lerian.studio/en/reporter/console/managing-reports Generate, filter, track status, and download regulatory reports created from your uploaded templates in the Reporter module of the Lerian Console. Once templates are uploaded, you can view and manage reports through [Lerian Console](/en/platform/console/about-lerian-console), including generating, filtering, tracking their status, and downloading results. Use the Reports page as the operational queue for generated files. It shows what was generated, whether generation finished, and what action the operator should take next. If the required Template and Data Source already exist, operators can start here to generate the next Report. Use **Data Sources** only when a connection is missing, unhealthy, or needs to be tested. ## Accessing the Reports page *** To open the **Reports** page, click **Reports** from the left-side menu on the **Reporter** page. The page displays all reports for the organization in two view modes: * **Table** (default) — a list with columns: Name, Report ID, Status, Format, and Completed At. * **Grid** — a card-based view. Toggle between modes using the button in the top-right area. ### Actions available Start a new generation from an existing Template. Save a finished Report for submission or archiving. Learn what to change when a generated Report is wrong. For reports with errors, additional actions are available in the menu (): * **View Details** — opens a panel with error details. * **Try Again** — reopens the generation wizard with the same template pre-selected. ## Filtering reports *** You can filter reports by date using the filter fields above the list. The list automatically refreshes while there are reports with `Processing` status. You don't need to reload the page. To clear all filters, click the icon. ## Report status *** Each report displays a status badge, so you always know where it stands: | Status | Definition | | :----------- | :-------------------------------------------------------------------------------- | | `Processing` | The system is generating the report. | | `Finished` | The report is ready for viewing and download. | | `Partial` | Some data sections failed. The report covers only the sections that answered. | | `Error` | Something went wrong during generation. You can view error details and try again. | If the status is `Error` or `Partial`, the Report is not a final artifact. Open the details first, fix the Template, filters, or Data Source, and generate a new Report. ## Viewing a report *** When a report reaches the `Finished` status, you can view it directly in the Console: * Click the report name in the list, or * Click the three dots () and select **View**. A preview dialog will open showing the report content. The preview adapts to the report's output format: | Format | Preview behavior | | :----- | :--------------------------------------------------------------------------------- | | CSV | Interactive table with headers, row numbers, and column/row count. | | XML | Syntax-highlighted code with line numbers and a virtual scrollbar for large files. | | PDF | Embedded PDF viewer rendered in an iframe. | | HTML | Sandboxed HTML rendered in an iframe. | | TXT | Plain text displayed in a monospaced block. | ### Preview actions The preview dialog includes action buttons at the top: * **Copy** — copies the report content to your clipboard (not available for PDF). * **Print** — opens the browser print dialog for the report content (not available for PDF). * **Download Report** — downloads the report file directly from the preview. For PDF reports, use the embedded viewer controls to navigate pages, zoom, and print. # Managing Templates Source: https://docs.lerian.studio/en/reporter/console/managing-templates Manage reusable templates that define the structure and format of every regulatory report Reporter generates from your Midaz and CRM data sources. Templates are reusable configurations that define the structure and format of generated reports. In this section, you'll learn how to manage templates via the [Lerian Console](/en/platform/console/about-lerian-console). Use the Templates page to manage the reusable definitions that operators select when generating Reports. A Template controls the output format, file layout, and the data fields Reporter uses. In the common internal-product journey, this is often where operators start: Data Sources for products such as Midaz or CRM may already be configured, so the first manual task is choosing, creating, or updating the Template. ## Accessing the Templates page *** To open the **Templates** page, click **Templates** from the left-side menu on the **Reporter** page. The page displays all templates for the organization in a table format, with the following columns: * **Name** — the template name. * **Template ID** — the unique identifier for the template. * **Format** — the output format (CSV, XML, HTML, TXT, or PDF). * **Last Modified** — the date the template was last modified. * **Actions** — action menu. You can filter templates by creation date or output format using the filter fields above the table. ### Creating templates The Templates page offers two creation modes: * **Template Builder** — a visual editor for building templates in a guided flow. * **New Template** — uploads a `.tpl` file created externally. Choose **Template Builder** when the operator needs a guided, visual flow. Choose **New Template** when a technical team already prepared the `.tpl` file outside the Console. If the Template needs fields from a Data Source, confirm that the source already appears in the builder. Add a new Data Source only when the required external connection is missing. ### Available actions menu For each template, the following actions are available in the action menu (): * **Preview** — previews the template (available only for templates created with the Template Builder). * **Edit** — opens the edit wizard or the Template Builder, depending on how the template was created. * **Delete** — permanently removes the template. ### Related pages Create a Template visually or upload a prepared `.tpl` file. Change the Template used for future Reports. Remove a Template that is no longer operationally needed. Build a Template with visual blocks instead of editing `.tpl` code. Updating or deleting a Template does not change Reports already generated. It only changes what can be generated next. # Concepts Map Source: https://docs.lerian.studio/en/reporter/console/reporter-concepts-map Understand how Templates, Reports, Data Sources, and downloads relate in Reporter before using the module in the Lerian Console for regulatory reporting. The Reporter Module uses a small set of core objects. They are related, but the operator does not always start by creating a Data Source. In some environments, internal Data Sources for Lerian products such as Midaz or CRM are already configured. This page is the shortest path to the mental model. ## Core flow *** Reporter has two common entry paths: * **Internal product data already configured:** `Template → Report → Download` * **External or missing data connection:** `Data Source → Template → Report → Download` * The **Template** says *what the report should look like and in which format*. * The **Data Source** says *where the data comes from*. It may already exist for internal products or be added manually for external databases. * The **Report** is the generated file, produced by running a Template against the connected data, optionally narrowed by filters. * The **Download** is the operator action that takes the finished file out of the Console. ### What each object means | Object | What it represents | Why it exists first or later | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `Data Source` | A configured data connection. It can be an internal product connection already created for the environment, or an external database connection such as PostgreSQL or MongoDB. | It exists before generation, but the operator only needs to create one manually when the required connection is not already configured. | | `Template` | A reusable definition of structure and output format (CSV, XML, HTML, TXT, or PDF), uploaded as a `.tpl` file or built visually with the Template Builder. | It describes *what* the report should look like. It does not execute anything by itself. | | `Filter` | An optional rule applied at generation time (Database + Table + Field + Operator + Value). | It narrows the data used for a single Report run, without changing the Template. | | `Report` | The output produced when a Template is run against the data. Has a status (`Processing`, `Finished`, `Partial`, `Error`). | It is the actual artifact the operator delivers. | | `Preview` | The in-Console view of a `Finished` report, adapted to its output format. | It lets the operator validate the result before sending it out. | | `Download` | The action that takes the finished report file out of the Console in its configured format. | It is the operational endpoint of the whole flow. | | `Reminder` | An optional delivery deadline tracked from the Overview page (Once, Daily, Weekly, Monthly, Semiannual, Annual). | Used when a Report has a recurring delivery cadence to manage operationally. | ## When a Template becomes a Report *** A Template by itself is just a definition. It becomes a Report at one specific moment: when you click **Generate Report** in the 4-step wizard on the **Reports** page. At that point: 1. Reporter takes the selected Template. 2. It applies any filters you configured. 3. It reads from the Data Sources referenced by the Template. 4. It produces a file in the Template's output format and adds it to the Reports list with `Processing` status. 5. When generation finishes, the status moves to `Finished` and the file becomes downloadable. Until that moment, you have only a *template*. After that moment, you have a *report*. ## Which fields actually matter *** When operators get stuck, it is usually around the same handful of fields. This is the short list of the ones with real operational effect. ### When checking or adding a Data Source * **Connection Name** — How the connection shows up everywhere else (in templates, in report filters). For external Data Sources, pick something that identifies the environment, not just the engine (e.g., `production-db`, not just `postgres`). * **Database Type** — Determines the default port, whether schemas are supported, and the connection driver used. * **Host / Port / Database Name / Username / Password** — The minimum required to reach the database. * **Schemas** (PostgreSQL, Oracle, SQL Server) — Limits which schemas Reporter can browse. Leave empty to use the default (`public`) for engines that support it. * **SSL Configuration** — Optional, but use it whenever the network path is not fully trusted. ### When adding a Template * **`.tpl` file** (upload mode) — The actual template definition. Required. * **Template Name** — How the Template appears in lists and pickers. * **Output Format** — Locks the final file type (CSV, XML, HTML, TXT, or PDF). When PDF is selected in the Template Builder, the generated code is HTML, rendered as PDF at generation time. ### When generating a Report * **Template** — The Template you want to run. * **Filters** (optional) — Each row is `Database + Table + Field + Operator + Value(s)`. Use filters to produce a narrower slice without editing the Template. ## Practical reading of the UI *** Use this interpretation when guiding users: * `Templates` defines *what the output should look like*. * `Reports` is where the structure and the data come together and produce a file. * `Data Sources` defines *what Reporter is allowed to read*. Use it to inspect existing connections, test connectivity, or add external databases. * `Overview` is the operational dashboard: KPIs, the delivery calendar, and reminders for upcoming or overdue deliveries. ## When users usually get lost *** A Template is a definition. A Report is a generated file. The same Template can be turned into many Reports over time (each with its own filters, run date, and status). The Data Source is the connection Reporter uses to read data. A Filter is a per-run narrowing that does not change the Template or the Data Source — only what data the current Report includes. The Template Builder generates `.tpl` code from blocks you assemble in the Console. Uploading a `.tpl` skips the builder and uses a file prepared externally. Both end up as the same kind of Template, but only builder-created templates can be re-edited in the visual editor. A Report reaching `Finished` means the file is generated and downloadable. Whether it was actually delivered (sent to a regulator, archived, etc.) is tracked separately via reminders on the Overview page. Pair this page with [Setup path](/en/reporter/console/reporter-setup-path) when onboarding new users. One page explains the model; the other explains the order. # What is Reporter module? Source: https://docs.lerian.studio/en/reporter/console/reporter-module Configure Data Sources, build Templates, generate Reports, and download the output through a visual interface in the Lerian Console. **Reporter** is a module inside the [Lerian Console](/en/platform/console/about-lerian-console) for operating report generation from data already available in your environment. Use it to define the structure of the output, generate a Report, review its status, and download the final file. When the report needs an external database or a connection needs validation, use **Data Sources** to manage that connection. Typical use cases include regulatory filings, internal audit extracts, recurring operational reports, and ad hoc reports for a specific period. Reporter does not change how report generation behaves in the product. This page only explains how to operate the Reporter Module from the Console. Reporter is only available if you've acquired the Reporter product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more. ## Who uses this page *** This page is for operators who need to produce or manage Reports in the Console. That usually means operations, compliance, finance, regulatory, or support teams responsible for turning stored data into a file that can be reviewed, submitted, or archived. Developers and integration teams may also use Reporter, but their main reference is the [Reporter API reference](/en/reference/reporter/reporter-developer-quick-start). The Console documentation explains the visual workflow. The API reference explains programmatic access. ## Start here first *** If this is your first time using Reporter, start by understanding which setup path applies. Some environments already include internal Data Sources for Lerian products, such as Midaz or CRM. In that case, the operator can often start from **Templates** and **Reports**. Use **Data Sources** first when you need to add an external database or validate an existing connection. Choose the right path: use existing internal Data Sources, or add and test an external Data Source before generating Reports. Understand how Data Sources, Templates, Reports, previews, and downloads relate. ## What you can operate *** From the Reporter Module, you can: * **Manage Data Sources** — Review, test, or register database connections that Reporter can read from during generation. Internal product connections may already exist; external databases are added from this area. * **Build Templates** — Define the structure and output format of the Report, either by uploading a prepared `.tpl` file or building one visually in Template Builder. * **Generate Reports** — Run a Template against connected data, with optional filters such as date range, status, or transaction type. * **Review generation status** — Track whether a Report is `Processing`, `Finished`, `Partial`, or `Error`. * **Preview and download output** — Open a finished Report in the Console and download the generated file in its configured format, such as CSV, XML, HTML, TXT, or PDF. * **Track delivery reminders** — Use the Overview area to follow upcoming and overdue reporting deadlines. ## Operational areas *** The Reporter sidebar is organized around the common operator workflow: understand the current state, define the output, generate and download files, then manage Data Sources when a connection needs to be added, tested, or changed. Monitor the current Reporter workspace: KPIs, delivery reminders, upcoming deadlines, and recent error count. Review internal connections and configure or test external database connections Reporter uses to fetch data. Create, update, preview, and delete Templates that define the Report layout and output format. Generate Reports, monitor their status, preview finished files, and download the result. ## Overview area *** When you open Reporter, you land on **Overview**. Use it as the operating dashboard for the module. It includes: * **What is Reporter?** — a short description of the module and a link to the documentation. * **Templates** — total number of registered Templates. * **Reports Generated** — total number of generated Reports. * **Data Sources** — total number of configured database connections. * **Errors** — errors recorded in the last 30 days. * **Delivery Calendar** — recurring or one-off reporting deadlines shown by date. * **Upcoming Deadlines** — the next reminders, their due dates, and their status. Delivery reminders help operators manage reporting routines. They do not generate or submit Reports automatically. Report generation still happens from the **Reports** area. ## Main tasks *** Register an external database connection when the needed Data Source is not already configured. Confirm that credentials, network access, SSL settings, and schema visibility are correct. Upload a prepared `.tpl` file or build a Template visually with Template Builder. Run a Template against your data and produce a downloadable file. Save the final file from a finished Report generation. Learn what to change when a generated Report has the wrong data, layout, or format. ## Console vs API *** Use the **Console** when an operator needs to configure, generate, review, and download Reports through the visual interface. Use the **API** when a system needs to automate Reporter operations, integrate Reporter into another workflow, or manage Templates, Reports, and Data Source connections programmatically. The same Reporter concepts apply in both places: Data Sources provide the data, Templates define the output, and Reports are the generated files. The Data Source may be preconfigured for internal Lerian products or manually added for external databases. ## Behavior note *** This documentation update does not introduce a product behavior change. It clarifies how to operate the existing Reporter Module in the Console and where to use the API reference for integration work. # Setup Path Source: https://docs.lerian.studio/en/reporter/console/reporter-setup-path Follow the minimum setup path for operating Reporter from the Lerian Console — data sources, templates, first report generation, and download verification. Use this path when you need to make Reporter usable from the Console for the first time. The sequence depends on your data source model: if internal product Data Sources are already configured, start with Templates; if you need an external database, add and test the Data Source first. ## Before you start *** Read the [Reporter concepts map](/en/reporter/console/reporter-concepts-map) if you have not used Reporter before. It explains the operational chain: * **Data Sources** define where Reporter reads data from. * **Templates** define the structure and output format of the file. * **Reports** are generated from a Template and can be monitored, viewed, and downloaded. Before opening the setup screens, decide which report you want to produce first. For example: "monthly tax report in CSV", "daily operational report in PDF", or "regulatory XML file filtered by date range". That decision tells you whether the needed Data Source already exists, which Template is required, which output format is expected, and which filters you need. ## Prerequisites *** | Requirement | What must be ready | Where it is used | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | Reporter access | Your organization can access Reporter in the Console. | All Reporter pages | | Console permissions | Your user can manage Data Sources, Templates, and Reports for the organization. | Data Sources, Templates, Reports | | Data Source model | Whether the report uses an internal Data Source already configured for products such as Midaz or CRM, or an external database that must be added manually. | Setup path | | Database connection details | Host, port, database name, user, password, and any required network or SSL information. Required only when adding an external Data Source. | Data Sources | | Source data context | The database, table, fields, and filters that should feed the first report. | Data Sources, Reports | | Template input | Either an existing `.tpl` file or enough report structure to build one in the Template Builder. | Templates, Template Builder | | Output expectation | The required file format, such as CSV, XML, HTML, TXT, or PDF. | Templates, Reports | | API automation need | A decision on whether setup will stay manual in the Console or be automated through the API. | API reference | If one of these items is missing, stop there first. Reporter can only generate useful files when the Template, intended output, and relevant data connection are known. ## Choose the setup path *** Use this decision before following the steps: | Scenario | Start with | Why | | --------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | The report uses internal product data already configured in Reporter. | **Templates** | Internal Data Sources for products such as Midaz or CRM may already exist, so the operator does not need to create a connection first. | | The report uses an external or new database. | **Data Sources** | Reporter needs a manually configured and tested connection before templates or filters can use its fields. | | The report fails because data is missing or unreachable. | **Data Sources** | Test the connection and confirm schemas, tables, and fields before regenerating. | ## Minimum setup sequence *** Start from the Reporter overview and confirm that the team knows which report will be created first. Use the overview when you need to explain what each Reporter area is for. Go to [Reporter Module](/en/reporter/console/reporter-module) and [Reporter concepts map](/en/reporter/console/reporter-concepts-map). Check whether the needed Data Source already exists. Internal product Data Sources may already be configured for the environment. If the report depends on an external database or a missing connection, add and test that Data Source before building reports. On the **Data Sources** page, use existing connections when they are already present. Add a new connection only when the required source is not available, then run **Test Connection** before using it in a report. Use [Adding a Data Source](/en/reporter/console/adding-a-data-source) and [Testing a Data Source connection](/en/reporter/console/testing-a-data-source). The Template defines the report structure and output format. You can upload an existing `.tpl` file or build the Template visually in the Console. On the **Templates** page, use [Adding a template](/en/reporter/console/adding-template). If the operator needs to assemble the layout from blocks, use the [Template Builder](/en/reporter/console/template-builder). On the **Reports** page, create a report from the Template. Select the Template, apply filters when needed, review the setup, then generate the file. Use [Generating a report](/en/reporter/console/generating-a-report). This is the point where a Template becomes a Report; before generation, it is only a reusable definition. Generated reports start in `Processing`. Wait until the status reaches `Finished`, then view or download the output file. If the report ends in `Error`, inspect the details and re-run it after fixing the Data Source, Template, or filter inputs. Use [Downloading a report](/en/reporter/console/downloading-a-report) when the file is ready. If the report has a recurring deadline, use the **Overview** page to add the delivery reminder. This keeps upcoming and overdue deliveries visible without changing how reports are generated. Use the reminder only for operational follow-up. It does not create or schedule reports automatically. ## When to use the API reference *** Use the Console for the first operational setup: confirming or creating the Data Source, testing connections when needed, adding or building the Template, generating the first Report, and downloading the file. Use the [Reporter API reference](/en/reference/reporter/reporter-developer-quick-start) when you need to automate or integrate the same workflow, such as: * creating or listing Templates from another system; * creating reports programmatically; * checking report status without opening the Console; * downloading generated files through an integration; * validating or managing connections outside the operator workflow. Manual Console setup does not require the API reference. The API reference is for implementation work, automation, and troubleshooting with engineering teams. ## What happens next *** At the end of the setup path, you should have: * one relevant Data Source available for the report, either preconfigured internally or manually added and tested; * one Template that matches the required output format; * one generated Report with `Finished` status; * one downloaded file ready for delivery or validation; * optional delivery reminders in the Overview page when the report has recurring deadlines. After that, the normal operating loop is simple: keep the relevant Data Source healthy when the report depends on one, update the Template when the file structure changes, generate Reports when needed, and download the final output. ## Common sequencing mistakes *** Some environments already include internal Data Sources for Lerian products such as Midaz or CRM. Check the existing list before adding a duplicate external connection. The connection may exist, but credentials, network access, and schemas still need validation when the report depends on that source. Run **Test Connection** before relying on it. The Template should match the required file format. Confirm whether the operation needs CSV, XML, HTML, TXT, or PDF before using it to generate reports. Operators can complete the first setup in the Console. Use the API reference when the same workflow must be automated, integrated, or debugged with engineering support. ## Related links *** Use when the Reporter workflow must be automated or integrated through APIs. # Using the Template Builder Source: https://docs.lerian.studio/en/reporter/console/template-builder Build report templates visually by assembling blocks instead of writing .tpl code manually, and let the builder generate it for you. Use Template Builder to create or maintain a Template visually. The builder converts blocks into `.tpl` code, so operators can define report structure without writing the Template file manually. ## Objective *** Build a Template from visual blocks and save it for future Report generation. For example, a monthly transaction Template can use fixed text for the header, a **Loop** for each transaction row, **Variable** blocks for fields such as amount and date, and an **Aggregation** block for the total. ## When to use *** Use Template Builder when: * operators need to create a Template without editing `.tpl` code; * a Template Builder Template needs to be maintained visually; * the report structure depends on fields, loops, conditions, or totals from a Data Source; * the team wants to download the generated `.tpl` file after building it. Use `.tpl` upload instead when a technical team already created and reviewed the Template outside the Console. ## Before you start *** Confirm: * at least one relevant Data Source exists if the Template needs database fields. It can be an internal source already configured for the environment or an external source added from **Data Sources**; * the Data Source was tested when connection health or schema visibility is uncertain, so its tables and fields are available in the sidebar; * the expected output format is known: XML, HTML, CSV, TXT, or PDF; * the operator knows the report structure, such as required header, rows, totals, or conditional sections. Blocks define the output structure. Filters applied during Report generation define which records enter that structure. ## Step by step *** Go to the **Templates** page and click **Template Builder**. Edit the Template name in the header. Use a name that operators can identify during report generation. Select XML, HTML, CSV, TXT, or PDF. When **PDF** is selected, the generated template code uses HTML format and is converted to PDF during report generation. Use the blocks toolbar to add structure. Common blocks are **Text**, **Variable**, **Loop**, **Conditional**, and **Aggregation**. Use the Data Sources sidebar to browse schemas, tables, and fields. Click a field to add a **Variable** block for that field. Switch from **Visual** to **Code** to inspect the generated `.tpl` preview. The code view is for review and copy/download, not manual editing. Click **Save**. The builder validates required block fields, generates `.tpl` code, and saves the Template. ## Field guide *** ### Builder-level fields and controls | Field or control | What it does | Example | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | **Template name** | Required name in the builder header. It maps to `name` and becomes the saved Template label and generated `.tpl` filename prefix. | `Monthly transaction CSV` | | **Output format** | Required format. Use `xml`, `html`, `csv`, `txt`, or `pdf`. When `pdf` is selected, code generation uses HTML internally. | `pdf` | | **Visual** | Block editor mode for assembling the Template. Technical state: `viewMode = visual`. | Use while arranging blocks | | **Code** | Read-only preview of generated `.tpl` code. Technical state: `viewMode = code`. | Review generated `.tpl` | | **Download .tpl file** | Downloads the generated `.tpl` without saving a new Template. | `monthly-transaction-csv.tpl` | | **Save** | Validates blocks, generates `.tpl` code, and uploads or updates the Template. Requires a nonblank name and at least one block. | Save after blocks are valid | ### Block field guide | Block | Fields to configure | Effect | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | **Text** | **Content** (`content`) | Renders fixed text such as CSV headers, XML tags, or HTML fragments. | | **Variable** | **Data Source**, **Table**, **Field**, **Index (optional)**, **Filters** (`dataSource`, `table`, `field`, `index`, `filters[]`) | Inserts a field value. Inside a parent **Loop**, **Data Source** can be auto-filled from the loop iterator. | | **Loop** | **Iterator name** and **Iterable source** (`iteratorName`, `iterableSource`) | Repeats child blocks over a collection. Use `dataSource.table` format for the iterable source. | | **Conditional** | **Condition** and **Include else block** (`condition`, `hasElse`) | Renders child blocks only when the condition is true; optional else content can render when false. | | **Aggregation** | **Aggregation type**, **Source**, **Field**, optional grouping/order/result fields (`aggregationType`, `source`, `aggregationField`) | Produces totals, counts, averages, min/max, or **Last Item by Group** values from a source collection. | | **Calculation** | **Expression** (`expression`) | Renders a calculated value. Supported operators shown in the UI include `+`, `-`, `*`, `/`, `**`, and `%`. | | **Date/Time** | **Format** and **Date source** (`format`, `dateSource`) | Formats a date value, for example `YYYY-MM-DD` from `transaction.createdAt`. | | **Counter** | **Mode**, **Counter name**, and **Counter names** (`counterMode`, `counterName`, `counterNames[]`) | **Increment** advances one counter. **Display** outputs one or more counters. | | **Comment** | **Comment** (`commentText`) | Stores an internal Template note and does not appear in the final Report. | | **Section** | **Section title** (`sectionTitle`) | Groups child blocks to organize larger Templates. | | **With (Assign)** | **Variable name** and **Assignment expression** (`variableName`, `assignment`) | Creates a reusable variable from an expression for child blocks. | | **Expression** | **Expression** (`inlineExpression`) | Renders an inline expression such as `item.name\|upper`. | | **Custom Tag** | **Tag name** and **Tag arguments** (`tagName`, `tagArgs`) | Emits an advanced Template tag such as `include` with arguments like `"header.html"` or `key=value`. | ## Block types *** | Block | What it represents in practice | | ----------------- | ------------------------------------------------------------------------------------ | | **Text** | Fixed content that always appears in the output, such as a header or label. | | **Variable** | A value from a data source field, such as document number, amount, or status. | | **Loop** | A repeated section, such as one row for each transaction. | | **Conditional** | Content that appears only when a rule is true, such as `account.status == "active"`. | | **Aggregation** | A total, count, average, min, max, or last item by group. | | **Calculation** | A calculated value, such as `value * 1.05`. | | **Date/Time** | A formatted date or time value. | | **Counter** | A row number or sequence value. | | **Comment** | Internal Template note that does not appear in the generated Report. | | **Section** | A named group of blocks for organizing larger Templates. | | **With (Assign)** | A reusable variable created from an expression. | | **Expression** | An inline expression rendered into the output. | | **Custom Tag** | A custom Template tag when advanced Template syntax is needed. | ## Working with blocks *** * Click a block type in the toolbar to add it to the canvas. * Drag blocks to reorder the output. * Configure each block inline. For example, a **Variable** needs a source and field, while a **Conditional** needs a condition. * Use **Inline** when the block should render without a line break after it. * Use **Trim whitespace** to remove extra spaces around block output. * Use **Duplicate** to copy a configured block. * Use **Delete** to remove a block from the Template. Container blocks such as **Loop**, **Conditional**, **Section**, and **With** can hold child blocks. Use them when the output needs repeated or grouped content. ## Expected result *** After saving, the Template appears on the **Templates** page and can be selected in the **Generate Report** wizard. You can also download the generated `.tpl` file from the builder. ## Common errors and care points *** The sidebar only shows configured Data Sources. If the expected internal source is not listed, confirm setup with the Reporter administrator. If the report needs an external database, add and test that Data Source before using database fields in the Template. The builder validates blocks during save. If a block is missing a source, field, condition, or expression, fix that block before saving. Template blocks define the file structure. Report filters are selected later during report generation and decide which records enter the output. PDF output is generated from HTML. Build the Template as HTML-compatible content when the final output format is PDF. ## Next steps *** * Use [Generate a Report](/en/reporter/console/generating-a-report) to test the Template with real filters. * Use [Update a Template](/en/reporter/console/updating-a-template) to edit the Template later. * Use [Add a Template](/en/reporter/console/adding-template) if you need to upload a prepared `.tpl` file instead. There is no separate API endpoint for the visual builder. Use [Upload template endpoint](/en/reference/reporter/upload-template) with a prepared `.tpl` file. # Test a Data Source Connection Source: https://docs.lerian.studio/en/reporter/console/testing-a-data-source Verify that a Data Source's credentials and network settings are correct before relying on it for regulatory report generation in the Reporter module. Use this page to confirm that Reporter can reach a Data Source and load its schema. The Data Source can be an internal connection already configured for a Lerian product or an external database connection added from the Console. ## Objective *** Validate connectivity before operators rely on the Data Source in Templates or Reports. For example, test after creating `production-postgres`, after a password rotation, or before generating a monthly report that depends on a recently changed schema. ## When to use *** Use this task: * immediately after adding an external Data Source; * when checking an existing internal Data Source before using it in a Template or filter; * after editing host, credentials, schema, or SSL settings; * before generating Reports that depend on the connection; * when no tables appear in the schema browser; * when Report generation fails because of connection or schema errors. ## Before you start *** Confirm: * the Data Source exists on the **Data Sources** page; * the expected host, port, database name, username, schema, and SSL settings are configured; * the database should be reachable from Reporter; * the database user has permission to read the required tables. A successful test confirms access at that moment. It does not guarantee future Reports will succeed if credentials, network rules, or database permissions change later. ## Step by step *** Go to **Data Sources**. Locate the card for the Data Source you need to test. Click **Test Connection**. The status changes to **Connecting** while Reporter tests the connection. If the test succeeds, the status changes to **Connected** and latency is displayed, such as `Latency: 45ms`. If it fails, the status changes to **Disconnected** and an error message appears. Expand the connection card to view the schemas, tables, and fields returned for the connection. ## Field guide *** ### Test action and result fields | Field or status | What it means | Example | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | **Test Connection** | Action on the Data Source card. The Console uses the selected `connectionId` to call the test endpoint. | Test `production-postgres`. | | **Connecting** | Local status while the test request is pending. Wait for the final result before editing or generating a Report. | **Connecting** | | **Connected** | Successful result. Reporter returned `connected` or `success`, and the Data Source is usable at that moment. | **Connected** with `Latency: 45ms`. | | **Disconnected** | Failed result or request error. Check host, port, credentials, network access, schema, and SSL settings. | **Disconnected** after `authentication failed`. | | Latency | `latencyMs`, returned in milliseconds after a successful test. It is a signal, not a guarantee of future Report generation speed. | `45` | | Test message | `message` returned by Reporter, usually most useful on failure. Use it as the first clue for what to fix. | `authentication failed` | ### Schema fields shown after testing | Schema item | What it shows | Example | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | Tables | `tables[].name` returned by schema validation. These are the tables operators can use in Template Builder and report filters. | `transactions` | | Fields | `tables[].fields[]` inside each table. These fields can be inserted as Template variables or used as Report filters. | `amount`, `created_at` | | No schema loaded | Empty `tables` array or schema load error. The connection may work, but table permissions or schema names still need correction. | No tables discovered. | ## Expected result *** The Data Source status is **Connected**, schema information is available, and operators can safely use its fields in Template Builder and report filters. ## Common errors and care points *** Check schema names and database permissions. The database user may connect but not have access to the tables needed for reporting. Reopen the Data Source and confirm username, password, host, port, and SSL settings. Network rules, rotated passwords, and database permissions can change. Re-test when generation errors appear. ## Next steps *** * Use [Template Builder](/en/reporter/console/template-builder) after schemas and fields are visible. * Use [Generate a Report](/en/reporter/console/generating-a-report) after the Data Source is healthy. * Use [Edit a Data Source](/en/reporter/console/editing-a-data-source) if the test fails because settings are wrong. Test a Data Source connection programmatically. Validate schema access for tables and fields. # Update a Template Source: https://docs.lerian.studio/en/reporter/console/updating-a-template Update a Template's name, output format, or replace the uploaded .tpl file directly from the Lerian Console — no need to delete and recreate the template. Use this page when a Template should remain available, but something about its name, output format, layout, or source file needs to change. ## Objective *** Update the reusable Template that future Reports will use. For example, update a Template when the report owner changes the required CSV columns, the file must be generated as XML instead of TXT, or the Template name is too generic for operators to identify safely. ## When to use *** Use this task when: * the Template is still needed; * the name or output format is wrong; * an uploaded `.tpl` file must be replaced; * a Template Builder Template needs block-level edits; * future Reports should use the corrected Template. Do not use this task to change a Report that was already generated. Generated Reports are historical files. Correct the source setup and generate a new Report. ## Before you start *** Check what kind of Template you are editing: * **Uploaded Template**: update the name, output format, or replace the `.tpl` file. * **Template Builder Template**: open the builder, edit the blocks, and save the generated `.tpl`. Confirm that the change is intended for future Reports. Reports already generated from the old Template stay unchanged. Replacing a `.tpl` file affects every future Report generated from this Template. Validate the new file before using it for a reporting cycle. ## Step by step *** Go to **Templates** and locate the Template you need to update. Click the three dots () in the **Actions** column and select **Edit**. In **Edit Template**, update the fields that changed. For uploaded Templates, you can change **Template Name**, **Output Format**, and **Template File (.tpl)**. Click **Update Template**. Generate a new Report with the updated Template and confirm that the output matches the expected structure. Templates created with **Template Builder** open in the builder when edited. Templates uploaded as `.tpl` files open in the upload/edit wizard. ## Field guide *** | Field | What to change | Example | | ------------------------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | **Template Name** | Required Console label for operators. It maps to `name` in the Console and to `description` in the Reporter API. | `Monthly tax XML` | | **Output Format** | File type for future Reports. Use one of `csv`, `xml`, `html`, `txt`, or `pdf`. | `xml` | | **Template File (.tpl)** | Optional replacement file. If supplied, it must be `.tpl`, larger than 0 bytes, and no larger than 5 MB. | Replace `tax-v1.tpl` with `tax-v2.tpl` | | Builder blocks | Visual structure for builder-created Templates. Saving requires at least one valid block. | Add a **Total amount** **Aggregation** block | ## Expected result *** The Template stays on the **Templates** page with the updated configuration. Future Reports generated from it use the new name, output format, and Template structure. ## Common errors and care points *** Updating a Template does not rewrite Reports already generated. Generate a new Report after correcting the Template. If the `.tpl` file structure and **Output Format** do not match the expected delivery format, operators can generate the wrong file type. Names like `Template 1` or `New Template` are hard to distinguish during report generation. Use a name that includes the report purpose or period. ## Next steps *** * Use [Generate a Report](/en/reporter/console/generating-a-report) to test the updated Template. * Use [Correct a Report](/en/reporter/console/editing-a-report) if the issue was found in a generated Report. * Use [Delete a Template](/en/reporter/console/deleting-a-template) only if the Template is no longer needed. [Update templates endpoint](/en/reference/reporter/update-templates) # Trial Balance Report with Midaz and Reporter Source: https://docs.lerian.studio/en/reporter/creating-trial-balance-report Produce a trial balance from your Midaz Ledger using Reporter and export it as XML, TXT, HTML, or PDF — no extra conversion tools needed. Security, compliance, and clarity in financial reporting start with your ledger. **Midaz** is a financial ledger at its core — meaning it already stores all the data required to produce a trial balance, from account structures to transaction histories. While Midaz provides the raw data, the **Reporter** product transforms this data into ready-to-share outputs. With Reporter, you can generate trial balances and export them directly in formats such as **XML, TXT, HTML, and PDF** — without needing extra conversion tools. # Key concepts *** Before diving into the step-by-step process, it's important to understand how Midaz structures data for regulatory reporting: ## OperationRoute and the code field *** The **OperationRoute** entity is central to COSIF mapping. Each OperationRoute has a code field (max 100 characters) designed specifically to store external reference codes such as COSIF account codes. When transactions are processed, each operation is associated with an OperationRoute. By aggregating operations grouped by OperationRoute.code, you can calculate balances per COSIF account — which is exactly what a trial balance requires. ```text theme={null} OperationRoute ├── code: "1.1.1.10-8" ← COSIF code for aggregation ├── title: "Checking Account Debit" ├── operationType: "source" | "destination" └── account: AccountRule ← Validation rules ``` ## TransactionRoute *** A **TransactionRoute** groups multiple OperationRoutes (sources and destinations) into a reusable template. When creating transactions, you reference a TransactionRoute, and Midaz validates that operations follow the defined rules. # Step-by-step process *** ## 1. Prepare your Chart of Accounts in Midaz *** Configure your **Organizations, Ledgers, Assets, Account Types, and Accounts** according to your business needs. The key entities for trial balance generation are: * **Account Types** — define the nature of each account (asset, liability, equity, revenue, expense) using the keyValue field. * **OperationRoutes** — map each type of operation to a COSIF code via the code field. * **TransactionRoutes** — group OperationRoutes into transaction templates with validation rules. ## 2. Create OperationRoutes with COSIF codes *** For each COSIF account in your chart of accounts, create corresponding OperationRoutes with the COSIF code in the code field. Example OperationRoutes for a Brazilian financial institution: OperationRoute for checking account debits (source): ```json theme={null} { "title": "Checking Account Withdrawal", "description": "Debit operations from personal checking accounts", "code": "1.1.1.10-8", "operationType": "source", "account": { "ruleType": "account_type", "validIf": ["checking_account_pf", "checking_account_pj"] } } ``` OperationRoute for checking account credits (destination): ```json theme={null} { "title": "Checking Account Deposit", "description": "Credit operations to personal checking accounts", "code": "1.1.1.10-8", "operationType": "destination", "account": { "ruleType": "account_type", "validIf": ["checking_account_pf", "checking_account_pj"] } } ``` OperationRoute for investment account: ```json theme={null} { "title": "Investment Account Movement", "description": "Operations involving investment accounts", "code": "1.2.3.00-0", "operationType": "source", "account": { "ruleType": "account_type", "validIf": ["investment_account"] } } ``` Common COSIF mappings: | COSIF Code | Description | Account Types | | ---------- | ------------------------------- | --------------------- | | 1.1.1.10-8 | Checking Accounts - Individuals | checking\_account\_pf | | 1.1.1.20-5 | Checking Accounts - Companies | checking\_account\_pj | | 1.2.3.00-0 | Investment Accounts | investment\_account | | 7.1.1.00-0 | Service Revenue | revenue\_services | | 8.1.1.00-0 | Administrative Expenses | expense\_admin | ## 3. Create TransactionRoutes *** Group your OperationRoutes into TransactionRoutes that represent common transaction patterns. When creating a TransactionRoute, you pass an array of OperationRoute UUIDs. When retrieving a TransactionRoute via the API, it returns the full OperationRoute objects with all their details. ```json theme={null} { "title": "Pix Transfer Between Accounts", "description": "Internal Pix transfer from one checking account to another", "operationRoutes": [ "uuid-of-checking-account-source-route", "uuid-of-checking-account-destination-route" ] } ``` ## 4. Process transactions with routes *** When creating transactions, reference the TransactionRoute. Each operation will be associated with its corresponding OperationRoute and COSIF code: ```json theme={null} { "code": "PIX_TRANSFER_001", "description": "Pix lunch payment", "route": "uuid-of-pix-transfer-route", "send": { "asset": "BRL", "value": 4550, "source": { "from": [{ "accountAlias": "@joao_silva_cc" }] }, "distribute": { "to": [{ "accountAlias": "@restaurante_abc" }] } }, "metadata": { "pixKey": "12345678901", "description": "Lunch" } } ``` The `asset` and `value` are defined at the `send` level. Individual operations in `from` and `to` use `accountAlias` (not `account`). The value is expressed in the smallest unit of the asset (e.g., cents for BRL, so 4550 = R\$ 45.50). The COSIF mapping happens automatically through the route: ```text theme={null} Transaction uses route → TransactionRoute ↓ OperationRoutes[] ↓ source: code = "1.1.1.10-8" (COSIF) destination: code = "1.1.1.20-5" (COSIF) ``` ## 5. Understanding the data flow *** Midaz stores all the financial data needed for trial balance generation: * **Operations** — each operation includes a `route` field containing the OperationRoute UUID * **OperationRoutes** — contain the `code` field with COSIF codes for aggregation * **Account Balances** — current positions per account **How aggregation works:** Each Operation stores a `route` field that references the OperationRoute UUID (not the COSIF code directly). To aggregate by COSIF code, Reporter joins operations with their corresponding OperationRoutes to access the `code` field: ```text theme={null} Operation.route (UUID) → OperationRoute.id → OperationRoute.code (COSIF) ``` **Available Midaz APIs:** ```text theme={null} GET /v1/organizations/{org_id}/ledgers/{ledger_id}/accounts/{account_id}/operations ``` Query parameters for filtering include `cursor`, `limit`, `type`, and `direction`. To narrow results to a specific OperationRoute, use `route_id` (UUID) or `route_code` (such as the COSIF code). Operations are retrieved per account. Reporter connects directly to the Midaz database for report generation, enabling efficient batch queries and aggregations. The APIs above are available for custom integrations and ad-hoc queries. ## 6. Generate your report with Reporter *** Reporter uses a template-driven architecture to generate reports. The process involves: 1. **Select or create a template** — templates use Pongo2 syntax (Django-like) with custom aggregation tags 2. **Configure data sources** — Reporter queries Midaz database tables directly 3. **Apply filters** — specify date ranges, account types, or other criteria 4. **Generate the report** — Reporter aggregates data and renders the template 5. **Export** in your chosen format (XML, TXT, HTML, PDF, CSV) **How Reporter aggregates data:** Reporter templates support built-in aggregation tags for calculating balances: * `{% sum_by collection by "field" %}` — sum values by field * `{% count_by collection if condition %}` — count records matching condition * `{% calc expression %}` — arithmetic calculations **Example template snippet for COSIF aggregation:** ``` {% for route in operation_routes %} {{ route.code }} | Debits: {% sum_by operations by "amount" if operation.route == route.id and operation.type == "DEBIT" %} | Credits: {% sum_by operations by "amount" if operation.route == route.id and operation.type == "CREDIT" %} {% endfor %} ``` Reporter includes ready-to-use templates for different reporting needs. Use the [**CADOC 4010**](/en/reporter/cadoc-4010-and-4016) template when generating reports for **BACEN regulatory submission**, as it follows the official required structure. ## 7. Export and integrate *** You can: * Export a **ready-to-share PDF** for auditors and regulators * Generate **XML** files formatted for BACEN submission * Integrate with your existing reporting workflows or accounting systems # Practical example — Complete Midaz setup *** Below is a fictional institution, **DigitalBank**, showing how Midaz entities connect to support a trial balance: ```json theme={null} { "legalName": "DigitalBank S.A.", "legalDocument": "12.345.678/0001-90", "address": { "line1": "Av. Faria Lima, 3064", "line2": "12th floor", "zipCode": "01451-000", "city": "Sao Paulo", "state": "SP", "country": "Brazil" }, "metadata": { "cnae": "6422-1", "licenseNumber": "BCB-2024-001", "foundedAt": "2024-01-15" } } ``` ```json theme={null} [ { "name": "Brazilian Real", "type": "currency", "code": "BRL", "status": { "code": "ACTIVE" }, "metadata": { "symbol": "R$", "centralBank": "BACEN" } } ] ``` ```json theme={null} [ { "name": "Personal Checking Account", "keyValue": "checking_account_pf", "description": "Checking accounts for individual customers" }, { "name": "Business Checking Account", "keyValue": "checking_account_pj", "description": "Checking accounts for business customers" }, { "name": "Investment Account", "keyValue": "investment_account", "description": "Investment and savings accounts" } ] ``` COSIF codes are defined in OperationRoutes via the `code` field, **not** in Account Types. Account Types define the nature of accounts and are used by OperationRoutes for validation rules. ```json theme={null} [ { "title": "PF Checking - Debit", "code": "1.1.1.10-8", "operationType": "source", "account": { "ruleType": "account_type", "validIf": ["checking_account_pf"] } }, { "title": "PF Checking - Credit", "code": "1.1.1.10-8", "operationType": "destination", "account": { "ruleType": "account_type", "validIf": ["checking_account_pf"] } }, { "title": "PJ Checking - Debit", "code": "1.1.1.20-5", "operationType": "source", "account": { "ruleType": "account_type", "validIf": ["checking_account_pj"] } }, { "title": "PJ Checking - Credit", "code": "1.1.1.20-5", "operationType": "destination", "account": { "ruleType": "account_type", "validIf": ["checking_account_pj"] } } ] ``` ```json theme={null} { "title": "Pix Transfer PF to PJ", "description": "Pix transfer from personal to business account", "operationRoutes": [ "uuid-pf-checking-debit", "uuid-pj-checking-credit" ] } ``` ```json theme={null} { "code": "PIX_TRANSFER_001", "description": "Pix lunch payment", "route": "uuid-pix-transfer-pf-to-pj", "send": { "asset": "BRL", "value": 4550, "source": { "from": [{ "accountAlias": "@joao_silva_cc" }] }, "distribute": { "to": [{ "accountAlias": "@restaurante_abc" }] } }, "metadata": { "pixKey": "12345678901", "purpose": "Lunch payment" } } ``` # Trial Balance output example *** After joining operations with their OperationRoutes and aggregating by the `code` field, your trial balance would look like: | COSIF Code | Description | Debits (R\$) | Credits (R\$) | Balance (R\$) | | ---------- | ------------- | -------------- | -------------- | ------------- | | 1.1.1.10-8 | Checking - PF | 150,000.00 | 145,000.00 | 5,000.00 | | 1.1.1.20-5 | Checking - PJ | 89,000.00 | 92,500.00 | -3,500.00 | | 1.2.3.00-0 | Investments | 50,000.00 | 48,000.00 | 2,000.00 | | **Total** | | **289,000.00** | **285,500.00** | **3,500.00** | Using Reporter, this data can be formatted into: * **XML** — structured for BACEN regulatory submission * **PDF** — professional report for auditors and stakeholders * **TXT** — flat file for legacy system integration The report consolidates ledger data by COSIF code, presenting balances grouped by accounting sections (Assets, Liabilities, Equity, Revenues, and Expenses), along with subtotals and a global total. This output reflects the result of automated aggregation and validation performed on top of Midaz data, ready for audit, compliance, and regulatory use. # Summary *** The key to generating accurate trial balances with Midaz is proper use of the `code` field in OperationRoutes: 1. **Create OperationRoutes** with COSIF codes in the `code` field 2. **Group into TransactionRoutes** for transaction validation 3. **Process transactions** referencing the appropriate routes 4. **Join operations with OperationRoutes** using the `route` field (UUID) to access COSIF codes 5. **Aggregate by OperationRoute.code** to calculate balances per COSIF account 6. **Generate reports** with Reporter using templates that perform the aggregation automatically This approach ensures: * Regulatory compliance with COSIF standards * Automatic balance aggregation by account code * Validation of transaction operations * Flexible reporting in multiple formats # Related documentation *** * [OperationRoutes API Reference](/en/reference/midaz/create-an-operation-route) * [TransactionRoutes API Reference](/en/reference/midaz/create-transaction-route) * [Account Types API Reference](/en/reference/midaz/create-an-account-type) * [Operations API Reference](/en/reference/midaz/list-operations-by-account) * [Reporter Overview](/en/reporter/what-is-reporter) * [CADOC 4010 and 4016](/en/reporter/cadoc-4010-and-4016) # How report generation works Source: https://docs.lerian.studio/en/reporter/how-report-generation-works The path a Reporter report takes: the request, extraction against the configured data sources, the render, the file in object storage, and the download. Report generation has two halves. The **manager** accepts your request, checks it, and hands it to a queue. The **worker** extracts the data, renders the template, and writes the file. Reporter answers your request as soon as the first half completes, so you poll or listen for the second. Both halves ship in one binary. A deployment runs the API surface, the worker surface, or both, and a queue carries the work between them. This page follows one report along that path. ## The path end to end *** You call [Create a report](/en/reference/reporter/create-report) with a template identifier and the row filters you want. Reporter validates the request and stores the report as `Processing`. The worker queries each data source the template names, one section per data source, and keeps only the fields in the template's field map. The extracted rows become the context for the template. Reporter renders the document in the template's output format. Reporter writes the rendered file to the configured object storage bucket and records the terminal status of the report. You call [Download a report](/en/reference/reporter/download-report) and receive the file with its content type and filename. ## What the manager does *** The manager owns everything that happens before the queue. It runs four checks in order, and any one of them can reject the request outright. **It applies idempotency.** Send an `X-Idempotency` header to name the request yourself. Without one, Reporter derives the key from the request body. A repeat of an in-flight request is rejected as a duplicate. A repeat of a completed request returns the original report and marks the response with `X-Idempotency-Replayed: true`. The window is 30 seconds. **It resolves the template.** Reporter loads the field map, the output format, and the description that belongs to the template identifier. An unknown identifier fails here. **It validates your filters.** Every field you filter on is checked against the live schema of its data source. A wrong field name fails the request instead of producing an empty report. **It records the report and queues the work.** The report is stored as `Processing` and the response returns immediately with that status. Reporter then publishes the generation command for the worker. The report keeps its own copy of the output format and the template description. That snapshot is what makes an old report reproducible after the template moves on. ## What the worker does *** The worker consumes the command and owns the rest of the path. A queue can deliver the same command twice. The worker therefore reads the report before it starts a run: a report that already settled as `Finished` or `Error` is left as it is, rather than generated again. It then loads the template file from object storage and extracts data. **Each data source is one section.** A section that fails does not stop the others. The worker collects the outcome of every section and only then decides the status of the report. When every section fails, Reporter records the failure and skips the render. No misleading file reaches the bucket. The render step turns the surviving sections into the document. When the output format is PDF, Reporter renders HTML first and converts it through a headless browser pool. The finished bytes go to the bucket, the terminal status goes to the report record, and a terminal event goes to the event stream. ## Report states *** A report reaches one of four states. | State | Meaning | Artifact | | ------------ | ------------------------------------------------------------- | ----------------- | | `Processing` | Accepted and queued, or in extraction | Not yet | | `Finished` | Every section returned data | Ready to download | | `Partial` | Some sections failed, others returned data | Incomplete | | `Error` | Every section failed, or the request never reached the worker | None | Poll [Check report status](/en/reference/reporter/check-report-status) or consume the terminal event. A `Partial` report carries a per-section outcome in its metadata, which names the data sources that did not answer: fix those data sources, then generate the report again. [Download a report](/en/reference/reporter/download-report) serves the artifact once the report is `Finished`. ## Time limits on the path *** Each stage carries its own limit, and an operator sets them per deployment. | Limit | Default | Applies to | | ---------------------- | ----------- | --------------------------------- | | Idempotency window | 30 seconds | Repeat detection on the request | | Extraction timeout | 300 seconds | One extraction run | | Extraction concurrency | 4 | Parallel queries inside one run | | PDF conversion timeout | 90 seconds | One HTML-to-PDF conversion | | PDF worker pool | 2 | Concurrent conversions per worker | Volume limits bound the same run: 10 data sources, 50 tables per data source, 200 fields per table, and 100 MiB of extracted result. See [Environment variables](/en/reporter/reporter-environment-variables) for the settings behind each one. ## Where deadlines fit *** A deadline is a due date with a recurrence rule, and it can name the template that satisfies it. It sits beside the generation path rather than on it. A deadline never starts a report. You generate the report through the request path above, then mark the deadline delivered. Reporter recomputes a deadline on read. A recurring obligation that carries a delivery mark rolls forward to its next due date on the first read after the current date passes, and that roll-forward clears the mark. The notifications operation returns the deadlines inside their alert window when you ask for them. See [Managing deadlines](/en/reporter/managing-deadlines). ## Storage and download *** Rendered files live in one S3-compatible bucket, under the `reports/` prefix. Reporter stores each file under the template identifier and names it after the report, so files stay grouped by the template that produced them. Download returns the bytes with the content type of the output format and a filename built from the report identifier. The download works after the template is deleted, because the report holds its own format snapshot. A report is created once and then retained. Its filters, its status, and its file describe exactly the request that produced them, which is what makes an old report evidence of what you reported at the time. Reporter keeps rendered files in the bucket. Set retention with a lifecycle policy on the bucket itself. ## Next steps *** The field map, the data context, and the two kinds of filter. Upload a template, generate a report, and download the result. Track a regulatory obligation and mark it delivered. One binary, two surfaces, and the stores they share. # Managing deadlines Source: https://docs.lerian.studio/en/reporter/managing-deadlines Track when regulatory and business reports are due with Reporter deadlines — create recurring obligations, monitor their status, and mark them delivered. A **deadline** is Reporter's tracking layer for report deliveries. Where templates define *how* a report looks and report generation produces the *output*, a deadline records *when* a report is due and whether it has been delivered. Each deadline represents a delivery obligation — typically a regulatory filing or a recurring business report — and can optionally be tied to the template used to fulfill it. Deadlines don't generate reports themselves. They sit alongside the [reporting lifecycle](/en/reporter/reporter-quick-start#the-reporting-lifecycle) as an operational tracker, so teams can see at a glance what's `pending`, `overdue`, or already `delivered`. ## Why deadlines exist *** Report generation answers *how* and *what*. Deadlines answer *when* and *whether it was met*. Without a tracking layer, a team can produce perfect reports and still miss a filing window, because nothing in the reporting engine itself knows that a report was *due*. Deadlines solve a delivery-compliance problem. They turn recurring reporting obligations into tracked, dated commitments so that nothing slips silently: * **Regulatory reporting obligations** — many filings must reach a regulator on a fixed schedule. A deadline records that obligation, its recurrence, and its delivery state, so a missed or late filing is visible before it becomes a compliance incident. * **Internal SLAs** — recurring business reports often carry internal commitments ("finance receives the monthly close pack by the 5th"). Deadlines make those commitments explicit and measurable. * **Audit trails for delivery** — because each deadline records `deliveredAt` and moves through `pending` → `overdue` → `delivered`, it leaves an auditable history of *when* each obligation was satisfied, not just that a report exists. ## Who uses deadlines *** Deadlines are a business and compliance tool layered on top of the reporting engine. Typical users include: * A **fintech delivering regulatory reports to BACEN** on fixed monthly or annual schedules, using deadlines to guarantee each filing window is tracked and met. * A **treasury or finance team** tracking recurring monthly report deliveries, using the calendar and status view to confirm each routine output went out on time. * A **compliance officer** monitoring overdue obligations across the organization, filtering by `overdue` status to catch anything at risk before it escalates. For these teams, the value isn't in producing the report — that's the reporting engine's job — but in *knowing the obligation landscape*: what's coming up, what's late, and what's done. ## How deadlines fit the Reporter workflow *** Deadlines wrap around reports and data sources to add a **delivery status layer** on top of the reporting engine. Data Sources provide the data, Templates define the output, and the reporting lifecycle produces the file. A deadline sits above all of that: it links (optionally) to the template that fulfills the obligation, watches the due date, and exposes a single status that tells the business whether the obligation has been met. In practice this means deadlines are *non-intrusive*. They never trigger, generate, or submit a report — they observe and record. You still generate reports through the normal lifecycle; the deadline is simply the tracker that turns that activity into a clear delivery status the rest of the organization can rely on. ## What a deadline tracks *** Each deadline captures the due date for a report obligation plus the metadata your team needs to manage it: | Field | Description | | ------------------ | ------------------------------------------------------------------------------------------ | | `name` | Human-readable name of the deadline (for example, *Monthly Regulatory Report*). | | `description` | Optional longer description of the obligation. | | `type` | Classification of the deadline, such as `regulatory` or `custom`. | | `frequency` | How often the deadline recurs, such as `monthly` or `annual`. | | `dueDate` | When the report is due, in RFC 3339 format. | | `color` | Hex color used to visually identify the deadline in dashboards. | | `notifyDaysBefore` | Number of days before the due date when notifications start. | | `monthsOfYear` | Months (1–12) in which the deadline applies. | | `templateId` | Optional UUID of the [template](/en/reporter/using-reporter) used to fulfill the deadline. | | `active` | Whether the deadline is currently active. | Reporter also maintains read-only fields on each deadline — `id`, `status` (`pending`, `overdue`, or `delivered`), `deliveredAt`, `templateName`, `createdAt`, and `updatedAt`. Include an `Authorization: Bearer ` header on every deadline request when [Access Manager](/en/platform/access-manager/access-manager) is enabled in your environment. ## Creating a deadline *** Create a deadline with the [Create a Deadline](/en/reference/reporter/create-deadline) endpoint (`POST /v1/deadlines`). The required fields are `name`, `type`, `frequency`, `dueDate`, and `color`. The remaining fields are optional — set `templateId` to link the deadline to the template that fulfills it, and `notifyDaysBefore` to control when reminders begin. ```json theme={null} { "name": "Monthly Regulatory Report", "description": "Monthly regulatory compliance report", "type": "regulatory", "frequency": "monthly", "dueDate": "2026-03-31T23:59:59Z", "color": "#FF5733", "notifyDaysBefore": 5, "monthsOfYear": [1, 6], "templateId": "00000000-0000-0000-0000-000000000000", "active": true } ``` A successful request returns `201 Created` with the full deadline, including its generated `id` and an initial `status`. API reference: [Create a Deadline](/en/reference/reporter/create-deadline) ## Listing deadlines *** Retrieve deadlines with the [Retrieve Deadlines](/en/reference/reporter/retrieve-deadlines) endpoint (`GET /v1/deadlines`). Results are paginated and can be filtered by status. | Query parameter | Description | Default | | --------------- | ----------------------------------------------- | ------- | | `status` | Filter by `pending`, `overdue`, or `delivered`. | — | | `limit` | Number of records per page (integer ≥ 1). | `10` | | `page` | Page number to return (integer ≥ 1). | `1` | For example, to list overdue deadlines, ten per page: ``` GET /v1/deadlines?status=overdue&limit=10&page=1 ``` The response contains an `items` array plus `page`, `limit`, and `total` for pagination. API reference: [Retrieve Deadlines](/en/reference/reporter/retrieve-deadlines) ## Updating a deadline *** Update an existing deadline with the [Update a Deadline](/en/reference/reporter/update-deadline) endpoint (`PATCH /v1/deadlines/{id}`). Only the fields included in the request body are changed, so you can send a partial payload — for example, to push back a due date or deactivate a deadline: ```json theme={null} { "dueDate": "2026-06-30T23:59:59Z", "notifyDaysBefore": 10, "active": false } ``` A successful request returns `200 OK` with the updated deadline. API reference: [Update a Deadline](/en/reference/reporter/update-deadline) ## Deleting a deadline *** Remove a deadline you no longer need to track with the [Delete a Deadline](/en/reference/reporter/delete-deadline) endpoint (`DELETE /v1/deadlines/{id}`). A successful request returns `204 No Content`. API reference: [Delete a Deadline](/en/reference/reporter/delete-deadline) ## Marking a deadline as delivered *** When the report behind a deadline has been filed or sent, mark the deadline as delivered with the [Deliver a Deadline](/en/reference/reporter/deliver-deadline) endpoint (`PATCH /v1/deadlines/{id}/deliver`). This is the action that closes the loop in the tracking workflow: it moves the deadline's `status` to `delivered` and stamps `deliveredAt`. ```json theme={null} { "delivered": true } ``` Because `delivered` is a boolean, the same endpoint can also reverse the action — send `"delivered": false` to reopen a deadline that was marked delivered by mistake. A successful request returns `200 OK` with the updated deadline. Delivering a deadline is a tracking action only. It records that the obligation was met — it does not generate or submit the underlying report. Generate the report through the [reporting lifecycle](/en/reporter/reporter-quick-start), then mark the deadline delivered to keep your tracker accurate. API reference: [Deliver a Deadline](/en/reference/reporter/deliver-deadline) ## How deadlines fit the workflow *** A typical obligation moves through these states: Register the obligation with its due date, frequency, and optional template. It shows as `pending` until the due date, then `overdue` if the report hasn't been delivered in time. Produce the report through the normal [reporting lifecycle](/en/reporter/reporter-quick-start), using the linked template. Call the deliver endpoint to set `status` to `delivered` and record `deliveredAt`. ## Next steps *** Walk through the reporting lifecycle that deadlines track. Build the templates that fulfill your deadlines. Ready-to-use templates for Brazilian regulatory reporting. Full request and response reference for every deadline endpoint. # Reporter architecture Source: https://docs.lerian.studio/en/reporter/reporter-architecture How Reporter runs: one binary whose RUN_MODE selects the API surface, the worker surface, or both, the queue between them, and the stores they share. Reporter ships as **one binary**. `RUN_MODE` selects which surfaces that binary serves: `api`, `worker`, or `all`. The API and the worker are two roles of the same program, not two products, and they are built, versioned, and released together. That single fact shapes everything else on this page. You choose a topology at deploy time by setting an environment variable, not by assembling separate services. ## Two surfaces, one binary *** | Surface | `RUN_MODE` | Serves | | ------- | ---------- | ------------------------------------------------------------------------------------------------------------------- | | API | `api` | Every REST operation on port `4005`, plus `/health`, `/readyz`, and `/version`. | | Worker | `worker` | No REST surface. Consumes the report queue. A small health server on `HEALTH_PORT` carries `/health` and `/readyz`. | | Both | `all` | Both surfaces in one process. | ```bash theme={null} # API surface RUN_MODE=api SERVER_ADDRESS=:4005 # Worker surface RUN_MODE=worker HEALTH_PORT=4006 ``` Run `all` when one process is enough, which is the usual choice for local development and small deployments. Split the roles into two deployables when report generation needs to scale on its own: rendering a large PDF costs far more than accepting the request that asked for it, and separate deployments let you size each side to its own load. ## The path a report takes *** A `POST` to `/v1/reports` names a template and its filters. Reporter takes an idempotency lock first, keyed on the `X-Idempotency` header when you send one and on a hash of the request body when you do not. A duplicate that is still in flight is rejected; a duplicate of a completed request replays the original report and marks the response as a replay. Reporter loads the template's field map and output format, checks the filter fields against the live schema of each data source, and writes the report with status `Processing`. It publishes a command message to the internal RabbitMQ queue and answers `201 Created` with the `Processing` report. The caller is done here. Rendering has not started. The worker consumes the command, skips it if the report already settled as `Finished` or `Error`, loads the template from object storage, extracts the data for each data source, renders the document, converts it to PDF when the format calls for it, and uploads the artifact. It writes `Finished`, `Partial`, or `Error` and emits the matching event. The download operation serves the artifact once the report is `Finished`. ## The queue between them *** The API and the worker communicate over one RabbitMQ queue that carries report commands in one direction, with a dead-letter queue behind it. This queue is private plumbing between the two surfaces of the same binary. It is not an integration point, it carries no contract you should build against, and its exchange, queue, and routing key are all operator-configured. Business events are a separate channel. Reporter publishes them to their own exchange, which the operator configures and which the deployment's reference value names `reporter.events`. Every emission happens after the database commit and never fails the work that produced it. High-value events go through a durable outbox rather than a direct publish, so a broker outage delays them instead of losing them. ## Data extraction *** The worker does not talk to your databases through hand-written queries. It runs the same extraction engine that powers [Fetcher](/en/fetcher/fetcher-core-concepts), embedded in process, with no network hop to a separate service. The engine bounds every run. The defaults allow 10 data sources per report, 50 tables per data source, 200 fields per table, four data sources extracted in parallel, and a five-minute deadline for the whole extraction. Failures are collected rather than fatal: a report whose sections partly succeed renders from what it has and settles as `Partial`, recording which sections failed. ## Stores and artifacts *** | Dependency | Role | | ---------------------------- | ------------------------------------------------------------------ | | MongoDB | Templates, reports, deadlines, and the event outbox. | | RabbitMQ | The internal report queue and the business-event exchange. | | Redis or Valkey | Idempotency locks, the schema cache, and tenant lifecycle signals. | | S3-compatible object storage | Template files and rendered artifacts, in one bucket. | Object storage is S3-compatible: Reporter speaks the S3 protocol and any service that answers it works as a target. Template sources land under the `templates/` prefix, and rendered artifacts under `reports/`, keyed by template and report identifier with the output format as the extension. Reporter applies no expiry of its own, so retention is a lifecycle policy you set on the bucket. ## Reporter and Midaz *** Midaz is a **data source** to Reporter, not a runtime dependency. Reporter carries no Midaz-specific code: a Midaz database is registered with the same `DATASOURCE_*` variables as any other PostgreSQL database, and templates address it by the `configName` the operator chose. Two consequences follow. Reporter starts and serves templates, deadlines, and metrics with no data source configured at all, so a Midaz outage never stops Reporter from running. And the same deployment can report over Midaz, over your own databases, and over both in one template. Point Reporter at a read replica where one exists. The extraction path only reads, but the query load of a heavy report is real, and a replica keeps it off the ledger's write path. See [Connecting Reporter to Midaz](/en/reporter/connecting-reporter-to-midaz) for the variables, the `configName` convention, and how to confirm the source is visible. ## Health and readiness *** Both surfaces expose `/health` and `/readyz` before authentication, so probes reach them without a token. `/health` answers 503 until the startup self-probe succeeds, which lets an orchestrator restart a pod that could not reach its dependencies at boot. `/readyz` reports the dependencies that surface actually uses, and its response states which deployment mode the process is running in. The API surface also serves `/version` with the build provenance. Read the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract shared across Lerian products. ## Next steps *** Templates, data sources, reports, deadlines, and the four report states. Every setting that shapes a Reporter deployment. # BACEN regulatory formats Source: https://docs.lerian.studio/en/reporter/reporter-bacen-templates Build BACEN-compliant regulatory reports — CADOC and CCS layouts — from Reporter templates that align with official BACEN validation rules and schedules. This section provides practical examples of Brazilian financial templates, focusing on regulatory reports required by the **Central Bank of Brazil (BACEN)**. The **Reporter** product helps you build these reports efficiently. Instead of starting from scratch, you can use the following guides to create templates that meet BACEN's official layout and validation rules. Each guide shows how to model your data, define XML structures, and use Reporter to generate compliant files. You can adapt these templates to your own business context while keeping them aligned with CADOC, CCS, and APIX standards. ## Template examples *** Here you will find step-by-step guides for some of the most common BACEN regulatory reports:
Accounting statement of financial institutions Semiannual analytical balance sheet. Daily balance sheet data. Customer registration and ownership details. Monthly Pix operational statistics.
Each page includes detailed examples, schema explanations, and sample outputs that you can generate through Reporter. These examples follow the official layouts and data models published by the Central Bank of Brazil. Use them as implementation guides to help you design compliant templates for your own reporting needs. # Reporter core concepts Source: https://docs.lerian.studio/en/reporter/reporter-core-concepts The Reporter model in one place: templates, data sources, reports, deadlines, the four report states, and the tenant boundary that separates them. Reporter has a small model. You upload a **template** that describes a document. An operator configures the **data sources** Reporter may read. You request a **report** against a template, and Reporter renders an artifact you download. A **deadline** tracks when a report is due and whether someone delivered it. This page defines each noun and the rules that connect them. ## The model at a glance *** | Concept | Definition | | ----------- | -------------------------------------------------------------------------------------- | | Template | A plain-text document definition, uploaded as a file, that declares one output format. | | Data source | A read-only database that an operator registers through configuration. | | Report | One rendering of one template, under the filters you asked for. | | Artifact | The file a finished report produces, held in object storage. | | Filter | A per-field condition that narrows the rows a report reads. | | Deadline | A due date and a recurrence rule, optionally bound to a template. | | Tenant | The isolation boundary. Every operation resolves exactly one. | ## Templates *** A template is a plain-text `.tpl` file that you upload as `multipart/form-data`, together with the output format it produces and a short description. Reporter keeps the file in object storage and its metadata in MongoDB. The template language is Pongo2, so a template mixes literal document text with variables, loops, conditionals, filters, and aggregation tags. One template declares one output format: `PDF`, `HTML`, `XML`, `TXT`, or `CSV`. PDF renders as HTML first and then converts through a headless-browser worker pool. A template addresses data by `configName`, the stable name of a data source, and by table: `{{ midaz_onboarding.accounts }}`. When more than one schema is in play, qualify it as `{{ midaz_onboarding:public.accounts }}`. At upload, Reporter reads the template text and derives the fields it touches, per data source and per table. It then checks every referenced table and field against the live schema of that data source. A data source that is unreachable produces a warning rather than a rejection, so a template still uploads while a database is down. Read [Template reference](/en/reporter/template-reference) for the tags, filters, and expressions the language offers. ## Data sources *** A data source is a database Reporter reads, registered entirely through environment configuration. Reporter connects to PostgreSQL and MongoDB. Access is read-only by construction: the extraction path issues `SELECT` statements and MongoDB finds, and no write path to a data source exists. `DATASOURCE__CONFIG_NAME` is the variable that makes a data source exist. Its value is the `configName` that templates and filters use, and it stays stable while hosts and credentials change around it. The rest of the block supplies the connection itself. The API surface over data sources is read-only too. You can list the configured sources and get one by identifier, which is how you confirm that a deployment sees the source a template expects. There is no create, update, or delete operation, because the environment owns that decision. Read [Connecting Reporter to Midaz](/en/reporter/connecting-reporter-to-midaz) for a worked configuration. ## Reports *** A report request names a template and, optionally, the filters that narrow the data behind it. Filters nest three levels — data source, then table, then field — and each field carries one condition: ```json theme={null} { "templateId": "0198c0f6-6d4d-7a34-a9ba-2c2b3ad2f1a0", "filters": { "midaz_onboarding": { "accounts": { "status": { "in": ["ACTIVE"] }, "type": { "nin": ["internal"] } } } } } ``` Eight operators exist: `eq`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, and `nin`. A report keeps its own copy of the output format and description that the template carried when the report was created. That snapshot is why an artifact stays downloadable in its original shape after the template moves on. Reports are create-and-retain. Four operations exist: create one, get it by identifier, list them, and download the artifact of a finished one. A report and its artifact remain in place once created; retention is a lifecycle policy on the object-storage bucket, set by whoever operates the deployment. ### The four report states Creation is synchronous only up to the point where the work is queued. `POST` answers `201 Created` with a report already in `Processing`, and a worker takes it from there. | State | Meaning | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `Processing` | Accepted and queued. The worker has not finished. | | `Finished` | Every data section succeeded and the artifact is stored. | | `Partial` | Some data sections failed. The report records which ones, by section, so you can fix those data sources and generate the report again. | | `Error` | Every attempted section failed, or the request never reached the worker. | `Processing` is the only entry state, and nothing returns to it once the worker settles the report. A queue can deliver the same command twice, so the worker reads the report before it starts a run: one that already settled as `Finished` or `Error` is left as it is, rather than generated again. The download operation serves the artifact once a report is `Finished`. Poll `GET /v1/reports/{id}` until the state settles, or subscribe to the terminal events Reporter emits and skip the polling. ## Deadlines *** A deadline models an obligation: a due date, a recurrence rule, and optionally the template that satisfies it. Its `type` is `regulatory` or `custom`. Its `frequency` is `once`, `daily`, `weekly`, `monthly`, `semiannual`, or `annual`, and the semiannual and annual rules also carry the months they fall in. Status is derived, never stored. A deadline is `delivered` once someone marks it so, `overdue` once its due date passes undelivered, and `pending` otherwise. A recurring deadline rolls forward on the first read after its due date passes, and only when it was marked delivered. That roll-forward clears the delivery mark, so the record returns to `pending` for the next occurrence and one record carries the whole series. Notification is pull-based. A single operation returns the deadlines inside their alert window, ranked overdue first, then warning, then informational. Reporter sends no mail and calls no endpoint of yours. Read [Managing deadlines](/en/reporter/managing-deadlines) for the full lifecycle. ## Tenants *** The tenant is the isolation boundary in Reporter. Reporter resolves it from the authenticated request itself — the bearer token that authorizes the call carries the tenant in its `tenantId` claim. No API operation takes a tenant from a request header, and no client-supplied value can widen the scope of a call. Isolation runs the whole depth of the stack. Each tenant gets its own metadata database, its own message-broker virtual host, its own connection pools, and its own event outbox. Resolution is fail-closed: a request whose tenant cannot be resolved is rejected, and nothing falls back to a shared pool. Single-tenant operation is the default and needs no tenant configuration at all. See [Multi-tenancy](/en/multi-tenancy) for the platform-wide model. ## Next steps *** One binary, two surfaces, and the queue between them. Upload a template and generate your first report. The tags, filters, and expressions available to a template. Create, track, and deliver a reporting obligation. # Reporter deployment Source: https://docs.lerian.studio/en/reporter/reporter-deployment Deploy Reporter: one binary with a run-mode selector, MongoDB, the report command queue, S3-compatible object storage, Redis or Valkey, worker sizing, and report retention through a bucket lifecycle policy. Reporter ships as a single binary with two surfaces. `RUN_MODE` selects which surfaces a process serves, so the same image runs as the API, as the report worker, or as both. Four dependencies sit behind them. Reporter reads your databases through an extraction engine that runs inside the worker process. There is no separate extraction service to deploy. ## What you deploy *** | Component | Role | Scale on | | ------------------- | ---------------------------------------------------------------------------------------- | ------------------------------ | | **API surface** | REST API for templates, reports, deadlines, and data sources. Publishes report commands. | Request rate. | | **Report worker** | Queue consumer. Extracts the data, renders the report, and writes the artifact. | Queue depth. | | **MongoDB** | Templates, reports, deadlines, and the durable event outbox. | Metadata volume. | | **RabbitMQ** | The report command queue, and the business event exchange. | Report rate. | | **Object storage** | Template sources and rendered reports. S3-compatible. | Artifact volume and retention. | | **Redis or Valkey** | Idempotency locks, schema cache, and tenant lifecycle. | Both surfaces. | ## Run modes *** `RUN_MODE=api` serves every REST operation, plus `/health`, `/readyz`, and `/version`, on the address in `SERVER_ADDRESS`. `RUN_MODE=worker` consumes the report command queue and serves `/health` and `/readyz` on `HEALTH_PORT`. `RUN_MODE=all` runs both surfaces in one process. Use `all` for local development. In production, deploy the two surfaces separately, so report generation scales on its own. ```bash theme={null} # API deployment RUN_MODE=api SERVER_ADDRESS=:4005 # Worker deployment RUN_MODE=worker HEALTH_PORT=4006 ``` ## MongoDB *** Both surfaces use the same MongoDB deployment. The API surface writes templates, reports, and deadlines; the worker updates a report as it completes. In single-tenant mode, `MONGO_HOST` and `MONGO_NAME` are required at startup. In multi-tenant mode, each tenant gets its own database, resolved from the JWT on the request. That path fails closed: a request that carries a tenant with no tenant database returns an error instead of touching a shared database. ## RabbitMQ *** Two separate concerns share one broker. ### The report command queue This queue carries work from the API surface to the worker. `RABBITMQ_EXCHANGE`, `RABBITMQ_GENERATE_REPORT_QUEUE`, and `RABBITMQ_GENERATE_REPORT_KEY` name the exchange, the queue, and the routing key. The API surface publishes, so it needs all three. The worker only consumes, so it needs `RABBITMQ_GENERATE_REPORT_QUEUE` alone. Both surfaces need the broker connection itself, and the objects must exist before the services start. The channel is internal to Reporter. To learn that a report finished, subscribe to the business events below, or poll the report. A worker that fails a message retries it up to five times with backoff, and then rejects it without requeue. Bind a dead-letter exchange to the command queue, so a rejected report lands somewhere you can inspect. ### The events exchange Business events (`template.*`, `report.*`, `deadline.*`) go to an exchange you name in `RABBITMQ_REPORT_EVENTS_EXCHANGE`. The value is operator-configured; `reporter.events` is the reference value. Set that exchange, `STREAMING_BROKERS`, and `STREAMING_CLOUDEVENTS_SOURCE` whenever `STREAMING_ENABLED=true`. With streaming off, both surfaces start normally and publish nothing. ## Object storage *** Reporter uses one S3-compatible bucket, named in `OBJECT_STORAGE_BUCKET`. It holds two kinds of object, each under its own prefix: * the template source, as `templates/.tpl` * the rendered report, as `reports//.` In multi-tenant mode, both prefixes sit under the tenant that owns the object: `/templates/...` and `/reports/...`. AWS S3, MinIO, and SeaweedFS all work. Reach SeaweedFS through its S3 gateway. `OBJECT_STORAGE_USE_PATH_STYLE=true` is what MinIO and SeaweedFS expect, and `OBJECT_STORAGE_DISABLE_SSL` stays `false` outside local development. ### Report retention Reporter keeps every report it renders, so the bucket grows with your report volume. Set expiry on the bucket, with the object store's own lifecycle policy, over the window your retention rules require. **Anchor the rule on the report prefix that your tenancy mode produces.** In single-tenant mode every report key starts at `reports/`, so one rule on that prefix covers the bucket. In multi-tenant mode the key carries the tenant ahead of it, so the rule needs the full `/reports/` prefix, one rule per tenant. Keep `templates/` out of scope either way: a rule that covers the whole bucket also removes the templates your reports are rendered from. ## Redis or Valkey *** `REDIS_HOST` is required on the API surface. On the worker it is required only when `MULTI_TENANT_ENABLED=true`, where it caches tenant discovery. Redis backs the idempotency lock on report creation, the datasource schema cache, and tenant lifecycle messages in multi-tenant mode. Idempotency state lives in Redis, not in process memory, so API replicas share it. The same report request sent twice, to two replicas, creates one report. ## Data sources *** Reporter reads reporting data from PostgreSQL and MongoDB data sources declared in the environment, one `DATASOURCE_{NAME}_*` block per source. See [Environment variables](/en/reporter/reporter-environment-variables) for the block. There is no API that registers a data source, so a new source is a configuration change and a restart. Reporter also starts with none configured, and serves templates, deadlines, and metrics. ## Sizing the worker *** `RABBITMQ_NUMBERS_OF_WORKERS` sets how many report jobs one worker process runs in parallel. For horizontal scale, add worker replicas and drive them from the depth of the command queue. PDF output renders through a headless-browser pool: `PDF_POOL_WORKERS` concurrent renders, which defaults to 2, each bounded by `PDF_TIMEOUT_SECONDS`, which defaults to 90. Size worker memory against that pool, not only against the rows a report reads. The other output formats do not use it. Every report also carries fixed extraction limits. They are 10 data sources, 50 tables per data source, and 200 fields per table, with 4 data sources read at a time, a 300-second deadline, and a 100 MiB cap on extracted data. The `ENGINE_*` variables change them. ## Deployment mode and TLS *** `DEPLOYMENT_MODE` declares the flavor of the deployment: `local`, `byoc`, or `saas`. It tags the `/readyz` response, and in `saas` it enforces TLS. **SaaS mode requires TLS on every dependency.** Set `DEPLOYMENT_MODE=saas` and a plaintext MongoDB, RabbitMQ, Redis, object storage, or Tenant Manager URL stops the process. It stops before any connection opens. Leave `ALLOW_INSECURE_TLS` unset in production. It bypasses those checks, and the local development stack is the only place for it. ## Startup checks *** Reporter validates its configuration before it serves anything. Each check below stops the process, and the error names every variable at fault. | Trigger | What the operator sees | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `SERVER_ADDRESS`, `REDIS_HOST`, or any of the three command-queue variables missing on the API surface | Config validation fails and lists each missing variable. | | `RABBITMQ_GENERATE_REPORT_QUEUE` missing on the worker surface | Config validation fails and names it. | | `MONGO_HOST` or `MONGO_NAME` missing, single-tenant mode | Config validation names them. | | `MULTI_TENANT_ENABLED=true` without `MULTI_TENANT_URL` or `MULTI_TENANT_SERVICE_API_KEY` | Config validation names the variable the tenant runtime needs. | | `STREAMING_ENABLED=true` without `RABBITMQ_REPORT_EVENTS_EXCHANGE`, `STREAMING_BROKERS`, and `STREAMING_CLOUDEVENTS_SOURCE` — all three are required for a streaming-enabled start | Startup aborts rather than run with events that go nowhere. | | `DEPLOYMENT_MODE=saas` with a plaintext dependency URL | Startup aborts before any connection opens. | **A dependency that is down at boot does not produce a silently broken pod.** `/health` answers 503 until the startup self-probe succeeds. The kubelet then restarts the pod instead of sending it traffic. ## Rolling updates *** On `SIGTERM`, both surfaces enter a drain. `/readyz` answers 503 from the moment the signal arrives, before the servers begin shutdown, and in-flight requests and in-flight messages finish. Kubernetes removes the pod from Service endpoints while it still works. Set the termination grace period above your longest report render. See the [health and readiness reference](/en/reference/health-and-readiness) for what the probes report during a drain. ## Next steps *** Every Reporter variable, by category. The configuration blocks every Lerian product shares. The probe contract and what each response means. Point Reporter at a Midaz database and render your first report. # Environment variables Source: https://docs.lerian.studio/en/reporter/reporter-environment-variables Reference the environment variables used to configure Reporter, organized by category — HTTP server, database, storage, authentication, and telemetry. This reference lists the environment variables used to configure **Reporter**, the service that generates regulatory, compliance, and accounting reports from configurable templates. Reporter ships as a single binary whose active surfaces are selected by `RUN_MODE` — the API manager, the report worker, or both. You set these at deploy time, through Helm values, Docker Compose, or your orchestrator's environment. Variables marked as required cause the server to fail on startup if not set. For the configuration blocks that every Lerian product shares — TLS posture, OpenTelemetry, Access Manager authentication, multi-tenancy, service discovery, and event streaming — see the [BYOC configuration reference](/en/reference/byoc-configuration). This page focuses on what is distinctive to Reporter. ## Run mode and ports `RUN_MODE` decides which surfaces the process serves. Run the API and worker as one process (`all`) for small deployments, or split them into separate deployables (`api` and `worker`) to scale report generation independently. See the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract. | Variable | Description | Default | Required | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------- | | `RUN_MODE` | Surfaces to run: `api`, `worker`, or `all` | `all` | No | | `SERVER_PORT` / `SERVER_ADDRESS` | API bind address (`RUN_MODE=api`/`all`), read from `SERVER_ADDRESS`; `SERVER_PORT` is the convention used to build it (`:4005`). Serves `/health`, `/readyz`, `/version`. | — | Yes (`api`/`all`) | | `HEALTH_PORT` | Worker health port (`RUN_MODE=worker`). Serves `/health`, `/readyz`. | `4006` | No | ## Deployment and TLS | Variable | Description | Default | Required | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `DEPLOYMENT_MODE` | Deployment flavor: `local`, `byoc`, or `saas`. In `saas`, TLS is mandatory for every dependency connection and the server refuses to start without it. In `byoc`, TLS is recommended and warned-on rather than enforced. Also tags the `/readyz` response. | `local` | No | | `ALLOW_INSECURE_TLS` | Bypass per-connection TLS enforcement on infrastructure DSNs. Leave unset or `false` in production. | `false` | No | ## CORS and proxies | Variable | Description | Default | Required | | ---------------------- | ------------------------------------------------------------------------------------------- | ------- | -------- | | `CORS_ALLOWED_ORIGINS` | Allowed CORS origins (CSV, or `*`). Lock to explicit origins in production. | `*` | No | | `CORS_ALLOWED_METHODS` | Allowed CORS methods | varies | No | | `CORS_ALLOWED_HEADERS` | Allowed CORS headers | varies | No | | `TRUSTED_PROXIES` | Trusted proxy CIDRs for `X-Forwarded-For` parsing. Set when running behind a load balancer. | — | No | | `SWAGGER_ENABLED` | Serve the OpenAPI/Swagger UI | `false` | No | ## API pagination | Variable | Description | Default | Required | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------- | -------- | | `MAX_PAGINATION_LIMIT` | Largest `limit` a list operation accepts. A request above the ceiling is rejected with a pagination error rather than clamped. | `100` | No | ## Database (MongoDB) Stores report metadata, templates, and run history. | Variable | Description | Default | Required | | --------------------- | ---------------------------------------------------------------------- | --------- | -------- | | `MONGO_URI` | Connection scheme: `mongodb` or `mongodb+srv` | `mongodb` | No | | `MONGO_HOST` | MongoDB host | — | Yes | | `MONGO_PORT` | MongoDB port | — | Yes | | `MONGO_USER` | Database user | — | Yes | | `MONGO_PASSWORD` | Database password. Sensitive. | — | Yes | | `MONGO_NAME` | Database name | — | Yes | | `MONGO_MAX_POOL_SIZE` | Maximum connection pool size | varies | No | | `MONGO_TLS_CA_CERT` | Base64-encoded PEM CA certificate for TLS (for example AWS DocumentDB) | — | No | ## Message broker (RabbitMQ) Carries the generate-report command queue between the API and the worker. | Variable | Description | Default | Required | | ----------------------------- | ---------------------------------------------------------------- | ------- | -------- | | `RABBITMQ_HOST` | Broker host | — | Yes | | `RABBITMQ_PORT_AMQP` | AMQP port | — | Yes | | `RABBITMQ_PORT_HOST` | Management/HTTP port | — | No | | `RABBITMQ_URI` | Connection scheme: `amqp` or `amqps`. Use `amqps` in production. | `amqp` | No | | `RABBITMQ_DEFAULT_USER` | Broker user | — | Yes | | `RABBITMQ_DEFAULT_PASS` | Broker password. Sensitive. | — | Yes | | `RABBITMQ_NUMBERS_OF_WORKERS` | Worker consumer concurrency | `5` | No | ## Object storage (S3-compatible) Where rendered reports are stored. Works with any S3-compatible endpoint. | Variable | Description | Default | Required | | ------------------------------- | ----------------------------------------------------------------- | ------------------ | -------- | | `OBJECT_STORAGE_ENDPOINT` | S3-compatible endpoint URL | — | Yes | | `OBJECT_STORAGE_REGION` | Storage region | `us-east-1` | No | | `OBJECT_STORAGE_BUCKET` | Bucket for rendered reports | `reporter-storage` | No | | `OBJECT_STORAGE_ACCESS_KEY_ID` | Access key ID. Sensitive. | — | Yes | | `OBJECT_STORAGE_SECRET_KEY` | Secret access key. Sensitive. | — | Yes | | `OBJECT_STORAGE_USE_PATH_STYLE` | Use path-style addressing (needed by some S3-compatible stores) | `false` | No | | `OBJECT_STORAGE_DISABLE_SSL` | Disable TLS to the storage endpoint. Leave `false` in production. | `false` | No | ## Cache (Redis / Valkey) | Variable | Description | Default | Required | | ------------------- | -------------------------------------------------------------- | ------- | -------- | | `REDIS_HOST` | Redis/Valkey host and port | — | Yes | | `REDIS_PASSWORD` | Authentication password. Sensitive. | — | No | | `REDIS_DB` | Logical database index | `0` | No | | `REDIS_PROTOCOL` | RESP protocol version | varies | No | | `REDIS_TLS` | Enable TLS for the connection | `false` | No | | `REDIS_CA_CERT` | Base64-encoded PEM CA certificate for TLS | — | No | | `REDIS_MASTER_NAME` | Sentinel master name (Sentinel deployments) | — | No | | `REDIS_USE_GCP_IAM` | Authenticate to GCP Memorystore with IAM instead of a password | `false` | No | ## PDF rendering (worker) | Variable | Description | Default | Required | | --------------------- | --------------------------------------- | ------- | -------- | | `PDF_POOL_WORKERS` | Concurrent PDF render workers | `2` | No | | `PDF_TIMEOUT_SECONDS` | Per-report PDF render timeout (seconds) | `90` | No | ## Report datasources Reports read from PostgreSQL and MongoDB datasources declared in the environment, one block per source. Replace `{NAME}` with your datasource identifier, and use the same identifier in every key of the block. `DATASOURCE_{NAME}_CONFIG_NAME` is the variable that makes a block exist. Reporter discovers datasources by scanning the environment for keys that match `DATASOURCE_*_CONFIG_NAME` — the prefix counts as much as the suffix, so a key that only ends in `_CONFIG_NAME` declares nothing. Reporter reads the rest of a block only for a name it found that way. The value is the name your templates use to address the source. Within a block, the variables marked required are the ones Reporter needs before it reads the block. | Variable | Description | Default | Required | | ------------------------------- | ----------------------------------------------------------------------- | -------- | -------- | | `DATASOURCE_{NAME}_CONFIG_NAME` | Name that templates use to address this datasource. Declares the block. | — | Yes | | `DATASOURCE_{NAME}_TYPE` | Datasource engine: `postgresql` or `mongodb` | — | Yes | | `DATASOURCE_{NAME}_HOST` | Datasource host | — | Yes | | `DATASOURCE_{NAME}_PORT` | Datasource port | — | Yes | | `DATASOURCE_{NAME}_DATABASE` | Database name | — | Yes | | `DATASOURCE_{NAME}_USER` | Datasource user | — | No | | `DATASOURCE_{NAME}_PASSWORD` | Datasource password. Sensitive. | — | No | | `DATASOURCE_{NAME}_SCHEMAS` | Schemas to expose from the datasource (CSV) | `public` | No | | `DATASOURCE_{NAME}_SSLMODE` | SSL mode for a PostgreSQL connection | — | No | | `DATASOURCE_{NAME}_SSLROOTCERT` | Path to the PostgreSQL SSL root certificate | — | No | | `DATASOURCE_{NAME}_SSL` | Enable TLS on a MongoDB connection | — | No | | `DATASOURCE_{NAME}_SSLCA` | Path to the MongoDB CA certificate file | — | No | | `DATASOURCE_{NAME}_OPTIONS` | Extra MongoDB URI options | — | No | | `CRYPTO_HASH_SECRET_KEY_CRM` | Hashing key to read CRM datasource PII. Sensitive. | — | No | | `CRYPTO_ENCRYPT_SECRET_KEY_CRM` | Encryption key to read CRM datasource PII. Sensitive. | — | No | A complete block. We recommend that you set the `CONFIG_NAME` value to the same string as the `{NAME}` segment, as every example on this page does — one name across the environment and your templates: ```bash theme={null} DATASOURCE_ONBOARDING_CONFIG_NAME=onboarding DATASOURCE_ONBOARDING_TYPE=postgresql DATASOURCE_ONBOARDING_HOST=midaz-postgres-replica DATASOURCE_ONBOARDING_PORT=5702 DATASOURCE_ONBOARDING_DATABASE=onboarding DATASOURCE_ONBOARDING_USER=reporter DATASOURCE_ONBOARDING_PASSWORD= DATASOURCE_ONBOARDING_SCHEMAS=public ``` A template then addresses that source by its config name, as in `{{ onboarding.accounts }}`. Datasources are declared only in the environment, so adding one is a configuration change and a restart. ## Shared configuration backbone The following blocks are identical across Lerian products and are documented in full in the [BYOC configuration reference](/en/reference/byoc-configuration). They default off. * **Access Manager authentication** — `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_ADDRESS`. Enable in production. * **Multi-tenancy** — `MULTI_TENANT_*`, plus `RABBITMQ_MULTI_TENANT_SYNC_INTERVAL` and `RABBITMQ_MULTI_TENANT_DISCOVERY_TIMEOUT`. Off by default. * **Service discovery** — `SD_*` (Consul; Reporter also accepts the legacy `SD_ADVERTISE_*` / `CONSUL_ADDR` aliases). Off by default. * **Event streaming** — `STREAMING_ENABLED`, `STREAMING_BROKERS`, `STREAMING_CLOUDEVENTS_SOURCE`, plus `RABBITMQ_REPORT_EVENTS_EXCHANGE` for the events exchange. Off by default. * **OpenTelemetry** — `ENABLE_TELEMETRY`, `OTEL_*`, `OTEL_INSECURE_EXPORTER`. Telemetry is OTLP push. # Reporter events Source: https://docs.lerian.studio/en/reporter/reporter-events Subscribe to Reporter's report and deadline events: the streaming manifest operation, the logical topic, the CloudEvents envelope, delivery policies, and what each payload carries. Reporter publishes a business event whenever a template, a report, or a deadline changes state. Subscribe to those events and you learn that a report finished without polling `GET /v1/reports/{id}` for it. `GET /v1/streaming/events` describes the contract in machine-readable form. This page covers the consumer side: what the manifest tells you, what arrives on the wire, and what each event carries. ## The manifest operation *** [Get streaming events](/en/reference/reporter/get-streaming-events) returns the static event catalog. It is authenticated like every other operation, answers `Cache-Control: no-store`, and is served whether or not this deployment publishes events. Read it at startup to check that your consumer and Reporter agree on the contract. The response has four fields: | Field | What it carries | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | Semantic version of the manifest wire format. | | `publisher` | Who publishes: `serviceName` is `reporter`, `sourceBase` is `//lerian.studio/reporter`, `routePath` is this operation, `outboxSupported` is `true`, plus the application and library versions. | | `events` | One entry per event definition, with its key, resource type, event type, schema version, description, and default delivery policy. | | `routes` | Empty. Reporter publishes no broker topology through the API. | ## The topic is a routing key *** Each event entry carries a `topic`. It is a **logical routing key**: a stable identifier for one event stream, composed from the publisher source and the event definition. With the source Reporter advertises, `report.requested` composes to: ``` lerian.studio-reporter.report.requested ``` That string is not a broker address. Do not bind a queue to it, and do not treat it as a destination. It exists so a consumer can key on one event stream across transports. What you bind to lives in deployment configuration instead. Reporter publishes to the exchange that `RABBITMQ_REPORT_EVENTS_EXCHANGE` names, and the routing key of each message is the event definition key verbatim — `report.finished`, `deadline.delivery_reverted`, underscores included. ## The event catalog *** Twelve event definitions exist across the two run modes. | Event | Emitted when | Class | | ---------------------------- | ----------------------------------------------------------------- | --------- | | `template.created` | A template is uploaded. | Important | | `template.updated` | A template's file or metadata changes. | Important | | `template.deleted` | A template is removed, with the count of deadlines that cascaded. | Important | | `report.requested` | A report request is accepted and queued. | Important | | `report.finished` | Every data section succeeded and the artifact is stored. | Critical | | `report.partial` | Some sections failed. An artifact exists. | Critical | | `report.errored` | The report ended in error. | Critical | | `deadline.created` | A deadline is created. | Important | | `deadline.updated` | A deadline changes. | Important | | `deadline.deleted` | A deadline is removed. | Important | | `deadline.delivered` | A deadline is marked delivered. | Critical | | `deadline.delivery_reverted` | A delivery mark is cleared. | Critical | This channel is publish-only. Reporter emits these events and consumes none of them. ## Delivery *** The class in the table above selects a delivery policy. | Class | Direct publish | Outbox | Dead letter | | --------- | -------------- | ----------------------------------------------- | ------------------- | | Important | Yes | Falls back to the outbox when the circuit opens | On routable failure | | Critical | No | Always | On routable failure | A critical event therefore never publishes straight to the broker. It lands in a durable outbox inside the same transaction, and a dispatcher replays it after a broker outage. Nothing is lost to a broker restart. Emission happens after the commit and never fails the work. A publication problem does not turn a stored report into an errored one. Delivery is at-least-once. Deduplicate on `ce-id`. Report events key on the report identifier and its terminal status, in the form `reporter.report..`, so every re-emission of the same fact carries the same identifier. Deadline events key on the deadline identifier, the event type, and the transition timestamp. ## The CloudEvents envelope *** Messages travel in CloudEvents binary mode, version 1.0. Context attributes ride as message headers. | Header | Value | | ------------------ | ------------------------------------------------------------------------------- | | `ce-specversion` | `1.0` | | `ce-id` | The deduplication key described above | | `ce-source` | The value of `STREAMING_CLOUDEVENTS_SOURCE` | | `ce-type` | `studio.lerian..`, for example `studio.lerian.report.finished` | | `ce-time` | RFC 3339 emission timestamp | | `ce-subject` | The identifier of the report, template, or deadline | | `ce-resourcetype` | `report`, `template`, or `deadline` | | `ce-eventtype` | `finished`, `created`, `delivery_reverted`, and so on | | `ce-schemaversion` | `1.0.0` | | `ce-tenantid` | The tenant that owns the change | Messages are marked persistent. A single-tenant deployment still stamps a tenant value, so one consumer handles both deployment shapes with the same code. ## What a payload carries *** Payload keys are `snake_case`, unlike the camelCase REST surface. A `report.finished` body: ```json theme={null} { "report_id": "019826f4-6a9c-7b31-9d40-2f1e8c5a4b77", "template_id": "8f2c1a9e-4b30-4c02-9a1b-2d5e6f7a1c33", "output_format": "pdf", "status": "Finished", "artifact_object_key": "8f2c1a9e-4b30-4c02-9a1b-2d5e6f7a1c33/019826f4-6a9c-7b31-9d40-2f1e8c5a4b77.pdf", "artifact_content_type": "application/pdf", "completed_at": "2026-07-29T14:22:08Z", "duration_ms": 8421, "section_count": 3 } ``` `artifact_object_key` is the artifact path relative to the report storage prefix, in the form `/.`. [Download a report](/en/reference/reporter/download-report) is the supported way to fetch the file, and it serves a report in the `Finished` state. [Deployment](/en/reporter/reporter-deployment) shows where that prefix sits in the bucket. `report.partial` adds `section_failures` and `failed_section_count` alongside the same artifact fields, so a consumer can route a usable-but-incomplete report differently from a clean one. `report.errored` replaces the artifact fields with `error_code` and `error_summary`. Both come from a fixed vocabulary — `report_generation_failed`, `report_generation_timeout`, or `report_generation_canceled` — each paired with a fixed summary. Raw error text never travels on the wire, so a payload cannot leak a query, a connection string, or tenant data. Branch on `error_code`. ## Enabling event publication *** Event publication is a deployment choice, set with `STREAMING_ENABLED`. Turn it on and Reporter requires three more settings at startup: | Setting | Value | | --------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `RABBITMQ_REPORT_EVENTS_EXCHANGE` | The exchange that carries the events. Operator-configured; the reference value is `reporter.events`. | | `STREAMING_BROKERS` | Must be present and non-empty. | | `STREAMING_CLOUDEVENTS_SOURCE` | Copied verbatim into `ce-source`. Give each deployment its own value when several producers share one broker. | Reporter refuses to start when publication is on and any of the three is blank, so a misconfigured deployment fails at startup instead of dropping events quietly. ## Next steps *** The 23 operations, authentication, pagination, and errors. The streaming manifest operation, with its full response shape. Every setting behind the streaming, exchange, and run-mode surfaces. Templates, reports, deadlines, and where they fit. # HTML template Source: https://docs.lerian.studio/en/reporter/reporter-html-template Build an analytical financial report in HTML with Reporter — organization details, per-account breakdowns, and detailed operations lists. This example shows how to structure an **Analytical Financial Report** focused on account-level transaction analysis, in HTML format. The report includes: * Organization and Ledger information. * Per-account breakdown. * Detailed operation list. ## Template code *** ```html HTML expandable theme={null} Analytical Financial Report

Analytical Financial Report

Detailed analysis of movements by linked accounts.

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|floatformat:2 }} {{ discount_amount|floatformat:2 }} {{ final_amount|floatformat: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 when the template is rendered, using 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_onboardTing.account` list. * This list represents all accounts linked to a customer or associated with an onboarding flow. * For each iteration, it creates a new scope where `account` refers to one specific item in that list. **`{% with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}`** Inside the loop, this line: * Uses the filter function to find a balance from the `midaz_transaction.balance` list where the `account_id` matches the current account's ID. * The `[0]` gets the first matching balance (assuming there's only one per account). * The result is stored in a temporary variable called `balance`, scoped only within this block. ### Account information ```html HTML theme={null}
Account: {{ account.alias }}

ID: {{ account.id }}

Currency: {{ balance.asset_code }}

Current Balance: {{ balance.available }}

```
**`
Account: {{ account.alias }}
`** Displays a section title with the account alias (a friendly or readable name for the account). **`

ID: {{ account.id }}

`** Shows the unique identifier of the account. **`

Currency: {{ balance.asset_code }}

`** Displays the currency code (like "BRL" or "USD") associated with the account’s balance. **`

Current Balance: {{ balance.available }}`** Shows the current available balance. ### Operations information ```html HTML theme={null} {% for operation in midaz_transaction.operation %} {% if operation.account_id == account.id %} {% set original_amount = operation.amount %} {% set discount_amount = original_amount * 0.03 %} {% set final_amount = original_amount - discount_amount %} ``` **`{% for operation in midaz_transaction.operation %}`** This loops through all operations from the `midaz_transaction.operation` list. **`{% if operation.account_id == account.id %}`** This line checks whether the current operation is related to the current account in the outer loop (from your earlier block) and only proceeds if: * The operation's `account_id` matches the `account.id` in focus. **`{% set original_amount = operation.amount %}`** This line sets a temporary variable named `original_amount` and gives it the value of the `amount` field in the operation object. **`{% set discount_amount = original_amount * 0.03 %}`** This line calculates a 3% discount on the original amount. * The result is saved as `discount_amount`. **`{% set final_amount = original_amount - discount_amount %}`** This line subtracts the discount from the original amount. * The final result is stored in `final_amount`, which can be rendered later. ### Table columns ```html HTML theme={null} {{ operation.id }} {{ operation.type }} {{ original_amount|floatformat:2 }} {{ discount_amount|floatformat:2 }} {{ final_amount|floatformat:2 }} {{ operation.description }} ``` This block outputs a clean, readable table row for each operation, including the following information: **`{{ operation.id }}`** Unique identifier for the operation. **`{{ operation.type }}`** Type of operation, such as `credit` or `debit`. **`{{ original_amount|floatformat:2 }}`** Shows the original (unadjusted) amount, formatted with 2 decimal places. **`{{ discount_amount|floatformat:2 }}`** Displays the calculated discount, also with 2 decimal places. **`{{ final_amount|floatformat:2 }}`** Shows the final amount after applying the 3% discount, nicely formatted. **`{{ operation.description }}`** Renders the description or label of the operation, such as "Transfer to savings". ### Report footer ```html HTML theme={null}

``` This line renders a footer at the bottom of the document, indicating that the content was automatically generated using Reporter by Lerian. * It also includes the legal document number of the first organization linked to the onboarding data, pulled dynamically using the `{{ midaz_onboarding.organization.0.legal_document }}` placeholder. ## Template output *** ```html HTML expandable theme={null} Analytical Financial Report

Analytical Financial Report

Detailed analysis of movements by linked accounts.

Generation Date: 23/05/2025 14:08

Organization: Mante - Pfannerstill

Ledger: Rice, Rowe and O'Hara

Account: @external/USD

ID: 0196d97e-a58d-7814-8eb3-e510b818f6a5

Currency: USD

Current Balance: 499950.00

Operation ID Type Original Amount Discount (3%) Final Amount Description
0196d982-4085-7e1d-b698-3aad306df721 CREDIT 500000.00 15000.00 485000.00 Duplicated account test
0196d983-a2c2-7dce-ad14-78eb36d9f2d1 DEBIT 20.00 0.60 19.40 Loan payment person1
0196d983-a2c2-7e0d-a246-45d5234138e3 DEBIT 30.00 0.90 29.10 Loan payment person1
```
## Rendered output *** Example financial report rendered from the HTML template code shown above # Reporter quick start Source: https://docs.lerian.studio/en/reporter/reporter-quick-start Upload a minimal Reporter template, generate your first report from live data, and verify the downloaded output — a hands-on setup in under ten minutes. Use this quick start to generate one report and confirm that your Reporter installation works end to end. For a recurring production workflow, see [Using Reporter](/en/reporter/using-reporter). For complete request and response schemas, use the [Reporter API quick start](/en/reference/reporter/reporter-developer-quick-start) and the linked API reference pages. ## Goal *** You will: 1. Create a minimal `.tpl` template. 2. Upload it as an HTML template. 3. Generate a report without data filters. 4. Track the report until processing ends. 5. Download and verify the finished file. ## Prerequisites *** You need: * A running Reporter instance * An authentication token, if Access Manager is enabled * `curl` Set the values used in the examples: ```bash theme={null} export REPORTER_URL="https://reporter.example.com" export TOKEN="your-access-token" ``` ## Create a minimal template *** Create `first-report.tpl` with static HTML content: ```html theme={null}

My first Reporter output

``` Reporter templates always use the `.tpl` extension. The template content must match the selected output format. Reporter supports HTML, CSV, XML, PDF, and TXT outputs. For PDF output, write the template in HTML and set `outputFormat=PDF`. For templates that read data, see [Template formats](/en/reporter/template-examples). ## Upload the template *** Upload the file with the three required multipart fields: `template`, `outputFormat`, and `description`. ```bash theme={null} curl --fail-with-body -X POST "$REPORTER_URL/v1/templates" \ -H "Authorization: Bearer $TOKEN" \ -F "template=@first-report.tpl" \ -F "outputFormat=HTML" \ -F "description=First Reporter output" ``` Save the returned template `id` as `TEMPLATE_ID`: ```bash theme={null} export TEMPLATE_ID="returned-template-id" ``` See [Upload template](/en/reference/reporter/upload-template) for the complete operation contract. ## Generate the report *** Create a report with the required `templateId` and `filters` fields. Use an empty object when you do not need filters. ```bash theme={null} curl --fail-with-body -X POST "$REPORTER_URL/v1/reports" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"templateId\":\"$TEMPLATE_ID\",\"filters\":{}}" ``` Save the returned report `id` as `REPORT_ID`: ```bash theme={null} export REPORT_ID="returned-report-id" ``` See [Create report](/en/reference/reporter/create-report) for filter syntax and the complete operation contract. ## Track processing *** Check the report until it leaves `Processing`: ```bash theme={null} curl --fail-with-body \ -H "Authorization: Bearer $TOKEN" \ "$REPORTER_URL/v1/reports/$REPORT_ID" ``` The active lifecycle is: ```text theme={null} Processing -> Finished | Partial | Error ``` | Status | Meaning | What to do | | ------------ | --------------------------------------------- | ------------------------------------------------------- | | `Processing` | Reporter is generating the output. | Check again later. | | `Finished` | Generation completed successfully. | Download the report. | | `Partial` | Generation completed with incomplete results. | Inspect the report details and application logs. | | `Error` | Generation failed. | Inspect the error and application logs before retrying. | See [Check report status](/en/reference/reporter/check-report-status) for the response schema. ## Download and verify the output *** Download only when the report status is `Finished`: ```bash theme={null} curl --fail-with-body \ -H "Authorization: Bearer $TOKEN" \ "$REPORTER_URL/v1/reports/$REPORT_ID/download" \ -o first-report.html ``` Confirm that the file is not empty and contains the expected heading: ```bash theme={null} test -s first-report.html grep -F "My first Reporter output" first-report.html ``` See [Download report](/en/reference/reporter/download-report) for response headers and error details. ## Troubleshooting *** | Problem | Check | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Template upload is rejected | Include the `.tpl` file, `outputFormat`, and `description`; confirm that the file content matches the format. | | Report creation is rejected | Send both `templateId` and `filters`; use `{}` when no filter applies. | | Report remains in `Processing` | Check Reporter worker and queue health, then inspect application logs. | | Report ends in `Partial` or `Error` | Inspect the report details, referenced data sources, and application logs before retrying. | | Download is rejected | Confirm that the report exists and has status `Finished`. | ## Next steps *** * Follow [Using Reporter](/en/reporter/using-reporter) to design a reusable, recurring workflow. * Learn how to reference configured data in [Template formats](/en/reporter/template-examples). * Review the specialized guidance in [BACEN templates](/en/reporter/reporter-bacen-templates) before working with regulatory report structures. * Use the [Reporter API quick start](/en/reference/reporter/reporter-developer-quick-start) for complete API examples. # Reporter REST API Source: https://docs.lerian.studio/en/reporter/reporter-rest-api Orient yourself in the Reporter API: the /v1 base path, bearer authentication, the 23 operations grouped by job, idempotency, pagination, and the RFC 9457 error shape. Reporter serves one HTTP API. Every operation sits under the base path `/v1`, with no product segment ahead of it. A template list is `GET /v1/templates`. The API carries **23 operations** across seven areas: templates, the template builder, reports, data sources, deadlines, metrics, and the streaming manifest. All 23 render under the **Reporter** anchor in the [API Reference](/en/reference/introduction). This page is the map, not the territory. It covers what the operations share and points you at the reference page for each one. The OpenAPI documents in this portal are render sources for the reference pages. They are not client contracts, and they are not a basis for SDK generation. ## Authentication *** Reporter accepts a JWT bearer token. One security scheme applies to every operation: ```http theme={null} Authorization: Bearer ``` Reporter authorizes each request against the `reporter` application, a resource, and an action. The resource follows the area — `templates`, `reports`, `deadlines`, `data-source`, `metrics`, or `streaming`. The action is the lower-cased HTTP method, so `POST /v1/reports` authorizes as the `post` action on `reports`. `PLUGIN_AUTH_ENABLED` turns the middleware on, and `PLUGIN_AUTH_ADDRESS` points it at the Access Manager. Probe routes stay outside authentication so an orchestrator reaches them without a token: `/health`, `/readyz`, and `/version`. Tenant identity travels inside the token. Reporter resolves the tenant from the JWT `tenantId` claim. None of the operations above take a tenant from a request header, and no client-supplied value overrides it. See [Multi-tenancy](/en/multi-tenancy). ## The operations by job *** ### Templates Five operations own the template lifecycle: [list](/en/reference/reporter/list-templates), [upload](/en/reference/reporter/upload-template), [get](/en/reference/reporter/retrieve-template-details), [update](/en/reference/reporter/update-templates), and [delete](/en/reference/reporter/delete-template). A template is a plain-text `.tpl` file plus its output format and description. Deleting one also removes the deadlines that point at it. ### Template builder Four operations back a visual editor without persisting anything. [List block definitions](/en/reference/reporter/list-block-definitions) and [list filter definitions](/en/reference/reporter/list-filter-definitions) return the catalog an editor draws from. [Validate blocks](/en/reference/reporter/validate-template-blocks) checks a block tree and reports per-block errors. [Generate code](/en/reference/reporter/generate-template-code) turns that tree into template source you can then upload. ### Reports Four operations, and only four: [create](/en/reference/reporter/create-report), [get](/en/reference/reporter/check-report-status), [list](/en/reference/reporter/retrieve-reports), and [download](/en/reference/reporter/download-report). The lifecycle is **create-and-retain**. `POST /v1/reports` answers `201` with a report in `Processing` and hands the work to a background worker. The report then reaches `Finished`, `Partial`, or `Error`. Poll the get operation, or subscribe to the events described in [Events](/en/reporter/reporter-events). Download serves a report in `Finished`. Every report you create stays addressable by its identifier for as long as your object-storage retention policy keeps the artifact. ### Data sources Two read operations: [list](/en/reference/reporter/list-data-sources) and [get one](/en/reference/reporter/retrieve-data-source), both under `/v1/data-sources` — note the hyphen. An operator registers data sources through environment variables, so these operations report what the deployment already has and expose no write path. ### Deadlines Six operations model a filing obligation with a due date and a recurrence rule: [list](/en/reference/reporter/retrieve-deadlines), [create](/en/reference/reporter/create-deadline), [update](/en/reference/reporter/update-deadline), [delete](/en/reference/reporter/delete-deadline), [deliver](/en/reference/reporter/deliver-deadline), and [notifications](/en/reference/reporter/retrieve-deadline-notifications). Status is derived from the due date and the delivery mark, never sent by a client. Notifications are pull-only: the operation returns the deadlines inside their alert window, sorted overdue first. ### Metrics and streaming [Metrics](/en/reference/reporter/get-metrics) returns deployment counters — templates, reports, data sources, and report errors for the current window against the one before it. `errorPeriodDays` sets that window and defaults to 7. [Streaming events](/en/reference/reporter/get-streaming-events) returns the event manifest covered in [Events](/en/reporter/reporter-events). ## Content types *** Template create and update are `multipart/form-data`: a `template` file part, plus `outputFormat` and `description`. Everything else that carries a body is `application/json`. Download answers with the bytes and a `Content-Disposition` filename. The media type follows the report's output format — `application/pdf`, `application/xml`, `text/csv`, `text/html`, or `text/plain`. ## Idempotency *** Template create and report create accept an `X-Idempotency` request header. Send your own key, or omit the header and Reporter derives one from a hash of the request body. * A request that repeats one still in flight is rejected rather than duplicated. * A request that repeats one already completed replays the original response and stamps `X-Idempotency-Replayed: true` on it. ## Pagination *** List operations use offset pagination with the same two parameters. | Parameter | Default | Rules | | --------- | ------- | ----------------------------------------------------------------------------- | | `page` | `1` | Page number. | | `limit` | `10` | Items per page. The ceiling is `MAX_PAGINATION_LIMIT`, which defaults to 100. | A `limit` above the ceiling is rejected with a pagination error rather than clamped. The report list also accepts a `cursor` parameter, which carries the position of the previous page in place of a page number. The report list adds filters on top: `status`, `template_id`, `output_format`, `description`, `type`, `active`, a `start_date`/`end_date` window, and `sort_order`, which defaults to `desc`. The template list filters on `outputFormat`, and the deadline list on `status`. ## Errors *** Every error answers `application/problem+json` and follows [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457), carrying a `title`, a `status`, a `detail`, and a stable domain `code`. Match on the code, never on the prose. The [Reporter error list](/en/reference/reporter/reporter-error-list) maps each code to its HTTP status and its fix. ## Reading the spec from a running service *** `SWAGGER_ENABLED=true` mounts a browsable reference at `/swagger/docs`, with the OpenAPI 3.1 document at `/swagger/openapi.json` and `/swagger/openapi.yaml`. It is off unless you turn it on. Keep it off in production. ## Next steps *** The 23 operations, with full request and response shapes. Upload a template, generate a report, and download it with cURL. Subscribe to report and deadline events instead of polling. Templates, reports, data sources, and where they fit. # Reporter SDK and embedding Source: https://docs.lerian.studio/en/reporter/reporter-sdk-and-embedding Integrate Reporter from your own software: the REST surface that carries every operation, the base URL and auth model, a first call, the Go SDK, and the pattern for running Reporter behind a service you already operate. Reporter's **REST API is the complete surface**. Templates, reports, data sources, deadlines, the template builder, and metrics are all HTTP calls. Nothing exists only inside a client library, and nothing exists only inside the Console. Start there. This page covers what you authenticate with, what your base URL looks like, what a first call returns, where the Go SDK saves you work, and how to run Reporter behind a service your own users already talk to. ## Authenticating *** One scheme protects every operation: a bearer token on the `Authorization` header. ``` Authorization: Bearer ``` Access Manager authorises each call against the resource behind the path — templates, reports, data sources, deadlines, metrics, or streaming — and the action, which is the HTTP verb. A token granted read access to reports can list and download them, and gets a `403` on create. Failures answer `application/problem+json`, so parse the problem document rather than the status line alone. The token also carries the isolation scope of the call. Reporter resolves it from the credential itself, so no operation takes a scoping header from your client and no header widens what a token already permits. ## Base URL *** The base path is `/v1`, with no product segment in front of it. Every path on this page appends to that: ``` https://reporter.example.com/v1 ``` Three content-type rules follow from the surface itself. Template upload and update are `multipart/form-data`, because a template is a `.tpl` file. Report creation is `application/json`. A download streams the rendered bytes with the `Content-Type` of the output format and a `Content-Disposition` filename. List operations take `limit` and `page`, 10 and 1 by default. The largest page is `MAX_PAGINATION_LIMIT`, which each deployment sets and which defaults to 100. A `limit` above that ceiling is rejected, not reduced. ## Your first call *** ```bash theme={null} curl -s "https://reporter.example.com/v1/templates" \ -H "Authorization: Bearer $TOKEN" ``` A `200` with a template list proves both. A `401` means the token; a `404` means the base URL. ```bash theme={null} curl -s -X POST "https://reporter.example.com/v1/reports" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "X-Idempotency: daily-balance-2026-07-28" \ -d '{ "templateId": "0196b270-a315-7137-9408-3f16af2685e1", "filters": { "midaz_onboarding": { "account": { "status": { "eq": ["ACTIVE"] } } } } }' ``` The response is `201` with the report in `Processing`. Generation runs asynchronously. ```bash theme={null} curl -s "https://reporter.example.com/v1/reports/$REPORT_ID" \ -H "Authorization: Bearer $TOKEN" curl -s "https://reporter.example.com/v1/reports/$REPORT_ID/download" \ -H "Authorization: Bearer $TOKEN" -o report.pdf ``` Download requires `Finished`. Send `X-Idempotency` on every report request, and on template upload for the same reason. Repeat a request that is still running and you get an error instead of a second report. Repeat one that already finished and Reporter replays the original report, marking the response `X-Idempotency-Replayed: true`. Derive the key from your own request identifier and a retry costs nothing. A report is created once and kept: create, get, list, download are its four operations. How long a rendered file lives is a lifecycle policy on the storage bucket, not an API call. ## Reading data sources *** An operator configures data sources through environment variables, so the API over them is read-only. Use it to discover what your templates can reference: ```bash theme={null} curl -s "https://reporter.example.com/v1/data-sources" \ -H "Authorization: Bearer $TOKEN" ``` Each entry carries the config name that templates address, plus its tables and fields. Fetch one by identifier with `/v1/data-sources/{dataSourceId}`. ## The Go SDK *** [`lerian-sdk-golang`](https://github.com/LerianStudio/lerian-sdk-golang) ships a `reporter` package alongside the other Lerian products. It is a convenience over the calls above for template and report work: it acquires an OAuth2 client-credentials token, refreshes it, and hands back typed results and paginated iterators. Configure it with the same `https:///v1` base URL you use with cURL, plus the client ID, the client secret, and the token URL of your authorization server, and a request timeout. ```go theme={null} report, err := client.Reporter.Reports.Get(ctx, reportID) if err != nil { return err } if report.Status == "Finished" { data, err := client.Reporter.Reports.Download(ctx, reportID) if err != nil { return err } if err := os.WriteFile(reportID+"."+report.Format, data, 0o600); err != nil { return err } } ``` `Download` returns the rendered bytes in the report's own format. Write them to disk, as above, or stream them to your caller. The SDK covers a slice of the product, not all of it. It carries template create, get, list, and delete, and report create, get, list, and download. Data sources, template update, deadlines, the template builder, metrics, and the streaming manifest are REST calls. Mixing both in one integration is normal and expected: the package where it fits, plain HTTP everywhere else. ## Running Reporter behind your own service *** Reporter is a service you deploy, not a library you link. To put it behind an application your customers already use, keep the credentials on your side and call Reporter server to server. Four rules keep that boundary clean. **Never hand a Reporter token to a browser.** Your service authenticates your user, decides whether that user may run this report, and then makes the call with its own token. **Answer immediately with an identifier.** Report creation returns in `Processing`. Return that identifier to your caller and let your own status endpoint expose progress. **Learn about completion once.** Poll `GET /v1/reports/{id}` on a modest interval, or subscribe to Reporter's report events and stop polling. See [Reporter events](/en/reporter/reporter-events). **Proxy the download.** The download endpoint streams bytes to an authenticated caller, so your service fetches them and re-serves them under its own auth. A `Partial` report means some data sections failed while others succeeded, and its metadata names the failing sections. Treat it as a signal about a data source or a filter, and decide in your own service what your users should see. ## Next steps *** Every operation, grouped by the job it does. The event contract, and how to subscribe instead of polling. Upload a template and generate a report with cURL. Reporter's error codes and what resolves them. # Reporter template engine Source: https://docs.lerian.studio/en/reporter/reporter-template-engine How Reporter evaluates a template: the field map it derives, the data context it builds, blocks, template filters, row filters, and the multi-schema data source model. A Reporter template is a plain-text `.tpl` file. Reporter renders it with a Pongo2 engine, which reads Django-style tags and pipe filters. [Template reference](/en/reporter/template-reference) lists every tag and filter. [Template formats](/en/reporter/template-examples) shows a worked file per output format. This page covers what the engine does around that syntax: what it reads out of your file, what data it puts in front of it, and which limits it applies. ## The field map *** When you upload a template, Reporter analyses the text and derives a **field map**: every data source, every table, and every field the template names. ```json theme={null} { "external_db": { "sales__orders": ["id", "total", "created_at"] } } ``` The map is stored with the template metadata and travels with each report request. Extraction reads only what the map lists. A field your template never names is never queried, so a template stays cheap as the underlying tables grow. Two consequences follow. First, Reporter derives the map when you upload or replace the file, never at render time. A new field reference reaches a report only after you upload the changed template. Second, Reporter checks the map against the live schema of each data source at upload time. A wrong table name or a wrong field name surfaces there, before any report runs. When a data source cannot answer, Reporter returns warnings and still accepts the template. One unreachable database does not block your work. ## The data context *** Extraction builds one context for the render. The first level is the data source config name. The second level is the table. Each value is a list of rows. ```json theme={null} { "external_db": { "orders": [ { "id": "018f...", "total": "1200.00", "created_at": "2026-07-01" } ] } } ``` Your template addresses that context by the same names it used to declare them: ```django theme={null} {% for order in external_db.orders %} {{ order.id }} — {{ order.total|floatformat:2 }} {% endfor %} ``` Rows come back under the key your template wrote. A bare table name stays bare. A schema-qualified reference becomes `schema__table`, with a double underscore, in both the field map and the render context. Row filters are looser and accept either `schema.table` or `schema__table`. ### Variables A template reads the context through variables. A loop alias binds one row at a time, and `{% with %}` names part of the context for the block below it. Both aliases are local to the block that declares them. The analyser follows an alias back to the table behind it. A field you read as `order.total` inside a loop over `external_db.orders` lands in the field map as `total` under that table, so extraction returns it. ## Multi-schema data sources *** An operator declares each data source through environment variables. `CONFIG_NAME` sets the name your templates use, and `SCHEMAS` lists the schemas Reporter discovers on it. Keep the environment-name segment and the `CONFIG_NAME` value identical, as in the block below: ```bash theme={null} DATASOURCE_EXTERNAL_DB_CONFIG_NAME=external_db DATASOURCE_EXTERNAL_DB_HOST=external-postgres DATASOURCE_EXTERNAL_DB_PORT=5432 DATASOURCE_EXTERNAL_DB_USER=db_user DATASOURCE_EXTERNAL_DB_DATABASE=external_database DATASOURCE_EXTERNAL_DB_TYPE=postgresql DATASOURCE_EXTERNAL_DB_SCHEMAS=sales,inventory,reporting ``` Without `SCHEMAS`, Reporter discovers the `public` schema alone. See [Environment variables](/en/reporter/reporter-environment-variables) for the full block. The engine resolves a bare table name against every discovered schema: | Owners of the table name | Result | | -------------------------------------- | ------------------------------------------------------------------- | | Exactly one schema | The engine reads that table. | | Several schemas, one of them `public` | The engine reads the `public` table. | | Several schemas, none of them `public` | The engine reports the table as ambiguous and names the candidates. | | No schema | The engine reports the table as not found. | Qualify the reference to remove the ambiguity. The qualified form names the source, the schema, and the table: ```django theme={null} {% for order in external_db:sales.orders %} {{ order.id }} {% endfor %} ``` A qualified reference must match the discovered schema exactly. The engine does not search other schemas for it. ## Blocks *** A block is the unit the visual template builder works in. Thirteen types exist, in six categories: | Category | Block types | | --------------------- | ------------------------------------------------------- | | Basic (`basic`) | `text`, `variable`, `comment` | | Control (`control`) | `loop`, `conditional`, `with` | | Data (`data`) | `aggregation`, `calculation`, `date_time`, `expression` | | Layout (`layout`) | `section` | | Numbering (`dimp`) | `counter` | | Advanced (`advanced`) | `custom_tag` | The value in brackets is the category string the block catalogue returns, so a client matching on the API response keys on it rather than on the label. Four of them hold children: `loop`, `conditional`, `section`, and `with`. A conditional also carries alternative branches. Blocks nest up to fifty levels deep. Blocks are never stored. The builder sends them to Reporter, and Reporter returns Pongo2 source plus the field map that source implies. The template you upload is always the text. Three operations support that flow: * [Validate template blocks](/en/reference/reporter/validate-template-blocks) checks the structure, parses the generated source, and reports each problem against the block that caused it. * [Generate template code](/en/reference/reporter/generate-template-code) returns the finished source for an output format. * [List block definitions](/en/reference/reporter/list-block-definitions) returns the catalogue, so a client stays in step with the engine. ## Two kinds of filter *** Reporter uses the word filter for two different things, and they run at different times. **Row filters** run during extraction. They live in the report request, not in the template, and they narrow the rows the database returns. The payload nests them three levels deep: data source, then table, then field. ```json theme={null} { "templateId": "018f2a6c-1c2f-7a10-9f1e-2b8c4d5e6f70", "filters": { "external_db": { "orders": { "status": { "nin": ["cancelled", "draft"] }, "created_at": { "gte": ["2026-07-01"], "lte": ["2026-07-31"] } } } } } ``` Eight operators exist: | Operator | Meaning | Values | | --------- | --------------------------- | ----------- | | `eq` | Matches any listed value | One or more | | `gt` | Above the value | One | | `gte` | At or above the value | One | | `lt` | Below the value | One | | `lte` | At or below the value | One | | `between` | Inside an inclusive range | Exactly two | | `in` | Matches any listed value | One or more | | `nin` | Excludes every listed value | One or more | Every operator takes an array. Several operators on one field combine, as the date range above shows. **Template filters** run during the render, after the rows arrive. They shape a value inside the document, and they use pipe syntax: `{{ value|percent_of:total }}`. [List filter definitions](/en/reference/reporter/list-filter-definitions) returns the catalogue with an example per filter. ## The render step *** The engine parses the prepared template once per report and executes it against the context. Two behaviours of that step change how you design a document. Numeric output loses its trailing zeros, so `1200.00` renders as `1200`. Use `floatformat` when a column needs fixed decimals. Values that carry several dots stay intact, which keeps an accounting code such as `1.1.2.00.000` unchanged. An XML template can declare the encoding of the stored file. Put the marker on the same line as the XML declaration: ```django theme={null} {# reporter:output-encoding=utf-16be #} ``` Reporter then writes the file in UTF-16BE with no byte order mark. PDF output ignores the marker. ## Limits the engine applies *** Reporter blocks the Pongo2 tags that load or extend another file: `include`, `extends`, `import`, `block`, and `ssi`. A template is a self-contained document. Block nesting stops at fifty levels, on validation and on code generation alike. Free-form block fields reject template delimiters, so builder input cannot inject a tag. Uploaded templates reject script tags. ## Next steps *** The path from a report request to a file you can download. Every tag, filter, and operator the syntax accepts. A worked template per output format. Templates, data sources, reports, and deadlines in one place. # TXT template Source: https://docs.lerian.studio/en/reporter/reporter-txt-template Build a transaction receipt template in plain text with Reporter — a simple .tpl file that renders into a printable TXT proof of payment. This page guides you through the creation of a transaction receipt template using **Reporter** in `.txt` format. ## Template code *** ```text Text expandable theme={null} ########################################## # PROOF OF PAYMENT # ########################################## Generation Date: {% date_time "dd/MM/YYYY HH:mm" %} Ledger Name: {{ midaz_onboarding.ledger.0.name }} {%- for transaction in midaz_transaction.transaction %} ----------------------------------------- Transaction ID: {{ transaction.id }} Transaction Date: {{ transaction.created_at }} Total Amount: {{ transaction.amount }} Transaction Status: {{transaction.status}} ------------------------------------------ Source Accounts: {%- for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %} {%- if operation.type == "DEBIT" %} - Alias: {{operation.account_alias }} - Debit Amount: {{ operation.amount }} {%- endif %} {%- endfor %} Target Accounts: {%- for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %} {%- if operation.type == "CREDIT" %} - Alias: {{operation.account_alias }} - Credit Value: {{operation.amount }} {%- endif %} {%- endfor %} {%- endfor %} ------------------------------------------ Automatically generated document. ``` ## Code breakdown *** \*\* `Generation Date: {% date_time "dd/MM/YYYY HH:mm" %}`\*\* Renders the **current date and time** when the template is processed. * Uses the format `dd/MM/YYYY HH:mm` (e.g., `26/05/2025 11:45`). \*\* `Ledger Name: {{ midaz_onboarding.ledger.0.name }}`\*\* Displays the **name of the ledger** associated with this report. * `midaz_onboarding.ledger` is a list, and `[0]` selects the **first ledger**. * `.name` fetches the name property, like `"Corporate Ledger"`. \*\* `{% for transaction in midaz_transaction.transaction %}`\*\* This loop iterates over **each transaction** in the `midaz_transaction.transaction` list. For every transaction, the code below will run once to display its information. ### Transaction header block Each transaction prints the following details: ```txt Text theme={null} ----------------------------------------- Transaction ID: {{ transaction.id }} Transaction Date: {{ transaction.created_at }} Total Amount: {{ transaction.amount }} Transaction Status: {{ transaction.status }} ------------------------------------------ ``` * `transaction.id`: Unique ID of the transaction. * `transaction.created_at`: Timestamp when the transaction was created. * `transaction.amount`: The raw amount of the transaction. * `transaction.status`: Current status such as `COMPLETED`, `PENDING`, or `FAILED`. ### Source accounts section ```txt Text theme={null} Source Accounts: {% for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %} {% if operation.type == "DEBIT" %} - Alias: {{ operation.account_alias }} - Debit Amount: {{ operation.amount }} {% endif %} {% endfor %} ``` * Filters the operations list to include only operations linked to the **current transaction**. * Further filters it to only show operations with `type == "DEBIT"` (funds leaving an account). * For each matching debit: * Displays the **account alias**. * Displays the **debit amount**. ### Target accounts section ```txt Text theme={null} Target Accounts: {% for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %} {% if operation.type == "CREDIT" %} - Alias: {{ operation.account_alias }} - Credit Value: {{ operation.amount }} {% endif %} {% endfor %} ``` * Same logic as the debit block, but filters for `type == "CREDIT"` (funds received). * For each matching credit: * Displays the **account alias**. * Displays the **credit amount**. ### Footer text ```text Text theme={null} ------------------------------------------ Automatically generated document. ``` * Simple text footer that marks the end of the report. * Indicates that the file was generated automatically, typically for traceability. ## Template output *** ```text Text expandable theme={null} ########################################## # PROOF OF PAYMENT # ########################################## Generation Date: 23/05/2025 14:08 Ledger Name: Rice, Rowe and O'Hara ----------------------------------------- Transaction ID: 75906707-8c31-479c-b354-aa805c4cefbc Transaction Date: 2025-05-22T14:15:22.123Z Total Amount: 5000.00 Transaction Status: ACTIVE ------------------------------------------ Source Accounts: - Alias: @external|USD - Debit Amount: 1500.00 Target Accounts: - Alias: @wallet_12345 - Credit Value: 1500.00 ------------------------------------------ Automatically generated document. ``` # XML template Source: https://docs.lerian.studio/en/reporter/reporter-xml-template Build an analytical financial report in XML with Reporter — account-level activity, custom logic like discounts, and per-account summaries. This page guides you through a full example of an **Analytical Financial** report in **XML**. The template breaks down account-level activity, applies custom logic (like discounts), and includes clear summaries to support analysis and auditing. **What this report shows:** * **Organization and Ledger details**. * **Account-level overview**: balance, currency, and alias. * **Operation breakdowns**: original amount, discount, final amount, status, description, and type. * **Summaries per account**: total operations, total value, and average per operation. ## Template code *** ```xml XML expandable theme={null} {{ midaz_onboarding.legal_name }} - Tax ID: {{ midaz_onboarding.legal_document }} {% date_time "dd/MM/YYYY HH:mm" %} {%- with ledger = midaz_onboarding.ledger[0] %} {{ ledger.name }} {%- for account in midaz_onboarding.account %} {{ account.id }} {{ account.alias }} {%- with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %} {{ balance.available }} {%- endwith %} {%- for balance in midaz_transaction.balance %} {%- if balance.account_id == account.id %} {{ balance.available }} {%- endif %} {%- endfor %} {{ account.asset_code }} {%- for operation in midaz_transaction.operation %} {%- if operation.account_id == account.id %} {%- set original_amount = operation.amount %} {%- set discount_amount = original_amount * 0.03 %} {%- set final_amount = original_amount - discount_amount %} {{ operation.id }} {{ operation.description }} {{ operation.type }} {{ operation.route }} {{ original_amount }} {{ discount_amount }} {{ final_amount }} {{ operation.asset_code }} {{ operation.status }} {%- endif %} {%- endfor %} {% count_by midaz_transaction.operation if account_id == account.id %} {% sum_by midaz_transaction.operation by "amount" if account_id == account.id %} {% avg_by midaz_transaction.operation by "amount" if account_id == account.id %} {%- endfor %} ``` ## Code breakdown *** ### Organization and Ledger information ```xml XML theme={null} {{ midaz_onboarding.legal_name }} - Tax ID: {{ midaz_onboarding.legal_document }} 28.04.2025 {%- with ledger = midaz_onboarding.ledger[0] %} {{ ledger.name }} ``` **`{{ midaz_onboarding.organization.legal_name }} - CNPJ: {{ midaz_onboarding.organization.legal_document }}`** Displays the organization’s legal name and corresponding *CNPJ* (Brazilian Tax ID). * `{{ midaz_onboarding.organization.legal_name }}`: retrieves the organization's name. * `{{ midaz_onboarding.legal_document }}`: retrieves the *CNPJ*. **`{% date_time "dd/MM/YYYY HH:mm" %}`** This function applies the date and time when the template is rendered, using the `dd/MM/YYYY HH:mm` format. **`{%- with ledger = midaz_onboarding.ledger[0] %}`** Creates a temporary variable pointing to the first available ledger. This keeps the code cleaner and avoids repetitive references. * `-`: removes extra spaces in the rendered file. * `ledger`: the name of the temporary variable. * `midaz_onboarding.ledger[0].name`: points to the first available ledger. ### Looping through accounts ```xml XML theme={null} {%- for account in midaz_onboarding.account %} {{ account.id }} {{ account.alias }} {%- with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %} {{ balance.available }} {%- endwith %} ``` **`{%- for account in midaz_onboarding.account %}` ... `{% endfor %}`** Loops through all accounts linked to the user. Since most users have more than one account, this allows you to display individual data for each one. * `midaz_onboarding.account`: the full list of user accounts. * `{%- for account in midaz_onboarding.account %}`: the loop runs once per account. **`{%- with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}`** Defines a variable named `balance` that holds the current account’s balance. * `filter()`: scans through `midaz_transaction.balance` to find the one matching the `account.id`. * `[0]`: grabs the first match. * The result is stored in `balance`. **` {{ balance.available }}`** Shows the account’s available balance (`balance.available`). ### Looping without `{%with...%}` ```xml XML theme={null} {%- for balance in midaz_transaction.balance %} {%- if balance.account_id == account.id %} {{ balance.available }} {%- endif %} {%- endfor %} ``` **`{%- for balance in midaz_transaction.balance %}`** Loops through all balances to find the one that matches the current account. This is a more explicit alternative to `filter()`. * `midaz_transaction.balance`: The list of all balances. * `{%- balance in midaz_transaction.balance %}`: the loop runs once per balance. **`{% if balance.account_id == account.id %}`** Filters balances to show only those that match the current account being processed. * If they match, the balance is displayed. ### Currency information ```xml XML theme={null} {{ account.asset_code }} ``` Shows the account’s currency (e.g., BRL, USD) to help you understand the value context. * `account.asset_code`: dynamically injects the currency code. ### Grouping operations ```xml XML theme={null} {%- for operation in midaz_transaction.operation %} {%- if operation.account_id == account.id %} {%- set original_amount = operation.amount %} {%- set discount_amount = original_amount * 0.03 %} {%- set final_amount = original_amount - discount_amount %} ``` ### `...` Defines the section where all operations for an account are listed. * Keeps the report structured and easy to scan. **`{%- for operation in midaz_transaction.operation %}`** Loops through all operations in the transaction, and displays operations that match the current account. * `operation`: represents the current operation that is part of the current transaction. * `midaz_transaction.operation`: the list of all operations in the transaction. **`{%- if operation.account_id == account.id %}`** Ensures that only the relevant operations for the current account are included. * If the `account_id` of the operation is equal to the current `account.id`, the rest of the block will be executed. **`{%- set original_amount = operation.amount %}`** Defines the original amount and sets it as a variable. * `original_amount`: the name of the variable. **`{%- set discount_amount = original_amount * 0.03 %}`** Defines a variable and calculates a 3% discount on the original amount. * `discount_amount`: the name of the variable. * `original_amount * 0.03`: applies a 3% discount on the original amount. **`{%- set final_amount = original_amount - discount_amount %}`** Subtracts the discount from the original amount to get the final value. * `final_amount`: variable that represents the final value. * `original_amount - discount_amount`: the subtraction operation. ### Operation block ```xml XML theme={null} {{ operation.id }} {{ operation.description }} {{ operation.type }} {{ operation.route }} {{ original_amount }} {{ discount_amount }} {{ final_amount }} {{ operation.asset_code }} {{ operation.status }} ``` Each `` contains detailed information about the transaction. These fields help users audit and understand what happened. **`{{ operation.id }}`** Unique ID of the operation. **`{{ operation.description }}`** Short description of the operation. **`{{ operation.type }}`** Operation type (e.g., credit, debit, adjustment). **`{{ operation.route }}`** Associated operation route entry. **`{{ original_amount }}`** Original amount before any adjustments. **`{{ discount_amount }}`** Value of the discount applied. **`{{ final_amount }}`** Final amount after discount. **`{{ operation.asset_code }}`** The currency used for the operation. **`{{ operation.status }}`** Operation status (e.g., Approved, pending). ### Account summary block ```xml XML theme={null} {% count_by midaz_transaction.operation if account_id == account.id %} {% sum_by midaz_transaction.operation by "amount" if account_id == account.id %} {% avg_by midaz_transaction.operation by "amount" if account_id == account.id %} ``` **`{% count_by midaz_transaction.operation if account_id == account.id %}`** Counts the number of operations associated with your account and assists in understanding the transaction volume for the reporting period. * The `count_by` function goes through `midaz_transaction.operation` and adds up how many operations have the same `account_id` as the current account. **`{% sum_by midaz_transaction.operation by "amount" if account_id == account.id %}`** Sums up the total value of all operations for this account, and outputs the result using two decimal places. * `sum_by` loops through transactions filtered by `account_id`. * Sums the `amount` fields. **`{% avg_by midaz_transaction.operation by "amount" if account_id == account.id %}`** Calculates the average operation value for this account. Useful for spotting spending patterns or outliers. * `avg_by` applies the average calculation to the amount values. * Only considers operations for the current account. ## Template output *** ```xml XML expandable theme={null} Ferry, Stiedemann and Jast - CNPJ: 78425230000190 28.04.2025 O'Connell, Dietrich and Bernhard 01965efe-5afd-733d-9a64-3d53f73b13c8 @external/BRL 1000.00 1000.00 BRL 01965f04-7087-73f1-a802-f8fceedb34ec Initial transfer DEBIT 1000.00 30.00 970.00 BRL 1 1000.00 1000.00 01965eff-0b2d-73cf-bbfe-0dc1ac9032d3 @wallet_46040127 0 0 BRL 0 0.00 0.00 01965f01-aa72-7254-bb4a-fa9c8c9d4009 @account1 300.00 300.00 BRL 01965f04-7087-73d2-a167-cb5146bee801 External to accounts 1 and 2 CREDIT 300.00 9.00 291.00 BRL 1 300.00 300.00 01965f01-ff74-712a-9e2c-d987f15a1d14 @account2 700.00 700.00 BRL 01965f04-7087-73b1-aff9-69f59d623d31 External to accounts 1 and 2 CREDIT 700.00 21.00 679.00 BRL 1 700.00 700.00 01965f02-55fc-7a06-9f95-07e9a9ff4941 @account3 0 0 BRL 0 0.00 0.00 ``` # Template formats Source: https://docs.lerian.studio/en/reporter/template-examples Design Reporter templates for HTML, XML, CSV, PDF, or TXT outputs — practical examples to start building reports that fit your use case. The **Reporter** product supports multiple output formats, so you can generate exactly the file type you need, whether it's HTML, XML, CSV, PDF or plain text. In this section, you'll find practical examples of how to structure your `.tpl` files for each format. These examples are a great starting point to help you design templates that fit your use case, follow formatting rules, and get parsed correctly during generation. The file content **must** follow the output format you need (HTML, XML, CSV, PDF or TXT), but make sure to save it with a `.tpl` extension. **This is required for the template to work properly.** ## Available examples ***
Analytical financial report with account-level transaction analysis. Analytical financial report in XML with custom logic and summaries. Transaction receipt template in plain text format.
Each page includes the full template code, field-by-field explanations, and sample outputs that you can generate through Reporter. ## Tip for rendering a transaction receipt *** To generate a transaction receipt using Reporter, you can send a request like this to the [Create a Report](/en/reference/reporter/create-report) endpoint: ```json JSON theme={null} { "templateId": "0196f99c-4d3e-73bc-b5d3-2a5bb1efbee4", "ledgerId": [ "0196d97e-587e-76d8-a458-9d2517ed9aba" ], "filters": { "midaz_transaction": { "transaction": { "id": ["0196d983-a2c2-7d5a-a5b7-029fe0dcb710"] } } } } ``` ### What does this filter do? The `filters` field lets you limit the data used when rendering the template. In this case, you're telling the system to use **only** the transaction with the ID `0196d983-a2c2-7d5a-a5b7-029fe0dcb710`. That means your receipt will include just that one transaction — and the operations linked to it — keeping the output clean and focused. ### What happens next? Once the request is sent: * The API returns a `reportId`. * You can use this ID to check the status of the report. * When it's ready, the report will be available for download. ### Why it matters This approach gives you clarity and control. Each receipt is generated in isolation, using the filters and templates you define. # Template reference Source: https://docs.lerian.studio/en/reporter/template-reference Reference every template tag, filter, and operator available in Reporter — loops, conditionals, placeholders, and built-in formatting helpers. This page is a complete reference for all template tags, filters, and operators available in Reporter. For an introduction to templates and placeholders, see [What is Reporter](/en/reporter/what-is-reporter). ## Building templates *** ### Common blocks * **Loop** ``` {% for in %} ... {% endfor %} ``` * **Loop with explicit schema** ``` {% for order in external_db:sales.orders %} {{ order.id }} - {{ order.total }} {% endfor %} ``` * **Simple condition** ``` {% if value_a == value_b %} ... {% endif %} ``` * **Temporary scope** ``` {% with as %} ... {% endwith %} ``` * **Value formatting** ``` {{ field_name | floatformat:2 }} --> renders 123.45 ``` ### 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 %} ``` 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 formatted according to the provided format string. Time is generated in UTC. ``` {% 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 %} ``` 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 %} ``` `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 ``` ### 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" }} ``` ### contains Checks if one value is partially included in another. 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 | | `contains` | Function | Partial string match | | `floatformat` | Filter | Format 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/reporter/using-reporter Manage Reporter templates, generate regulatory reports on demand, track their status, and download completed files — all from the Lerian Console interface. 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 is running 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. See [Template examples](/en/reporter/template-examples) and the [template reference](/en/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/reporter/upload-template) as a multipart request with all three 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. Reporter returns the template identifier that you use when generating reports. ### Maintain existing templates Use the template endpoints to: * [List templates](/en/reference/reporter/list-templates). * [Retrieve template details](/en/reference/reporter/retrieve-template-details). * [Update a template](/en/reference/reporter/update-templates). * [Delete a template](/en/reference/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/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/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/reporter/list-data-sources) returns the available sources and their schemas and tables. * [Retrieve a data source](/en/reference/reporter/retrieve-data-source) returns the tables and fields for one source. These endpoints are read-only. Operators configure data sources at deployment time; the API doesn't create or update them. *** ## Interpret report statuses and errors Call [Check report status](/en/reference/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 only `Finished` 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/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. The download endpoint serves only reports with `Finished` status. *** ## Troubleshooting | Symptom | Check | | ------------------------------ | -------------------------------------------------------------------------------------------------------- | | Template upload is rejected | Send `template`, `outputFormat`, and `description`, 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 only when it is `Finished`. | *** ## 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` | ```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 ``` The MinIO and SeaweedFS examples below use HTTP only for local development. Production deployments require HTTPS and TLS. ```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 ``` ```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 ``` 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 Define each PostgreSQL or MongoDB source with `DATASOURCE__*` environment 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__OPTIONS` | Additional MongoDB URI options. | MongoDB only | | `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. The Manager loads data source configuration and connects on demand. The Worker connects during startup and retries unavailable sources. It can continue with reduced functionality when a source remains unavailable. *** ## Related tasks * [Get started with Reporter](/en/reporter/reporter-quick-start) * [Connect Reporter to Midaz](/en/reporter/connecting-reporter-to-midaz) * [Build templates](/en/reporter/template-reference) * [Review template examples](/en/reporter/template-examples) # Reporter in Lerian Console Source: https://docs.lerian.studio/en/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 — all from a single interface. Access the full documentation for using Reporter in the Lerian Console. ## Available actions *** From the Lerian Console, you can: * [Manage templates](/en/reporter/console/managing-templates) — Upload, update, and delete `.tpl` template files * [Manage reports](/en/reporter/console/managing-reports) — Generate, track, and download reports # What is Reporter? Source: https://docs.lerian.studio/en/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. It is available to licensed customers, and Lerian maintains its source code in a private repository. ## What Reporter helps you achieve 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 Reporting often combines data extraction, document formatting, and delivery in one fragile manual process. That approach becomes difficult to review and maintain as the number of reports grows. Reporter separates those concerns. 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 At a high level, 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 ## 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. * **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 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/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 Complete the shortest path from access to a generated report. Understand how Reporter reads data from a Midaz deployment. Follow task-oriented guidance for templates, data sources, and reports. Review the available template tags, filters, and expressions. # Audit and compliance Source: https://docs.lerian.studio/en/tracer/audit-compliance Understand how Tracer's immutable, hash-chained audit trail meets SOX and GLBA requirements, 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, and can we prove no one tampered with that answer?"* **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." That shift is the whole point. **Trade-off to be honest about:** 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. **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 compliance overview and retention sections assume zero API knowledge; the query and verify sections assume basic REST. Tracer maintains a complete, immutable audit trail of all validation decisions. This guide explains how the audit system works and how to query validation history for compliance reporting. ## Compliance overview *** Tracer is designed to meet the audit requirements of financial regulations including: | Regulation | Requirement | How Tracer complies | | ------------------------------------- | ------------------------------------------- | ---------------------------------------------- | | **SOX** (Sarbanes-Oxley) | Complete audit trail of financial decisions | Every validation is recorded with full context | | **GLBA** (Gramm-Leach-Bliley) | Protection of customer financial data | Data encrypted at rest and in transit | | **General audit** | Ability to reconstruct decisions | Immutable records with input/output snapshots | *** ## Audit trail architecture *** Tracer records every validation decision with full context for compliance and investigation. ### What gets recorded Every validation creates an immutable audit record containing: | 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 | Audit events are **deduplicated** for transaction validations. If a validation request is retried with the same `requestId`, only the first audit event is stored. 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**: * Records cannot be modified after creation. * The audit table is protected at the database level: a `TRUNCATE` trigger 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` and `message` reports tampering. This is the cryptographic basis for SOX/GLBA tamper-evidence guarantees. 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}`. The audit trail is designed for compliance audits. You can reconstruct exactly what happened for any validation, even years later, and prove that the records were not modified after the fact. *** ## Data retention *** Tracer retains data according to regulatory requirements and operational needs. ### Retention periods | Data type | Retention period | Reason | | ---------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------- | | **Validation records** | 7 years minimum | SOX/GLBA compliance requirement | | **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 | | **Application logs** | 90 days | Debugging and troubleshooting | ### Compliance considerations * **SOX requirement:** Maintain records for 7 years from the date of the audit report * **GLBA requirement:** Retain records demonstrating compliance with privacy rules * **Data export:** Records can be exported for external audit systems *** ## 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. **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`, it 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 Date parameters must use RFC3339 format with mandatory timezone. Date-only formats are rejected. **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 | When using cursor pagination, `sort_by` and `sort_order` are fixed 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 were 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` / `RESERVATION_SKIPPED` | Two-phase reservation lifecycle on the ledger seam: capacity held, committed, returned on abort, expired, or skipped on a ledger fail-open | Reservation events appear in the log but no filter selects them: `event_type`, `action`, and `resource_type` accept only the values listed in the table below, and a reservation value in any of them is rejected with error `0009`. To read reservation events, query by date range and page through the results. ### 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 or limit lifecycle event type | | `action` | enum | `VALIDATE`, `CREATE`, `UPDATE`, `DELETE`, `ACTIVATE`, `DEACTIVATE`, `DRAFT` | | `result` | enum | `SUCCESS`, `FAILED` (for CRUD) or `ALLOW`, `DENY`, `REVIEW` (for validations) | | `resource_type` | enum | `transaction`, `rule`, `limit` | | `resource_id` | UUID | The affected resource ID | | `actor_type` / `actor_id` | string | `user` 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 utilization 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 **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 * **Document your retention policy** alignment with Tracer's 7-year retention ### 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 what data was provided 4. **Check matched rules** to understand why the decision was made 5. **Verify limit status** if limits were involved *** ## 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 decision is governed by the `DEFAULT_DECISION_WHEN_NO_MATCH` environment variable (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 | Set `DEFAULT_DECISION_WHEN_NO_MATCH=DENY` for fail-closed semantics in high-security deployments. 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 | | ------------------- | ---------- | | Validation records | 7+ years | | Active rules/limits | Indefinite | | Application logs | 90 days | # Managing limits Source: https://docs.lerian.studio/en/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 currency. * 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 | An `ACTIVE` limit must be deactivated before moving back to `DRAFT`. ## Creating a new limit *** Click **New Limit**. 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. | | **Currency** | Yes | `BRL`, `USD`, `EUR` | | **Max Amount** | Yes | Numeric max amount | | **Scope** | No | Context restrictions | Click **Create**. New limits are created 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. `Limit Type` and `Currency` are locked when editing an existing limit. ## Scope *** Scope conditions support: | Scope type | Input | | ---------------- | ------------------------------------------ | | Account | UUID | | Merchant | UUID | | Segment | UUID | | Portfolio | UUID | | Transaction Type | Selector (`CARD`, `PIX`, `WIRE`, `CRYPTO`) | | Sub Type | Text | Use **Add Condition** to add scope entries. ## Related *** * [Spending limits](/en/tracer/spending-limits) - Conceptual guide * [Create limit](/en/reference/tracer/create-limit) - API reference * [Update limit](/en/reference/tracer/update-limit) - API reference * [Retrieve limit usage](/en/reference/tracer/retrieve-limit-usage) - API reference # Managing rules Source: https://docs.lerian.studio/en/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 | An `ACTIVE` rule must be deactivated before moving back to `DRAFT`. ## Creating a new rule *** Click **New Rule**. 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 | Context restrictions | Click **Create**. New rules are created 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 *** In **Scope**, you can add one condition per scope type: | Scope type | Input | | ---------------- | ------------------------------------------ | | Account | UUID | | Merchant | UUID | | Segment | UUID | | Portfolio | UUID | | Transaction Type | Selector (`CARD`, `PIX`, `WIRE`, `CRYPTO`) | | Sub Type | Text | Use **Add Condition** to append conditions. ## Related *** * [Rule engine](/en/tracer/rule-engine) - Conceptual behavior * [Create rule](/en/reference/tracer/create-rule) - API reference * [Update rule](/en/reference/tracer/update-rule) - API reference # Managing validations Source: https://docs.lerian.studio/en/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. 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 currency. * 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/tracer/audit-compliance) - Conceptual guide * [Integration guide](/en/tracer/integration-guide) - Validation generation flow * [Retrieve validation](/en/reference/tracer/retrieve-validation) - API reference * [List validations](/en/reference/tracer/list-validations) - API reference # Tracer in Lerian Console Source: https://docs.lerian.studio/en/tracer/console/tracer-module Open the Tracer module landing page in Lerian Console to navigate rules, limits, and validations from a single unified real-time transaction control view. **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. 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**. ## Module landing page *** The landing page includes: * An **About Tracer** block with module context. * A **Read the Docs** shortcut to external Tracer docs. * Navigation cards for **Rules**, **Limits**, and **Validations**. ## Available pages *** ### Rules and limits
Manage rule logic, decision action, lifecycle status, and optional scope conditions. Configure period-based spending caps with controlled activation and scope filtering.
### Audit and investigation
Inspect validation decisions, reason, transaction type, and evaluated rules in detail. Access conceptual guides and API references for Tracer.
## Related *** * [What is Tracer](/en/tracer/what-is-tracer) - Product overview * [Getting started](/en/tracer/getting-started) - Initial setup * [Integration guide](/en/tracer/integration-guide) - Connect Tracer to your systems # Creating a rule from a policy Source: https://docs.lerian.studio/en/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, rehearses it where it reaches nobody, and puts 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, and each change to the rule leaves an audit event behind it. **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 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 `currency` on the request | `amount`, `currency` | | "card-not-present" | `subType`, free-form text your integration sets | `subType` | | "a device we have not seen before" | metadata, 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?* `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). Do not repeat scope conditions inside the expression. `transactionType == "CARD"` in both places is not wrong, but 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 is not evaluated, 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 stores the rule name in a normalized form, so the `name` it returns can differ from the string you sent. Take the `ruleId` from the response — that is the handle every call below uses. See [Create a rule](/en/reference/tracer/create-rule). The expression is compiled 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 is considered only for transactions on that one test account, so activating it puts it in front of exactly the traffic you send it. ```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/tracer/activate-rule). ```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", "currency": "BRL", "transactionTimestamp": "'"$TS"'", "account": { "accountId": "8a1f2c34-5d6e-4a7b-8c9d-0e1f2a3b4c5d", "type": "checking", "status": "active" }, "metadata": { "deviceFirstSeen": true } }' ``` See [Validate a transaction](/en/reference/tracer/validate-transaction). ```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` is the rehearsal passing. 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`. Send a new `requestId` for every attempt. `requestId` is the idempotency key: repeat one and Tracer answers `200` with the decision it already recorded for that key, so the change you just made will look like it did nothing. 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, and 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. 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`), so 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/tracer/deactivate-rule). ```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/tracer/update-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" ``` 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. ```http theme={null} GET /v1/rules?status=ACTIVE&transaction_type=CARD&sort_by=updated_at X-API-Key: {api_key} ``` The listing answers "what is enforcing on card traffic right now" — see [List rules](/en/reference/tracer/list-rules). For one rule, `GET /v1/rules/{id}` returns the expression and scopes as stored ([Retrieve a rule](/en/reference/tracer/retrieve-rule)). Both report the stored state, not what each instance's cache holds. 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, and 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, and 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. `matchedRuleIds` 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 — the exception belongs inside the `DENY` expression, as one more condition that makes it narrower: ```cel theme={null} subType == "card_not_present" && amount > 5000 && metadata.deviceFirstSeen == true && metadata.customerTier != "vip" ``` Note what that costs: 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 is doing. | What changed | How | | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | The threshold, the entry mode, the device flag — anything inside the expression | 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}` | 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`. `POST /v1/rules/{id}/draft` is the step people miss; see [Draft a rule](/en/reference/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 was not enforcing, which is what a reviewer will look for. 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, stops being evaluated, 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 — and only after deactivating, because a rule in `ACTIVE` cannot be deleted: ```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/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. 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 *** **What usually goes wrong turning a policy into a rule:** * **"The rule is ACTIVE but nothing matches."** 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 stores names in a normalized form. 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/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/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 — so the decision lives in one place instead of being 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, not in the next release. Audit goes from "let me piece together logs from N systems" to "here is the immutable record of why this transaction got this decision." **Trade-off to be honest about:** you add one HTTP call to the critical path of every transaction (target p99 under 80ms). In return, you get a single point for policy, audit, and analytics — and you remove duplicated logic from product code. **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/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/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 * **Complete audit trail**: Immutable validation records for SOX/GLBA compliance * **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 executing transactions and 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; the Audit Context is intentionally not shown 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 is built around four bounded contexts: 1. **Validation Context** - Orchestrates requests, coordinates evaluation, records audit trail 2. **Rules Context** - Manages rule definitions and expression evaluation 3. **Limits Context** - Manages spending limits and usage tracking 4. **Audit Context** - Keeps the immutable event log and verifies its hash chain *** ## Prerequisites *** Before you start, make sure you have: * [ ] **Docker** and **Docker Compose** installed * [ ] **Go 1.26+** (for local development — the exact toolchain version is declared in the repository's `go.mod`) * [ ] **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 and audit trail. 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 | | 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) 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 has applied the schema and exited successfully, so the service always boots against an already-migrated database. Tracer is available to licensed customers; its repository is maintained internally. The steps below assume you already have access to the required Tracer project files. 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 two authentication modes. Which one you use depends on the deployment topology: | Deployment | Auth header | When to use | | ------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Single-tenant** | `X-API-Key: ` | Local development, single-customer BYOC, or any deployment with `MULTI_TENANT_ENABLED=false` (the default). | | **Multi-tenant (SaaS / BYOC Multi-Tenant)** | `Authorization: Bearer ` | Any deployment with `MULTI_TENANT_ENABLED=true`. The JWT is issued by [Access Manager](/en/platform/access-manager/access-manager) and carries the `tenantId` claim. | The remaining steps in this guide use the single-tenant API Key form because most local-development setups run that way. If your environment is multi-tenant, replace `X-API-Key: your-secure-api-key` with `Authorization: Bearer $JWT` in every example. ### API Key (single-tenant) 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 (multi-tenant) 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} # Single-tenant: List rules curl -H "X-API-Key: your-secure-api-key" \ http://localhost:4020/v1/rules ``` ```bash theme={null} # Multi-tenant: List rules curl -H "Authorization: Bearer $JWT" \ https://tracer.sandbox.lerian.net/v1/rules ``` API Keys and JWTs should be kept secure. Never expose them in client-side code or public repositories. API key authentication is **disabled by default** (`API_KEY_ENABLED=false`). The provided `.env.example` keeps it off so local development works without setup, but a production deployment **must** set `API_KEY_ENABLED=true` (single-tenant) or `MULTI_TENANT_ENABLED=true` and `PLUGIN_AUTH_ENABLED=true` (multi-tenant) before exposing 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 calendar day at 00:00 UTC | | `WEEKLY` | Maximum amount per week | A new count starts each ISO week, Monday at 00:00 UTC | | `MONTHLY` | Maximum amount per month | A new count starts on the 1st of the month at 00:00 UTC | | `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 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", "currency": "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 are created in `DRAFT` status and follow the lifecycle `DRAFT` → `ACTIVE` → `INACTIVE`. Inactive limits can return to `DRAFT` for editing or be permanently deleted. 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: the cap, the amount attempted, and the projected consumption of that cap's current period if the transaction is allowed. `GET /v1/limits/{id}/usage` reports a cumulative total across the limit's counters, for reviewing 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, currency, timestamp) * Account information * Optional: segment, portfolio, merchant, and custom metadata ```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", "currency": "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" } }' ``` The `transactionTimestamp` must be recent, which is why the example generates it: future timestamps are rejected with error code `0419` (1-minute clock skew tolerance), and timestamps older than 24 hours are rejected with error code `0421`. `requestId` is the idempotency key. Send a new UUID for each attempt — repeat one and Tracer returns the decision it already recorded for that key, so 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 includes details about which rules were evaluated, which matched, and the current limit usage — useful for debugging and customer support. **Why Tracer returns a decision instead of blocking directly.** Tracer is a decisioning layer, not an authorization gateway. The calling system is the one that holds the customer relationship, knows the channel, and decides what to do with a DENY — for example, your card-issuing system may decide to honor a `DENY` for a stand-in pre-auth but still want to 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/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" } ] }' ``` Tracer keeps the rule name in a normalized form, so the `name` in the response can differ from the string you sent. 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" ``` Activation takes effect right away on the instance that served the activate call, so a single-instance setup starts evaluating 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 (`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`. Active rules can be deactivated and reactivated 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_audit_persist_failures_total` - Audit record persistence failures (compliance risk) * `tracer_validation_rollback_failures_total` - Usage rollback failures during REVIEW decisions (eventual consistency gaps that self-correct at period boundaries) Standard HTTP request metrics are provided automatically by Tracer's built-in OpenTelemetry HTTP middleware. *** ## Verification *** Confirm that everything is working correctly. ### Checklist * [ ] Docker services started and healthy * [ ] API Key authentication working * [ ] Spending limit configured * [ ] Test transaction validated successfully * [ ] Rule created and activated *** ## Next steps *** You've successfully set up Tracer and validated your first transaction. 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 * **[Audit and compliance](./audit-compliance.mdx)** - Query validation history and understand the audit trail *** ## Quick reference *** The three flows you'll use most: * **Validate a transaction**: `POST /v1/validations` — see the [Tracer API quick start](/en/reference/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 | Tracer returns decisions as recommendations. Your system is responsible for implementing the appropriate action based on each decision. # Integration guide Source: https://docs.lerian.studio/en/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; 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 audit. Done poorly — no timeout, no retry strategy, no fallback — it becomes a single point of failure. **Trade-off to be honest about:** you're adding a network hop. The good news is the contract is simple: idempotent by `requestId`, no callbacks, deterministic three-state response. The bad news is you need to think about timeouts, retries, and what to do if Tracer is unreachable — most of this guide is about that. **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 only submits the transaction to Midaz 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. Midaz can also drive Tracer itself, through an optional per-ledger reservation seam that is off by default: the ledger reserves limit capacity before committing a transaction, then confirms or releases the hold. That seam still runs Midaz → Tracer; Tracer never calls out to the ledger. The rest of this guide covers the application-orchestrated pattern. ## Integration overview *** Tracer is designed to be called by **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 **Key principle:** Tracer does not fetch external data during validation. Your system is responsible for providing all context needed for rule evaluation. *** ## Payload-Complete Pattern *** Tracer uses the **Payload-Complete Pattern**, which means all context required for validation must be included in the request. This design ensures: | Benefit | Description | | ----------------------- | ------------------------------------------------------------------- | | **Predictable latency** | No external calls during validation; response time stays under 80ms | | **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 Tracer validates what you send. If your payload is missing 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. **Recording audit trail** for compliance 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 ### Step 2: Call Tracer API Send a POST request to `/v1/validations` with the complete transaction context including: * Transaction details (type, subType, amount, currency, timestamp) * Account information (required) * Optional: segment, portfolio, merchant, and custom metadata For complete payload structure and field details, see the [API reference](/en/reference/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 audit trail correlation, 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. 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. **Why it matters:** Network timeouts and retries can cause duplicate requests. Without idempotency, a retried request could double-count against limits or create duplicate audit 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) 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 that can be used independently or combined. ### 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 both modes are enabled, Tracer uses this priority: 1. If `PLUGIN_AUTH_ENABLED=true` and the endpoint is not flagged for API-key-only → Plugin auth 2. If `API_KEY_ENABLED=true` or the endpoint is flagged for API-key-only → 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. The `/v1/validations` endpoint can be configured 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. **This flag is incompatible with multi-tenant mode** (`MULTI_TENANT_ENABLED=true`) — the service fails to start with error code `0458`. ### Multi-tenant authentication When `MULTI_TENANT_ENABLED=true`, Tracer runs in multi-tenant mode and the authentication model changes: * **Plugin auth is required.** The service fails to start with error code `0457` if `PLUGIN_AUTH_ENABLED=false`. * **Every `/v1/*` request must carry a JWT bearer token** issued by [Access Manager](/en/platform/access-manager/access-manager): `Authorization: Bearer `. * **`tenantId` is resolved from the JWT claim**, not from a header, path, body, metadata, or rule scope. There is no `X-Tenant-ID` header — passing the tenant identifier anywhere other than the token claim is unsupported and ignored. * Each tenant operates on its own PostgreSQL database. The tenant-specific connection is resolved by the multi-tenancy platform service at request time. * **Public endpoints (`/health`, `/readyz`, `/metrics`, `/version`) stay unauthenticated** in multi-tenant mode too — the bearer-token requirement applies only to `/v1/*`. If the JWT is missing, malformed, or expired, the request returns HTTP 401 with `"code": "Unauthenticated"` (the same code used for missing API keys; no separate TRC code is emitted). One case is distinct: a token that **parses but carries no `sub` claim** is rejected up front with HTTP 401 and error code `0474`. `sub` is what the audit writer uses to attribute the action to a principal, so Tracer fails loudly rather than recording the change against a generic system actor — make sure your Access Manager tokens always carry it. If the multi-tenant deployment hits its per-instance tenant cap, requests for cold tenants return HTTP 503 with error code `0466` and a `Retry-After` header. The client should back off and retry; the cap auto-resets as the LRU pool evicts cold tenants. See [Multi-tenancy](/en/multi-tenancy) for the platform-wide tenant model. *** ## Performance considerations *** Optimize your integration for low latency and high reliability. ### Timeout budget Tracer is designed to respond 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 ``` 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. **Common integration pitfalls:** * **"My retry created a duplicate validation in the audit trail."** Reuse the same `requestId` across retries. Tracer deduplicates by that field — the second call returns the cached result (HTTP 200) without creating a duplicate event. 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's work is already wasted on returning a response. Set client timeout aggressively (100ms) and trust the retry path. * **"My validation is rejected 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, you need to set the timestamp to the actual transaction moment, not the moment you're calling Tracer. * **"Test transactions show up in production audit."** All validations are recorded, including 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 | Stale data leads to incorrect decisions. If an account was suspended but your cache shows it as active, Tracer will allow transactions that should be denied. 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: * [ ] API Key is configured and secured * [ ] Each request includes a unique requestId (UUID) * [ ] Client handles both 201 and 200 responses as success * [ ] Client timeout is set to 100ms * [ ] Retry logic is implemented for 5xx errors * [ ] Fallback behavior is defined * [ ] All required fields are populated * [ ] Timestamps use RFC3339 format with timezone * [ ] Currency codes are uppercase ISO 4217 * [ ] Decision handling is implemented (ALLOW/DENY/REVIEW) * [ ] Validation IDs are logged for audit trail 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, "currency": transaction.currency.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 * **[Audit and compliance](./audit-compliance.mdx)** - Query validation history and audit trail # Reviewing a denied transaction Source: https://docs.lerian.studio/en/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 is asking 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 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 its audit event can be checked against the hash chain. **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`; repeating a `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", "currency": "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/tracer/validate-transaction). 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) | | `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 | `reason` carries the same text on `REVIEW` decisions — `Rule matched with REVIEW action` — and Step 3 reads a review the same way it reads a denial. 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/tracer/retrieve-rule). Compare `matchedRuleIds` with `evaluatedRuleIds` when the question is the opposite one — "why did my rule *not* fire?". A rule absent from `evaluatedRuleIds` was not evaluated for this transaction, so start with its status and its scope rather than its expression. 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`, with `currentUsage` reporting what that limit's current period and matched scope would hold if this transaction were allowed. 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/tracer/retrieve-limit). The decision record keeps the ceiling, period, and scope as they were when the decision was made, so 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 *** Every decision is stored 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`, `currency`, `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/tracer/retrieve-validation). This is the record to read out in a dispute: it is the input and the outcome in one document, and 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} ``` **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`, `currency`, `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/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", "currency": "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/tracer/retrieve-audit-event) | 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/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, so 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/tracer/verify-audit-event). 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 *** **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 is the projected figure, with the attempted amount already added. On an exceeded limit the counter was not incremented, 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/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/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 transactions are approved or blocked, without touching application code. Each rule is a small expression that runs on every transaction Tracer validates — "block this MCC for this segment", "send anything over R\$ 50k to manual review", "deny if the account is suspended". **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. Every match is recorded, so a denied customer call six months later can be traced back to the exact rule that fired. **Trade-off to be honest about:** 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. **Who is this guide 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 type-safe expression language from Google. Expressions are compiled at rule creation and run 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 * **Performance**: Compiled expressions evaluate in under 1ms each * **Type safety**: Expression syntax validated at rule creation * **No short-circuit**: matching rules are evaluated together, so the audit trail records the rules that triggered, not just 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 In this flow: * **Load rules** fetches all active rules from cache (or database on cache miss) * **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 Rules whose scope matches the transaction are evaluated together. There is no priority ordering and no short-circuit evaluation. This ensures: * Complete audit trail (all matching rules recorded) * No information loss (analysts can see all triggers) * Simple logic (no priority conflicts) **Decision precedence** (highest to lowest): 1. **DENY** — any matching `DENY` rule wins outright. 2. **Limit exceeded** — if no DENY rule matched but any applicable limit is exceeded, the decision is DENY (rule precedence applies first; limits come in only when no DENY rule matched). 3. **REVIEW** — if no DENY rule matched and no limit was exceeded, any matching `REVIEW` rule wins. 4. **ALLOW** — if only `ALLOW` rules matched, the decision is ALLOW. 5. **Default** — if no rule matched at all, Tracer returns the configured `DEFAULT_DECISION_WHEN_NO_MATCH` (`ALLOW` unless explicitly set to `DENY` for fail-closed deployments). Only `ALLOW` and `DENY` are accepted; `REVIEW` is deliberately not a valid no-match default, and any other value fails the service at boot. `matchedRuleIds` in the response contains every rule that matched, regardless of the winning category, so audit consumers can see all triggers. **Why DENY beats REVIEW beats ALLOW.** Precedence is fixed and not configurable, 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. Tracer returns decisions; it does not block transactions directly. Your system receives the decision and is responsible for taking 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 Expressions are written 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 are caught when you create the rule—not when transactions are being processed. 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. Both `merchant.category` and `merchant["category"]` are accepted; 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/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. | | `currency` | string | ISO 4217 code (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` accepts `active`, `suspended`, `closed`, and `account.type` accepts `checking`, `savings`, `credit`. * `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. `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 == "..."`. A rule that reads a context field the request does not carry does not match, and the other rules still run — so you do not need a presence guard for that case. When presence itself is the condition you want, write `size(segment) > 0` or `"risk_score" in metadata`. Expression cost is bounded by `CEL_COST_LIMIT` (default `10000`). The check runs at **compile time — on create, on expression update, and again on activate** — not only at activation: an expression whose worst-case estimated cost exceeds the limit is rejected with error code `0342` (cost limit exceeded) the first time you submit it. 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 organized 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 ``` Metadata fields are provided by your integration. 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 that is not specified is treated as a wildcard (matches any value). At least one field must be set — empty scope objects (`{}`) are rejected 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 ### 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) | Active rules must be deactivated before deletion. This prevents accidental removal of rules that are currently being evaluated. *** ## Create a rule *** Create rules using `POST /v1/rules`. Rules are created in `DRAFT` status by default. A rule requires: * **name**: A descriptive name, unique **within its context**. The context is derived from the rule's scopes (the lowest `segmentId` across them); 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-insensitive and ignores repeated whitespace; a collision returns `409 Conflict` with error code `0441`. Tracer keeps the name in a normalized form, so the `name` it returns can differ from the string you sent — 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/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) | 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 *** Update rules using `PATCH /v1/rules/{id}`. Rules can be updated in any status, with one important restriction: 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 that are no longer needed. Only DRAFT and INACTIVE rules can be deleted. ACTIVE rules must be deactivated first. ```http theme={null} DELETE /v1/rules/{id} X-API-Key: {api_key} ``` Deletion is permanent. Deleted rules cannot be recovered and 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 are triggering * **Monitor DENY rates** - High deny rates may indicate overly aggressive rules * **Audit regularly** - Ensure rules still align with business requirements **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 — `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 `{}` is being 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/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 stops being a constant compiled into a service. It becomes a stored definition you create, activate, raise, and stop — and every decision that touches it reports what it consumed. **Who is this guide 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 currency of 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 choice that decides the answer to "R\$ 20,000 per account" is **which id you name**: ```json theme={null} "scopes": [ { "accountId": "550e8400-e29b-41d4-a716-446655440100" } ] ``` The cap is addressed 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. ```json theme={null} "scopes": [ { "segmentId": "2f1c9b7e-40a5-4d18-9c6b-3e7f5a0d1b24" } ] ``` The cap is addressed 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. ```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. A limit also carries a `currency`, and Tracer checks a limit against a transaction only when the two match. A limit created in `BRL` is not checked against a `USD` transaction, so 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. An empty list, or an empty object, is rejected — 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 calendar day, UTC | Each day at 00:00 UTC | | `WEEKLY` | One ISO week, UTC | Each Monday at 00:00 UTC | | `MONTHLY` | One calendar month, UTC | On the 1st at 00:00 UTC | | `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`. **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, so that amount lands in the August count, not July's. When a cap is agreed in local terms, expect the last hours of the local month to belong to the next one. `CUSTOM` takes `customStartDate` and `customEndDate` and is the shape for a campaign or a promotion. Those two fields belong to `CUSTOM` and are rejected 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. It is created in `DRAFT`, which means it is stored but not yet checked. ```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", "currency": "BRL", "scopes": [ { "accountId": "550e8400-e29b-41d4-a716-446655440100" } ] }' ``` A successful call answers `201`. The body is 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` — the limit is not being checked yet. [Step 4](#step-4-activate-it) changes that | **Names are unique across limits, and the character set is narrow.** `name` accepts ASCII letters and digits, spaces, and `-`, `_`, `.`, `(`, `)`. An accent or a dash typed as an em dash is rejected with `0371`. A name another live limit already holds is rejected with `0442`; deleting a limit frees its name again. For the full payload and every optional field, see [Create a limit](/en/reference/tracer/create-limit). *** ## Step 4: Activate it *** A `DRAFT` limit is not checked. 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, every `POST /v1/validations` whose transaction matches the scope and the currency is measured against it. **Activating mid-month does not import the month's history.** Tracer counts a transaction against a limit at the moment it decides on it, so 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/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 — the cap is now spent, and the next transaction on that account is denied until August. When a limit is exceeded, the decision is `DENY` and `reason` is `limit_exceeded`; the amount that would have crossed the ceiling is not added to the count. 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. A counter is deleted 90 days after its period ends, so on a long-running limit this total covers only the periods still retained, not the limit's full lifetime. See [Usage tracking](./spending-limits.mdx#usage-tracking). See [Retrieve a limit usage snapshot](/en/reference/tracer/retrieve-limit-usage). *** ## Step 6: Change it, pause it, remove it *** ```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` and `currency` are not — a request that sends either is rejected with `0380`, and a different period or currency means a new limit. A body with nothing in it is rejected with `0183`. See [Update a limit](/en/reference/tracer/update-limit). Changing the ceiling does not clear the running count. Lower it below what the current period already consumed and the account is denied until the next period starts. ```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 stops being checked. It keeps its definition and its place in the audit trail, and `POST /v1/limits/{limitId}/activate` puts it back in the path. See [Deactivate a limit](/en/reference/tracer/deactivate-limit). ```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/tracer/draft-limit). ```bash theme={null} curl -X DELETE http://localhost:4020/v1/limits/{limitId} \ -H "X-API-Key: your-secure-api-key" ``` Answers `204`. A limit that is currently `ACTIVE` has to be deactivated first — Tracer rejects the deletion with `0363`, which is what keeps a live ceiling from disappearing by accident. See [Delete a limit](/en/reference/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/tracer/list-limits), and [Retrieve a limit](/en/reference/tracer/retrieve-limit) for one by id. *** ## What goes wrong *** **What usually goes wrong when a cap is set up:** * **"The customer overspent and nothing stopped them."** Check `status` first — a limit in `DRAFT` or `INACTIVE` is not checked. Then check that the limit's `currency` 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."** Period boundaries are 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. * **"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 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 | `currency` is three uppercase letters but not an ISO 4217 code | | `0371` | 400 | `name` carries a character the field does not accept | | `0380` | 422 | The request tried to change `limitType` or `currency` | | `0442` | 409 | Another live limit already holds that name | The complete list is in the [Tracer error list](/en/reference/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/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 stop being 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 to be honest about:** counters need to stay consistent across replicas and races. Tracer handles that transactionally — if a transaction is denied or sent to REVIEW, the counter rolls back. You give up "local clever logic in each service" and gain a single, consistent number. **Who is this guide for?** Product managers configuring caps, risk teams reviewing exposure, compliance auditing what was 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). Limits are evaluated 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 * **Real-time tracking**: Every decision reports how much of each cap it consumed * **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 calendar day at 00:00 UTC | | `WEEKLY` | Maximum amount per week | A new count starts each ISO week, Monday at 00:00 UTC | | `MONTHLY` | Maximum amount per month | A new count starts on the 1st of the month at 00:00 UTC | | `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 | ### Time windows Time windows restrict **when** a limit is enforced during the day. When a transaction occurs outside the configured time window, the limit is **skipped** (not enforced) and the transaction is allowed to proceed without counting against that limit. * **Format**: `HH:MM` (24-hour, UTC) * **Both fields required**: If `activeTimeStart` is set, `activeTimeEnd` must also be set (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 Time windows can be applied to **any** limit type (DAILY, WEEKLY, MONTHLY, CUSTOM, or PER\_TRANSACTION). If no time window is configured, the limit is active 24/7. **Example: Pix compliance** A financial institution needs to enforce lower Pix transfer limits during nighttime hours (as recommended by BACEN): * `limitType`: `DAILY` * `maxAmount`: `"1000.00"` * `activeTimeStart`: `"20:00"` * `activeTimeEnd`: `"06:00"` * Scope: Pix transactions Transactions between 20:00 and 06:00 UTC are checked against the R\$ 1,000 limit. Transactions outside this window are not affected by this limit. ### 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 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 Time windows and custom periods can be used together on `CUSTOM` limits. When combined, a transaction must be within **both** the custom period **and** the time window to be evaluated against the limit. For example, a `CUSTOM` limit with `customStartDate` Nov 25 to `customEndDate` Nov 30 and a time window of `09:00` to `18:00` would only enforce the limit 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 that is not specified is treated as a wildcard (matches any value). At least one field must be set — empty scope objects (`{}`) are rejected 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.** When a transaction matches multiple limits (for example, both an account-level and a segment-level limit), Tracer checks **all** applicable limits independently in a single transaction. The transaction is denied as soon as any one of them is exceeded. ### 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). A counter is kept for **90 days** after its period ends, then a background worker deletes it. *** ## How limits work *** Tracer evaluates limits during every validation request. ### Limit check flow When a transaction is validated, Tracer checks all applicable limits: How Tracer checks all applicable spending limits during a validation request and updates their usage counters 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, the limit is **skipped** (the client-supplied `transactionTimestamp` is not used here) 3. **Check custom period** - If the limit is `CUSTOM`, verify the **current server time** falls within `customStartDate`/`customEndDate`. If outside, the limit is **skipped** (again, `transactionTimestamp` is not used) 4. **Calculate projected usage** - Add transaction amount to current usage 5. **Compare threshold** - Check if projected usage exceeds limit amount 6. **Return result** - If any applicable limit is exceeded — or any DENY rule matches — Tracer returns a DENY decision (your system should then block the transaction) Limit checks and counter increments are **transactional**. If a transaction is denied (by limits or rules) or flagged for review, all counter increments are rolled back atomically. This prevents limit leakage from partial operations. When a limit is **skipped** 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 Skipped limits are reported for transparency but do **not** participate in the DENY decision, and their counters are **not** incremented. The window check uses **server time**, not the client-supplied `transactionTimestamp`, to prevent timestamp-manipulation attacks. **Why server time instead of `transactionTimestamp`.** The client can set `transactionTimestamp` to whatever they want — including 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; in practice, Tracer's NTP-synced clocks keep this in single-digit milliseconds. ### 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`. Limits are created 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"`) * **currency**: ISO 4217 currency code (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) Limit names must be globally unique across all non-deleted limits — unlike rule names, which are unique only within their scope context. Uniqueness is enforced on the name **exactly as stored**, after leading and trailing whitespace is trimmed: 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/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 `POST /v1/validations` response carries `limitUsageDetails`, with one entry per limit Tracer checked. Each entry reports: * **limitId** and **limitAmount** — which cap was checked, and its ceiling * **currentUsage** — the projected consumption of that cap's current period and matched scope if this transaction is allowed * **attemptedAmount** — the amount checked against the cap * **exceeded** — whether the attempted amount would push this cap past its ceiling; every cap is evaluated, 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, so use it to review a limit's overall consumption rather than to answer how much a customer has left in the current period. A counter is deleted 90 days after its period ends (see [Usage tracking](#usage-tracking)), so 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` and `currency` fields are immutable and cannot be changed after creation. Changing the limit amount does not clear the current count. If you reduce a limit below what the current period already consumed, subsequent transactions are denied 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 ### 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; can be reactivated | | `DELETED` | Permanently removed; does not appear in listings | ### 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 | | Delete | DRAFT, INACTIVE | DELETED | Permanently remove (cannot delete ACTIVE limits) | *** ## 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 * **Read the decision payload** - `limitUsageDetails` shows how much of each cap every transaction consumed * **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 **Common pitfalls when working with limits:** * **"My customer is reporting overspend — they should have hit the limit."** Check whether the limit is `ACTIVE`. A limit in DRAFT or INACTIVE state is not evaluated. Also confirm the limit's scope actually matches the transaction (segment, transaction type, etc.). * **"My PATCH lowered the limit but transactions are still being denied."** Lowering the limit does not clear the count. If the current period already consumed more than the new ceiling, subsequent transactions are denied 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), the optional time-window and custom-period fields, and the full scope-field list, see [Limit types](#limit-types) earlier in this guide and the [API reference](/en/reference/tracer/create-limit) for schema-level details. # Environment variables Source: https://docs.lerian.studio/en/tracer/tracer-environment-variables Reference for Tracer environment variables covering ports, TLS, PostgreSQL, authentication, 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 — TLS posture, OpenTelemetry, Access Manager authentication, multi-tenancy, service discovery, and event streaming — see the [BYOC configuration reference](/en/reference/byoc-configuration). This page focuses on what is distinctive to 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 (opt-in) | `TRACER_GRPC_PORT` | unset (off) | 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 | | ---------------------- | -------------------------------------------------------------------------------------------------- | ------- | -------- | | `VERSION` | Service version tag | varies | No | | `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 returned when no rule matches a transaction. Only `ALLOW` (fail-open) and `DENY` (fail-closed) are accepted — `REVIEW` is deliberately rejected, 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, all origins are allowed — lock to explicit origins in production. The explicit value `*` is rejected at boot when `API_KEY_ENABLED=true`. | — (allow all) | No | | `TRUSTED_PROXY_CIDRS` | Trusted proxy CIDRs for `X-Forwarded-For` parsing. Set when running behind a load balancer. | — (uses peer IP) | No | ## Database (PostgreSQL) Tracer stores rules and usage counters in its own `tracer` database on the shared Midaz PostgreSQL primary. Schema migration is applied by a dedicated migration-runner image before the application starts; the service boots against an already-migrated schema and does not run migrations in-process. | 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_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. Off unless `TRACER_GRPC_PORT` is set. | Variable | Description | Default | Required | | --------------------------- | -------------------------------------------------------------------- | ----------- | -------- | | `TRACER_GRPC_PORT` | gRPC listen port for the reservation server | unset (off) | No | | `TRACER_TLS_MODE` | Seam security: `mesh` (TLS terminated by the service mesh) or `mtls` | `mesh` | No | | `TRACER_TLS_CERT_FILE` | Server certificate PEM path (when `mtls`) | — | No | | `TRACER_TLS_KEY_FILE` | Server private-key PEM path (when `mtls`). Sensitive. | — | No | | `TRACER_TLS_CLIENT_CA_FILE` | Client CA certificate PEM path for mutual verification (when `mtls`) | — | No | ## Shared configuration backbone The following blocks are identical across Lerian products and are documented in full in the [BYOC configuration reference](/en/reference/byoc-configuration). They default off. * **Access Manager authentication** — `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_ADDRESS`. Enable in production. In discovery-enabled builds (see **Service discovery** below), Tracer resolves the Access Manager host through Consul and falls back to `PLUGIN_AUTH_ADDRESS` if resolution fails; current builds always use `PLUGIN_AUTH_ADDRESS`. * **Multi-tenancy** — `MULTI_TENANT_*`, plus Tracer's per-tenant pool knobs (`MULTI_TENANT_MAX_TENANT_POOLS`, `MULTI_TENANT_MAX_OPEN_CONNS_PER_TENANT`, `TENANT_CAP_RETRY_AFTER_SECONDS`). Off by default. `APPLICATION_NAME` identifies the module to Tenant Manager. * **Service discovery** — `SD_*` (Consul). Off by default, and temporarily inert: current builds compile the Consul integration out, so `SD_ENABLED=true` has no effect and Tracer always resolves Access Manager from the static `PLUGIN_AUTH_ADDRESS`. In discovery-enabled builds, Tracer registers itself as `midaz-tracer` (advertising the HTTP port from `SERVER_ADDRESS`, default `4020`) and resolves Access Manager through Consul. The reservation gRPC seam is not advertised — Tracer is the server on that seam. * **Event streaming** — `STREAMING_*` (lib-streaming producer). Off by default. * **OpenTelemetry** — `ENABLE_TELEMETRY`, `OTEL_*`. Tracer also exposes a Prometheus `/metrics` endpoint. # Tracer in Lerian Console Source: https://docs.lerian.studio/en/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 rules (`Rules`) * Transaction caps (`Limits`) * Validation history (`Validations`) To access Tracer in the Lerian Console, you need to acquire the product. Contact your account manager for more information. Access the full documentation for using Tracer in the Lerian Console. ## Available actions *** From the Lerian Console, you can: * [Rules](/en/tracer/console/managing-rules) — Create and manage transaction rules * [Limits](/en/tracer/console/managing-limits) — Configure and monitor spending limits * [Validations](/en/tracer/console/managing-validations) — Inspect validation history and details # What is Tracer? Source: https://docs.lerian.studio/en/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 — all in real time, before a transaction executes. For every transaction, Tracer evaluates your business rules and spending limits, then returns an instant decision: allow, deny, or flag for review. Response times stay under 100ms, so customers experience no delays. Tracer is available to licensed customers; its repository is maintained internally. ## 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, and only submits to the ledger if the decision is `ALLOW` — so unauthorized or over-limit movements never reach the ledger in the first place. Tracer stays **separate from the ledger**: it never calls Midaz and never reads balances — it only evaluates the context sent to it. 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/reference/tracer/create-reservation) — off by default — where the ledger holds limit capacity before it commits a transaction and then confirms or releases the hold. Either way, traffic flows into Tracer, never out of it. For where this sits in the bigger picture, see [Building a complete core banking](/en/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 foundational capabilities: * **Validation rules**: Business rules that evaluate transaction context and return instant decisions — without requiring code deployments. You define rules using expressions, not code, making them accessible to analysts and compliance teams. * **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. Whether you need to detect transactions that exceed spending caps, evaluate custom business rules, or maintain a complete audit trail for SOX/GLBA compliance, Tracer provides the speed, flexibility, and governance required for modern financial operations. ## Key capabilities *** * Real-time validation with response times under 80ms (p99) * Expression-based rule engine using CEL (Common Expression Language) 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 * Complete audit trail with 7+ year retention for SOX/GLBA compliance * Configurable no-match default (`DEFAULT_DECISION_WHEN_NO_MATCH`) — when no rule matches a transaction, 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 is built around 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 for SOX/GLBA compliance. When a transaction is submitted for validation: How Tracer processes a validation request across its Validation, Rules, and Limits contexts and returns an ALLOW, DENY, or REVIEW decision; the Audit Context is intentionally not shown All active rules are evaluated, and if any DENY rule matches, Tracer returns a DENY decision. Limits are checked against current usage, and the decision is returned with a complete audit record. **Your system is responsible for acting on this 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, holds limit capacity before committing and settles the hold afterwards | `POST /v1/reservations`, then the confirm / release transitions | | **Tracer** | Evaluates rules and limits, records the result, returns the decision | Runs internally — never calls back | | **Compliance / audit** | Queries the trail, verifies hash chain integrity, runs SOX/GLBA reports | `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**. Rule and limit definitions are set up beforehand by risk/product; compliance reads the audit log later. Tracer never reaches back into your stack — there are no webhooks or callbacks. Monetary values (transaction `amount`, spending limit `maxAmount`, and usage counters) are expressed as decimal strings, for example `"1500.00"` or `"50000.00"`. ## When to use Tracer *** Tracer fits naturally 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**, not a complete fraud management system. It is a specialized component optimized for low-latency decisions (\< 100ms) that validates pre-enriched payloads against configurable rules and limits. This focused scope enables Tracer to deliver predictable performance while integrating seamlessly with broader fraud prevention ecosystems. ### 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 for compliance (SOX/GLBA) * **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 | These boundaries keep Tracer fast, predictable, and focused on its core value: instant transaction decisions with complete audit trails. ## Integration *** Integrating with Tracer is straightforward. Your authorization system sends validation requests with complete transaction context, and Tracer responds with a decision—typically in under 35ms. This **Payload-Complete Pattern** keeps things simple: all the context Tracer needs arrives in a single request, so there are no external calls during validation and latency stays predictable. Tracer runs single-tenant by default and supports multi-tenant operation when deployed in SaaS or BYOC Multi-Tenant mode. In multi-tenant mode, tenant resolution and isolation work the same way as in the rest of the platform — see [Multi-tenancy](/en/multi-tenancy) for the model and [Access Manager](/en/platform/access-manager/access-manager) for the auth flow. Like all Lerian products, Tracer ships with native [Access Manager](/en/platform/access-manager/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 *** Browse endpoints for validation rules, spending limits, and audit trails. Set up Tracer and configure your first authorization rule. # Fetcher architecture Source: https://docs.lerian.studio/en/fetcher/fetcher-architecture How Fetcher runs: a Manager service, a Worker service, and the dependency-free extraction Engine that both of them host in process. Fetcher ships as two services. The **Manager** exposes the HTTP API and dispatches work. The **Worker** consumes that work and produces results. Both services are **hosts**. They host the **Engine** for the shared extraction contract. The Worker uses it for generic datasources; `plugin_crm` remains an explicit Worker-owned compatibility path. That division explains why your own application can run generic Fetcher extraction without either service. ## The two services *** | Service | Role | | ----------- | ------------------------------------------------------------------------------------------------------------------ | | **Manager** | HTTP API for connections and jobs. Keeps metadata in MongoDB. Publishes extraction jobs to RabbitMQ. | | **Worker** | Queue consumer. Runs the extraction, protects the result, writes it to object storage, and emits a terminal event. | ### Manager The Manager follows a hexagonal layout. HTTP adapters map requests onto services, and services reach infrastructure only through ports. Read paths and write paths live in separate service packages. It serves twelve operations across two path prefixes: | Path | Operations | | ------------------------------------------------------------- | ---------------------------------------------------------------------- | | `/v1/fetcher` | Create an extraction job. Get a job by identifier. | | `/v1/management/connections` | List and create connections. Validate a schema mapping. | | `/v1/management/connections/{id}` | Get, update, delete, test, and discover the schema of one connection. | | `/v1/management/connections/unassigned` and `.../{id}/assign` | Two migration operations for connections that predate product scoping. | Three infrastructure dependencies back the API: * **MongoDB** holds connection records and job records. * **RabbitMQ** carries a new job to the Worker on the queue that `RABBITMQ_FETCHER_WORK_QUEUE` names. * **Valkey or Redis** backs the schema cache and the rate limiter on the connection test. The schema cache falls back to process memory when Redis is unreachable. A create-job request answers `202 Accepted`. A repeat of the same request inside a five-minute window answers `200 OK` with the job that already exists. ### Worker The Worker has no primary HTTP server. It consumes the work queue and runs `RABBITMQ_NUMBERS_OF_WORKERS` jobs in parallel, five by default. A small health server on `HEALTH_PORT` carries the probes and the metrics endpoint. For each job the Worker: 1. Resolves the connections the job maps and runs the extraction through the embedded Engine. 2. Signs the plaintext JSON with HMAC-SHA256, using a key derived from the master key. 3. Encrypts the signed payload with AES-GCM and writes it to S3-compatible object storage. 4. Publishes `job.completed` or `job.failed` to the exchange that `RABBITMQ_JOB_EVENTS_EXCHANGE` names. The Worker drives the Engine in direct mode. The Engine hands back the exact bytes, and the Worker owns the protection and the storage of those bytes. Both terminal events are mandatory routes. The Worker refuses to start when `STREAMING_ENABLED` is false or the events exchange is blank, because a silent no-op emitter would swallow the contract. The Worker speaks the S3 protocol. SeaweedFS works as a storage target through its S3-compatible gateway, which is how the local Docker Compose stack runs. Set retention on results with a lifecycle policy on the bucket. ### Messages between them Every message the Manager publishes carries an HMAC-SHA256 signature. The signed payload binds the timestamp, the signature version, the tenant ID, the job ID, the exchange, and the routing key. The body comes last. A replay of the same body under a different tenant or a different route fails verification. The signer refuses any key shorter than 32 bytes and compares signatures in constant time. ## The Engine underneath *** The Engine owns what an extraction **means**. A host owns how it **runs**. | The Engine decides | The host provides | | ------------------------------- | ---------------------------------------------- | | Connection lifecycle rules | Where connections live | | Schema discovery and validation | The driver that reads the catalog | | Query planning and execution | The database connectors | | Resource limits and timeouts | The transport that surfaces them | | Tenant scope on every operation | The tenant identity from the request | | Error categories | The HTTP status or queue behavior per category | The Engine is a separate Go module: `github.com/LerianStudio/fetcher/pkg/engine`. Its `go.mod` carries no `require` block at all, and a CI job fails the build if one appears. A second build-enforced test walks every transitive dependency and rejects anything that is neither the Go standard library nor engine-module-local. No HTTP framework, no queue client, no database driver, and no cloud SDK can enter. That constraint is the point. The Engine adds no third-party dependency, so a host application can embed it without a single new entry in its own dependency tree. ### Ports A host wires the Engine through a small set of ports. One is always mandatory. A second is mandatory only when encrypted persistence is on. The rest are optional. | Port | Required | Without it | | ------------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------- | | `ConnectorRegistry` | Yes | The Engine refuses to construct. | | `CredentialProtector` | Conditional | Required when encrypted persistence is on. The Engine refuses to construct rather than store a plaintext credential. | | `ConnectionStore` | No | Connection-backed operations fail. `Limits()`, `AuthorizeConnectionAccess()`, and `CheckActiveExecutions()` remain available. | | `SchemaCache` | No | Every schema call discovers live from the datasource. | | `ResultSink` | No | Store mode is unavailable. Extraction returns the rows inline. | | `ExecutionStore` | No | The Engine tracks no durable execution state. The host owns status. | | `ActiveExecutionChecker` | No | The Engine applies no conflict gate. Connection updates and deletes proceed. | | `Observability` | No | Span hooks disappear. Behavior does not change. | A port supplied as a typed nil counts as absent. The Engine detects that at construction, so a misconfigured host fails at startup instead of panicking on the first call. The module also ships in-memory implementations of the storage-facing ports — the connector registry, the connection store, the schema cache, the result sink, and the execution store. That covers the infrastructure a test needs, so you can exercise the Engine with no MongoDB, no Redis, no RabbitMQ, and no object storage. It ships no `CredentialProtector`, so a test that turns encrypted persistence on has to supply its own. ### Failure classification The Engine returns one of eleven stable categories: `validation`, `not_found`, `unauthorized`, `forbidden`, `limit_exceeded`, `conflict`, `unavailable`, `connect`, `timeout`, `canceled`, and `internal`. A host maps each category onto its own transport. The Manager maps `conflict` to HTTP 409, for example. Errors that cross the Engine boundary carry fixed text. The Engine discards the driver error behind a failure, because that text can embed a DSN, a credential, or driver internals. ## Health and readiness *** Both services mount `/health`, `/readyz`, `/readyz/tenant/{id}`, and `/metrics` before authentication, so Kubernetes and load balancers reach them without a token. * `/health` answers 503 until the startup self-probe succeeds. The kubelet then restarts a pod that could not reach its dependencies at boot. * `/readyz` runs every dependency probe in parallel on each request, with no cache and a per-dependency timeout. The Manager probes MongoDB, RabbitMQ, and Redis. The Worker probes MongoDB, RabbitMQ, and the storage bucket. In multi-tenant mode both services add the Tenant Manager and the multi-tenant Redis. * On `SIGTERM` both services answer `/readyz` with 503 for `READYZ_DRAIN_DELAY_SEC` seconds, 12 by default, before they tear down connections. Kubernetes removes the pod from the Service while it still serves traffic. ## Embedding instead of deploying *** The Manager and the Worker are hosts, not privileged layers. Provide a connector registry and a connection store, and your application can use the Engine's generic query planning, limits, tenant checks, and result contracts in process. Worker-specific compatibility paths and post-processing remain host responsibilities. It needs no queue, no object storage, and no separate deployment. Lerian products embed the Engine exactly this way. Read [Core concepts](/en/fetcher/fetcher-core-concepts) for the model the Engine implements. ## Next steps *** Register, test, update, and delete a datasource connection. The environment variables that shape a Fetcher deployment. # Fetcher configuration Source: https://docs.lerian.studio/en/fetcher/fetcher-configuration The environment variables that shape a Fetcher deployment — encryption, MongoDB, RabbitMQ, object storage, multi-tenancy, telemetry, and internal datasources. Fetcher configures itself entirely from the environment. Each service reads its own set. The Manager and the Worker share most variables, and each one owns a few of its own. For local work, `make set-env` copies each component's `.env.example` to `.env`. In production, set the variables through your orchestrator. **Both services need the same `APP_ENC_KEY`.** The Worker uses it to decrypt datasource credentials and to check the signature on every message the Manager sends. A missing or short key stops the process at startup. The log reads `master key too short: got 0 bytes, minimum 32 required`, and the service never binds a port. ## Both services *** ### Application | Variable | Description | Default | Required | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `APP_ENC_KEY` | Base64-encoded 32-byte master key. Generate it with `make generate-master-key`. Fetcher derives four independent keys from it. | — | Yes | | `APP_ENC_KEY_VERSION` | Key version recorded against every credential Fetcher encrypts. Increment it when you change the master key. | `1` | No | | `ENV_NAME` | Environment label. It also selects the logger profile: `production`, `staging`, `uat`, `development`, or `local`. An unset or unknown value selects the `local` profile. The shipped example sets `development`. | — | No | | `LOG_LEVEL` | Log verbosity. Unset, it follows the environment profile: `debug` for `local` and `development`, `info` for the rest. | — | No | | `VERSION` | Version string reported by `/version`. The shipped example sets `v1.0.0`. | `0.0.0` | No | | `DEPLOYMENT_MODE` | `saas`, `byoc`, or `local`. Tags the `/readyz` response and drives SaaS TLS enforcement. `local` also relaxes license enforcement. | `local` | No | | `ALLOW_INSECURE_TLS` | Permits plaintext connections to MongoDB, Redis, PostgreSQL, and RabbitMQ. Leave it unset in production. | unset | No | | `READYZ_DRAIN_DELAY_SEC` | Drain window after `SIGTERM`, in seconds. Minimum `1`. | `12` | No | ### MongoDB MongoDB holds Fetcher's own metadata: connection records and job records. | Variable | Description | Default | Required | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `MONGO_URI` | Connection scheme: `mongodb` or `mongodb+srv`. Fetcher composes the connection URI from it. The shipped example sets `mongodb`. | — | Yes | | `MONGO_HOST` | MongoDB host. | — | Yes | | `MONGO_PORT` | MongoDB port. | — | Yes | | `MONGO_NAME` | MongoDB database name. Repository access resolves this name in lower case. | — | Yes | | `MONGO_USER` | Database user. | — | Yes | | `MONGO_PASSWORD` | Database password. Fetcher URL-escapes it. | — | Yes | | `MONGO_PARAMETERS` | Extra query-string parameters for the URI. | — | No | | `MONGO_TLS_CA_CERT` | Base64-encoded PEM CA certificate for TLS. | — | No | ### RabbitMQ | Variable | Description | Default | Required | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `RABBITMQ_URI` | AMQP scheme. Fetcher composes the broker URL from it. The shipped example sets `amqp`. | — | Yes | | `RABBITMQ_HOST` | Broker host. | — | Yes | | `RABBITMQ_PORT_AMQP` | AMQP port. | — | Yes | | `RABBITMQ_PORT_HOST` | Management port. | — | No | | `RABBITMQ_DEFAULT_USER` | Broker user. | — | Yes | | `RABBITMQ_DEFAULT_PASS` | Broker password. | — | Yes | | `RABBITMQ_HEALTH_CHECK_URL` | Management URL the readiness probe calls. The shipped example builds it from `RABBITMQ_HOST` and `RABBITMQ_PORT_HOST`. | — | No | | `RABBITMQ_FETCHER_WORK_QUEUE` | Queue that carries extraction jobs from the Manager to the Worker. Both services must name the same queue. The shipped example uses `fetcher.extract-external-data.queue`. | — | Yes | | `RABBITMQ_TLS` | Enables TLS to the broker. | `false` | No | ### Multi-tenancy | Variable | Description | Default | Required | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------ | | `MULTI_TENANT_ENABLED` | Turns on database-per-tenant isolation. It also activates the host-safety guard on tenant-supplied datasource hosts. | `false` | No | | `MULTI_TENANT_URL` | Tenant Manager service URL. | — | With multi-tenancy | | `MULTI_TENANT_SERVICE_API_KEY` | Default API key for Tenant Manager lookups. | — | With multi-tenancy | | `MULTI_TENANT_SERVICE_API_KEY_` | Per-service key. The suffix is the service name, upper-cased, with hyphens turned into underscores (`plugin-crm` becomes `PLUGIN_CRM`). It falls back to the default key. | — | No | | `MULTI_TENANT_REDIS_HOST` | Redis host for tenant Pub/Sub. | — | With multi-tenancy | | `MULTI_TENANT_REDIS_PORT` | Tenant Pub/Sub Redis port. | `6379` | No | | `MULTI_TENANT_REDIS_PASSWORD` | Tenant Pub/Sub Redis password. | — | No | | `MULTI_TENANT_REDIS_TLS` | TLS for tenant Pub/Sub Redis. The client trusts the runtime trust store, so install a private CA there. | `false` | No | | `MULTI_TENANT_MAX_TENANT_POOLS` | Ceiling on concurrent per-tenant connection pools. | `100` | No | | `MULTI_TENANT_IDLE_TIMEOUT_SEC` | Idle timeout before a tenant pool is released. | `300` | No | | `MULTI_TENANT_CACHE_TTL_SEC` | TTL of the cached tenant configuration. The shipped example sets `120`. | — | No | | `MULTI_TENANT_TIMEOUT` | Timeout for Tenant Manager calls, in seconds. | `30` | No | | `MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD` | Consecutive failures before the tenant circuit breaker opens. | `5` | No | | `MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC` | How long the breaker stays open before a reset attempt. | `30` | No | | `MULTI_TENANT_ALLOW_INSECURE_HTTP` | Permits plaintext HTTP to the Tenant Manager. | `false` | No | **Startup stops in multi-tenant mode.** `MULTI_TENANT_ENABLED=true` requires `MULTI_TENANT_URL`, `MULTI_TENANT_SERVICE_API_KEY`, and `MULTI_TENANT_REDIS_HOST`. A missing value aborts startup on both services, and the error names the variable. Two `MULTI_TENANT_SERVICE_API_KEY_` variables that normalize to the same token also abort startup, and the error names the token. One service's credential never silently overwrites another's. ### Telemetry | Variable | Description | Default | Required | | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------- | | `ENABLE_TELEMETRY` | Turns on OpenTelemetry traces and metrics. | `false` | No | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint. | — | With telemetry | | `OTEL_INSECURE_EXPORTER` | Allows a plaintext exporter connection. | `false` | No | | `OTEL_RESOURCE_SERVICE_NAME` | Service name in telemetry. The shipped examples set `fetcher` for the Manager and `fetcher-worker` for the Worker. | — | No | | `OTEL_LIBRARY_NAME` | Instrumentation library name. The shipped example sets `github.com/LerianStudio/fetcher/v2`. | — | No | | `OTEL_RESOURCE_SERVICE_VERSION` | Service version attribute. `/readyz` reports this value, and reports `unknown` when it is unset. The shipped example points it at `VERSION`. | — | No | | `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT` | Deployment environment attribute. The shipped example points it at `ENV_NAME`. | — | No | ## Manager only *** | Variable | Description | Default | Required | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --------- | | `SERVER_ADDRESS` | Listen address for the HTTP API. The same listener serves `/health`, `/readyz`, `/metrics`, and `/version`. The shipped example builds it from `SERVER_PORT`, which it sets to `4006`. | — | Yes | | `SWAGGER_ENABLED` | Serves the API reference at `/swagger/docs` and the OpenAPI 3.1 contract at `/swagger/openapi.json` and `/swagger/openapi.yaml`. | `false` | No | | `PLUGIN_AUTH_ENABLED` | Turns on Access Manager authentication. Multi-tenant mode requires it. | `false` | No | | `PLUGIN_AUTH_ADDRESS` | Access Manager address. | — | With auth | | `SCHEMA_CACHE_TTL_SECONDS` | TTL for a cached datasource schema. An empty or invalid value falls back to 5 minutes. | `300` | No | | `REDIS_HOST` | Valkey or Redis host. It backs the schema cache and the rate limiter on the connection test. | — | No | | `REDIS_PORT` | Redis port. | — | No | | `REDIS_PASSWORD` | Redis password. | — | No | | `REDIS_DB` | Redis database index. | `0` | No | | `REDIS_TLS` | Enables TLS to Redis. | `false` | No | | `REDIS_CA_CERT` | Base64-encoded PEM CA used when `REDIS_TLS=true`. | — | No | | `MAX_PAGINATION_LIMIT` | Ceiling on the `limit` query parameter of every list operation. | `100` | No | | `MAX_PAGINATION_MONTH_DATE_RANGE` | Ceiling on the creation-date window of every list operation, in months. | `1` | No | **The router refuses to build on two security mismatches.** It reports `tenant middleware requires effective authentication` when multi-tenant mode runs with authentication off. It reports `auth middleware is enabled but its address is empty` when `PLUGIN_AUTH_ENABLED=true` and `PLUGIN_AUTH_ADDRESS` is blank. In both cases the Manager does not start. ## Worker only *** ### Runtime and events | Variable | Description | Default | Required | | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------- | | `HEALTH_PORT` | Port for the Worker's health micro-server. It serves `/health`, `/readyz`, `/readyz/tenant/:id`, and `/metrics`. The Worker has no other HTTP server. | `4007` | No | | `RABBITMQ_NUMBERS_OF_WORKERS` | How many jobs the Worker processes in parallel. | `5` | No | | `MONGO_MAX_POOL_SIZE` | Mongo client pool size for the metadata store. The shipped example sets `1000`. | `100` | No | | `STREAMING_ENABLED` | Must be `true`. Job terminal events are a product contract, so the Worker refuses to start without streaming. | `false` | Yes | | `STREAMING_BROKERS` | Bootstrap broker list for the streaming layer, as comma-separated `host:port` pairs. The shipped example sets `localhost:9092`. | — | Yes | | `STREAMING_CLOUDEVENTS_SOURCE` | CloudEvents source stamped on emitted Worker events. The shipped example sets `//lerian.fetcher/worker`. | — | Yes | | `STREAMING_CLIENT_ID` | Streaming client identifier. | — | No | | `STREAMING_COMPRESSION` | Compression codec: `snappy`, `lz4`, `zstd`, `gzip`, or `none`. | `lz4` | No | | `STREAMING_REQUIRED_ACKS` | Producer acknowledgement level: `all`, `leader`, or `none`. | `all` | No | | `RABBITMQ_JOB_EVENTS_EXCHANGE` | Exchange that carries `job.completed` and `job.failed`. The shipped example uses `fetcher.job.events`. | — | Yes | | `MULTI_TENANT_RECONCILE_INTERVAL_SEC` | Interval at which the Worker reconciles per-tenant consumers against the active tenants. | `60` | No | | `ENGINE_MAX_RESULT_BYTES` | Lowers the engine's serialized-result ceiling, as a byte count. Zero or negative keeps the default. | `268435456` | No | | `CRYPTO_ENCRYPT_SECRET_KEY_PLUGIN_CRM` | Encryption key for the CRM compatibility extraction path. | — | No | | `CRYPTO_HASH_SECRET_KEY_PLUGIN_CRM` | Hash key for the CRM compatibility extraction path. | — | No | **The Worker fails closed on event configuration.** `STREAMING_ENABLED` unset or `false` aborts startup with `STREAMING_ENABLED=true is required for mandatory job event notifications`. A blank `RABBITMQ_JOB_EVENTS_EXCHANGE`, an empty `STREAMING_BROKERS`, and an empty `STREAMING_CLOUDEVENTS_SOURCE` each abort startup the same way, as does a `STREAMING_COMPRESSION` or `STREAMING_REQUIRED_ACKS` outside its accepted set. The Worker never falls back to a silent no-op emitter. ### Object storage The Worker writes every stored result to S3-compatible object storage. The endpoint scheme controls TLS, so `http://` turns it off. | Variable | Description | Default | Required | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------- | -------- | | `OBJECT_STORAGE_ENDPOINT` | S3-compatible endpoint, for example `http://minio:9000`. Leave it unset to target AWS S3. | — | No | | `OBJECT_STORAGE_REGION` | S3 region. | `us-east-1` | No | | `OBJECT_STORAGE_BUCKET` | Bucket for stored results. The Worker does not start without it. | — | Yes | | `OBJECT_STORAGE_KEY_PREFIX` | Key prefix applied to stored objects. | — | No | | `OBJECT_STORAGE_ACCESS_KEY_ID` | Static access key ID. Set it with `OBJECT_STORAGE_SECRET_KEY`, or leave both unset to use the ambient AWS credential chain. | — | No | | `OBJECT_STORAGE_SECRET_KEY` | Static secret access key. Set it with `OBJECT_STORAGE_ACCESS_KEY_ID`. Supplying only one of the two aborts startup. | — | No | | `OBJECT_STORAGE_USE_PATH_STYLE` | Path-style addressing. Set it to `true` for MinIO and SeaweedFS. | `false` | No | Set result expiry with a lifecycle policy on the bucket. See [Deployment](/en/fetcher/fetcher-deployment). ### Message signature compatibility | Variable | Description | Default | Required | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `RABBITMQ_ALLOW_LEGACY_BODY_SIGNATURE_FALLBACK` | Compatibility flag for a rolling upgrade. It also accepts body-only signatures on consumed messages. A body-only signature does not bind the message to its tenant, job, exchange, or routing key. It ships off. Turn it on only while a fleet rolls forward, then turn it off. | `false` | No | ## Internal datasources *** Internal datasources are the databases of other Lerian products. Fetcher resolves them without an API-registered connection. You declare each one with a `DATASOURCE_{NAME}_*` group, where `{NAME}` is a prefix you choose. The accepted `_CONFIG_NAME` values are a fixed registry: `midaz_onboarding`, `midaz_transaction`, and `plugin_crm`. Fetcher skips any other name and logs a warning that names both the rejected value and the accepted set. For your own databases, register a connection through the API instead. | Variable | Description | Example | | ------------------------------- | ------------------------------------------------------ | ------------------------- | | `DATASOURCE_{NAME}_CONFIG_NAME` | Registry name of the datasource. Required. | `midaz_onboarding` | | `DATASOURCE_{NAME}_TYPE` | Datasource type, matched case-insensitively. Required. | `POSTGRESQL` | | `DATASOURCE_{NAME}_HOST` | Host. Required. | `db.internal.example.com` | | `DATASOURCE_{NAME}_PORT` | Port. Required. | `5432` | | `DATASOURCE_{NAME}_DATABASE` | Database name. Required. | `onboarding` | | `DATASOURCE_{NAME}_USER` | Username. | `fetcher_ro` | | `DATASOURCE_{NAME}_PASSWORD` | Password. Supply it from your secret manager. | — | | `DATASOURCE_{NAME}_SSLMODE` | TLS mode. This is the TLS control on this path. | `require` | | `DATASOURCE_{NAME}_OPTIONS` | Query-string options. MongoDB only. | `authSource=admin` | **A bad value skips the datasource, loudly.** An invalid `_TYPE`, an invalid `_SSLMODE`, or a missing `_HOST` or `_DATABASE` makes Fetcher skip that datasource and log a warning that names the config name and the offending value. Fetcher never downgrades a TLS setting to reach a database. ## Next steps *** Dependencies, queues, storage, scaling, and startup checks. Master key, derived keys, signing, and host validation. Probes, drain behavior, metrics, and tracing. A first extraction, with or without infrastructure. # Connections Source: https://docs.lerian.studio/en/fetcher/fetcher-connections Register, test, update, and delete a Fetcher connection — the named, credential-encrypted reference to one external database that every job addresses. A **connection** is a stored, named reference to one external database. It carries the datasource type, the host and port, the database name, the credentials, and any TLS settings. Every extraction job and every schema call addresses a datasource by the connection's `configName`, never by its host. Fetcher owns the credential from the moment it arrives. It encrypts the password before storage and never returns it. ## What a connection holds *** | Field | Notes | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `configName` | The identity a job uses. 3 to 100 characters, letters, digits, underscores, and hyphens only. | | `type` | One of `POSTGRESQL`, `MYSQL`, `ORACLE`, `SQL_SERVER`, `MONGODB`, upper-case. Request validation matches those five strings exactly, so any other casing fails with `400`. | | `host` and `port` | The port must fall between 1 and 65535. | | `databaseName` | Required. | | `schema` | Optional. Narrows discovery and extraction to one namespace. | | `userName` and `password` | Both required. The Manager encrypts the password under its credential key and records the key version with the stored record. | | `ssl` | Optional block. When you set it, both a mode and a CA become mandatory. | | `metadata` | Free-form key-value data. You can filter the connection list by it. | The Manager rejects an invalid TLS mode for the declared type. Each database driver accepts a different set of modes, and Fetcher validates the mode against the type before it stores the record. ## Lifecycle *** `POST /v1/management/connections` with the connection body and an `X-Product-Name` header. The header names the product that owns the connection. A successful create answers `201 Created`. `POST /v1/management/connections/{id}/test` opens a real connection to the datasource and reports the round-trip latency. Run it before any job depends on the connection. `GET /v1/management/connections/{id}/schema` returns the tables and fields that Fetcher finds on the live datasource. See [Schema discovery](/en/fetcher/fetcher-schema-discovery). Reference the connection by its `configName` in the `mappedFields` map of an extraction job. `PATCH` applies a partial update and leaves omitted fields untouched. `DELETE` is a soft delete: the record keeps a deletion timestamp. Both operations answer `409 Conflict` while jobs still run against the connection. ## Encrypted credentials *** Fetcher derives four independent keys from the single `APP_ENC_KEY` master key, through HKDF-SHA256. One of those keys protects datasource credentials. The password reaches storage encrypted with AES-256-GCM, and the stored record keeps the `APP_ENC_KEY_VERSION` that protected it. That version is what makes key rotation tractable: a record states which key opens it. The empty key version carries meaning. It marks an **internal** datasource — one an operator declares through `DATASOURCE_{NAME}_*` environment variables rather than through the API. Fetcher builds those connections in memory at startup and keeps no record of them at rest. The operator's own secret manager owns the credential. See [Configuration](/en/fetcher/fetcher-configuration). Both services must run with the same `APP_ENC_KEY`. The Worker needs it to open the credentials the Manager stored and to verify the signature on the message that carried the job. Neither service starts without a valid key of at least 32 bytes. ## Testing a connection *** The test operation does real work. It builds the connector, opens the datasource, runs the driver's own connectivity check, and closes the connector on every path — success or failure. The response carries `latencyMs`, the observed round trip in milliseconds. Use it as a signal about the network path between Fetcher and the datasource, not as a benchmark of the database. The endpoint is rate-limited to **10 tests per minute per connection**. A caller past that budget receives `429 Too Many Requests` with a wait hint. The limit lives in the Manager, not in the Engine, so an embedded host sets its own policy. A failed test tells you that the connection failed. It does not tell you why in driver terms. Fetcher discards the underlying error, because that text can carry a DSN or a credential. ## The 409 on active jobs *** **Fetcher blocks update and delete while jobs run against the connection.** `PATCH /v1/management/connections/{id}` and `DELETE /v1/management/connections/{id}` both answer `409 Conflict` when at least one job still runs against that connection's `configName`. A caller must handle this. Treat it as "not yet", not as "invalid". Wait for the jobs to reach a terminal state and retry, or cancel them first. The rule exists to keep a running extraction consistent with the connection it planned against. A host swap or a credential change mid-extraction would leave a job reading from a datasource nobody asked for. The Engine enforces the gate through an optional port, not through a hard dependency on job storage. The Manager answers the question from its job repository. An embedded host answers it however it tracks work — an in-memory set, a distributed lock, or a flat "no". A host that supplies nothing gets no gate, and mutations proceed. ## Host safety in multi-tenant mode *** With `MULTI_TENANT_ENABLED=true`, Fetcher validates the host of every tenant-supplied connection before it dials. Validation runs at two layers: * At request parsing, a check without DNS rejects a blocked IP literal outright. * In the datasource factory, a resolving check rejects blocked hostnames such as `localhost` and cloud metadata names, then rejects every address the hostname resolves to. Private ranges, loopback, and cloud metadata endpoints are blocked. A rejected host returns `400`. A DNS resolution failure is deliberately **not** a block. Turning "does not resolve" into a rejection would build a reconnaissance oracle and would fail legitimate connections during a transient DNS problem. The driver surfaces its own connect error instead. The guard never applies to internal datasources. An operator who configures a datasource through environment variables has already made that decision. ## Migration operations *** Two operations exist only for connections that predate product scoping: * `GET /v1/management/connections/unassigned` lists connections with no product. * `POST /v1/management/connections/{id}/assign` attaches one to the product in the `X-Product-Name` header. The assignment is one-time and irreversible. A second attempt on an already-assigned connection returns a conflict. ## Next steps *** Read a datasource schema, cache it, and validate a job against it. The Manager, the Worker, and the Engine they both run over. # Fetcher core concepts Source: https://docs.lerian.studio/en/fetcher/fetcher-core-concepts The Fetcher model in one place: connections, datasource types, schemas, extraction jobs, filters, results, hosts, and the tenant scope that separates them. Fetcher has a small model. You register a **connection** to an external database. Fetcher **discovers the schema** behind it. You submit an **extraction job** that names the fields you want. Fetcher gives you back a **result** with an integrity digest over the exact bytes it produced. This page defines each noun. Each section then points to the page that covers it in full. ## The model at a glance *** | Concept | Definition | | --------------- | ----------------------------------------------------------------------------- | | Connection | A stored, named reference to one external database. | | Datasource type | The database engine behind a connection. Fetcher accepts five. | | Schema | The tables and fields that Fetcher finds on a datasource. | | Extraction job | An asynchronous request for named fields from named tables. | | Filter | A per-field condition that limits the rows a job returns. | | Result | The extracted rows, plus the digest that identifies them. | | Engine | The extraction core. It decides what an extraction means. | | Host | The application that runs the Engine. It owns queues, storage, and transport. | | Tenant | The only isolation boundary. Every operation carries one tenant ID. | ## Connections *** A connection holds the datasource type, host, port, database name, credentials, and optional TLS settings for one external database. A short `configName` identifies it inside the tenant, and every job and schema call addresses the datasource by that name. Fetcher encrypts the password with AES-256-GCM before it reaches storage. The stored record also keeps the key version that protected it. A connection never travels back to a caller with its password. Read [Connections](/en/fetcher/fetcher-connections) for the full lifecycle, the connection test, and the rule that blocks an update or a delete while jobs still run. ## Datasource types *** A connection declares one of five types. Send the value upper-case: request validation matches these five strings exactly and rejects any other casing with `400`. * `POSTGRESQL` * `MYSQL` * `ORACLE` * `SQL_SERVER` * `MONGODB` The five behave differently under the hood. PostgreSQL and SQL Server qualify tables outside the default schema. Oracle works in owner namespaces. MongoDB has no declared schema at all, so Fetcher infers one from a document sample. ## Schemas *** A schema snapshot lists the tables of a datasource and the fields of each table. Fetcher builds it directly from the datasource, so you do not maintain a separate catalog. Two things build on a snapshot. Schema **validation** checks a job mapping against it before extraction starts. Schema **caching** keeps a recent snapshot under the tenant and the config name, so repeated work skips the round trip to the database. Read [Schema discovery](/en/fetcher/fetcher-schema-discovery) for live-versus-cached behavior, the per-database differences, and the validation report. ## Extraction jobs *** A job names the fields to extract, per table, per datasource: ```json theme={null} { "dataRequest": { "mappedFields": { "my_postgres": { "accounts": ["id", "email", "created_at"] }, "my_mongo": { "transactions": ["*"] } } } } ``` One job can span several datasources, several tables per datasource, and several schemas. Use `["*"]` to take every field of a table. The Manager accepts a job and answers `202 Accepted`. A repeated request inside a five-minute window answers `200 OK` and returns the job that already exists. A job that failed does not suppress a retry. Engine limits bound generic datasource work. The defaults allow 10 datasources per extraction, 20 tables per datasource, 50 fields per table, and a five-minute deadline. Embedded Engine callers can lower, but never raise, those limits with `ExtractionRequest.Overrides`. The standalone Manager job payload has no limit-override field, and the Worker accepts only a positive `ENGINE_MAX_RESULT_BYTES` override. The `plugin_crm` portion uses the Worker's explicit compatibility path. ## Filters *** A filter narrows the rows of one table. The job payload nests filters four levels deep: datasource, then table, then field, then operator. ```json theme={null} { "dataRequest": { "filters": { "my_postgres": { "transactions": { "status": { "in": ["completed", "pending"] } } } } } } ``` Ten operators exist: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, `nin`, and `like`. Every operator takes a JSON array. Several operators on the same field combine with `AND`. ## Results *** An extraction produces exactly one result shape. In **direct mode** the Engine returns the rows inline as indented JSON and stamps a SHA-256 digest over those bytes. The payload leaves the Engine unencrypted, and the host decides what to do next. In **store mode** the Engine streams the rows to a sink that the host provides, one JSON object per line. It returns a reference instead of the bytes, with a SHA-256 digest over exactly the bytes written. The Engine holds no complete result in memory on this path. Each mode hashes what it emits: direct mode the indented document, store mode the streamed lines. The Engine canonicalizes its planned field and step order before serialization. A digest identifies the exact bytes emitted by that execution; do not treat it as a cross-run equivalence guarantee unless the datasource query order and all host-side processing are controlled. The two modes write different shapes, so compare a digest only against another digest from the same mode. The generic Engine runner stops on its first failing step and does not return a successful direct result. Hosts define their own behavior for compatibility or multi-stage orchestration paths. The standalone Worker drives direct mode. It then signs the plaintext with HMAC-SHA256, encrypts it with AES-GCM, and writes it to S3-compatible object storage. See [Architecture](/en/fetcher/fetcher-architecture). ## Engine and hosts *** The Engine is the part of Fetcher that owns connection lifecycle, schema discovery, query planning, extraction, limits, and tenant safety. It ships as its own Go module with no third-party dependencies, and it talks to the outside world only through ports that a host provides. A **host** supplies those ports and owns everything the Engine refuses to know: HTTP, queues, object storage, authentication, and the job lifecycle. The Manager and the Worker are two such hosts. Your own application can be a third. Read [Architecture](/en/fetcher/fetcher-architecture) for the two services, the ports, and what each side owns. ## Tenants *** Tenant ID is the only isolation boundary in the Engine. There is no organization concept and no product concept below it. Every operation validates the tenant before it touches a connection, a cache entry, or a datasource. Single-tenant mode is the default. Multi-tenant mode gives each tenant its own metadata database, which Fetcher resolves from JWT claims. Without a tenant database in context, the call fails. It never falls back to the shared database. See [Multi-tenancy](/en/multi-tenancy) for the platform-wide model. ## Next steps *** The Manager, the Worker, and the Engine they both run over. Register, test, update, and delete a datasource connection. How Fetcher reads a schema, caches it, and validates a job against it. Run Fetcher locally and execute your first extraction job. # Datasources Source: https://docs.lerian.studio/en/fetcher/fetcher-datasources How PostgreSQL, MySQL, Oracle, SQL Server, and MongoDB differ inside Fetcher — schema namespaces, name matching, type inference, pools, and timeouts. Fetcher reads from five database engines. The job request looks the same for all of them. What differs is how each engine names its tables, how Fetcher discovers a schema, and how values come back. This page covers those differences. Pick your engine and read its section. ## What every engine shares *** Set the engine with the connection `type`, upper-case: `POSTGRESQL`, `MYSQL`, `ORACLE`, `SQL_SERVER`, `MONGODB`. Request validation matches those five strings exactly, so any other casing fails with `400`. These bounds are the same everywhere: | Bound | Value | | --------------------------- | ----------- | | Connection timeout | 5 seconds | | Schema discovery timeout | 30 seconds | | Query timeout, no filters | 60 seconds | | Query timeout, with filters | 120 seconds | The four relational engines share one connection pool profile: 25 open connections, 10 idle connections, a 5-minute connection lifetime, and a 1-minute idle timeout. MongoDB uses a pool of at most 100 and at least 10, with the same 1-minute idle timeout. Field projection also works the same way. A list of field names selects those fields. The single entry `["*"]` selects all of them. ## PostgreSQL *** **Schemas.** PostgreSQL is the fullest multi-schema engine in Fetcher. Discovery reads `information_schema` for the schemas your job names, and falls back to `public` when the job names none. **Table names.** A table outside `public` comes back schema-qualified, such as `accounting.invoices`. A table inside `public` comes back bare, such as `users`. Write the name the same way in `mappedFields`. **Filter keys.** A filter key resolves in three forms: the exact name, the name without its schema prefix, and the name with `public` added. `transactions` and `public.transactions` therefore reach the same table. **JSON columns.** The driver returns `JSON` and `JSONB` columns as raw bytes. Fetcher tries to read each value as a JSON object, then as a JSON array, then as a JSON string. The parsed value goes into the result. A value that matches none of the three stays raw, and Fetcher logs a warning. ## MySQL *** **No schema list.** MySQL is the one relational engine with no schema dimension in Fetcher. The connected database is the namespace. Discovery reads `information_schema` for tables, columns, and primary keys of that database. **Table names.** Write table names bare. A schema prefix has nothing to resolve against. **Filter keys.** A filter key must match the table name exactly as you wrote it under `mappedFields`. **JSON columns.** Fetcher applies the same three-step JSON read to byte values that PostgreSQL gets: object, then array, then string. ## Oracle *** **Owners, not schemas.** Oracle calls the namespace an owner, and Fetcher treats the owner as the schema. When your job names owners, discovery reads `all_tables`, `all_tab_columns`, and `all_cons_columns` restricted to those owners. When it names none, discovery falls back to `user_tables` and `user_cons_columns`, which cover the connected user only. **The default owner** is the user the connection authenticates as. **Table names.** A table owned by the connected user comes back bare. Any other table comes back owner-qualified, as `OWNER.TABLE`. Fetcher compares owner names without case sensitivity, and upper-cases the owner before it queries the Oracle catalog. Fetcher also upper-cases table and column names in the schema it reports back, and extracted rows use those upper-case column names as keys. You can write a job in upper case or lower case, because Fetcher upper-cases the names in your request the same way. Read the results by the upper-case key. **Filter keys.** Oracle matches a filter key exactly. There is no prefix fallback. **JSON columns.** Byte values pass through the same three-step JSON read. ## SQL Server *** **Schemas.** SQL Server is multi-schema, with `dbo` as the default. Discovery reads `information_schema.tables` for the schemas your job names, and restricts to `dbo` when the job names none. **Table names.** A table outside `dbo` comes back schema-qualified, such as `accounting.invoices`. A table inside `dbo` comes back bare. This is the same rule PostgreSQL applies to `public`. **Filter keys.** SQL Server resolves a filter key in the same three forms PostgreSQL does, with `dbo` as the default prefix. **JSON columns.** Byte values pass through the same three-step JSON read. ## MongoDB *** **Collections, not tables.** MongoDB has no schema namespace. Write collection names bare. A filter key must match the collection name exactly. **Inferred schema.** Documents carry no declared schema, so Fetcher builds one in two passes over each collection: 1. An aggregation pipeline collects the full set of field names. 2. A sample of up to 50 documents infers the type of each field. A field the sample never sees keeps the type `unknown`. **Sampling.** The field-name pass adapts to collection size. Up to 10,000 documents, it aggregates over at most 1,000 documents. Above 10,000 documents it switches to a `$sample` stage, at a size that targets 95% confidence with a 5% margin of error: | Documents in the collection | Sample size | | --------------------------- | ----------- | | Up to 1,000 | all of them | | 1,001 to 10,000 | 1,000 | | 10,001 to 100,000 | 2,000 | | 100,001 to 1,000,000 | 5,000 | | Over 1,000,000 | 10,000 | An inferred schema is therefore a strong sample, not a guarantee. A field that exists in a handful of documents of a very large collection can be missed. If the aggregation fails on a collection, Fetcher logs a warning and falls back to sampling instead of failing discovery. **Filters.** MongoDB takes the same ten operators, translated into query operators. A `like` pattern becomes a case-insensitive regular expression. MongoDB is also the one engine that does not run the UUID check on identifier-like field names. See [Filters](/en/fetcher/fetcher-filters). **Connection options.** An internal datasource passes driver options such as `authSource` and `directConnection` through `DATASOURCE_{NAME}_OPTIONS`. ## Next steps *** How a job names datasources, tables, schemas, and fields. The ten filter operators and the value shape each one takes. # Fetcher deployment Source: https://docs.lerian.studio/en/fetcher/fetcher-deployment Deploy the Fetcher Manager and Worker: MongoDB, RabbitMQ with a dead-letter queue, S3-compatible storage, Valkey or Redis, worker concurrency, autoscaling, and the startup checks that stop a misconfigured service. A standalone Fetcher deployment is two services and four dependencies. This page covers what each dependency does, how to size the Worker, and what stops a service at startup. If you only need extraction inside one Go application, you deploy none of this. See [Embedding the Engine](/en/fetcher/fetcher-embedding-the-engine). ## What you deploy *** | Component | Role | Scale on | | ------------------- | -------------------------------------------------------------- | ---------------------------- | | **Manager** | HTTP API for connections and jobs. Publishes work to RabbitMQ. | Request rate. | | **Worker** | Queue consumer. Runs extractions and writes results. | Queue depth. | | **MongoDB** | Connection records and job records. | Metadata volume. | | **RabbitMQ** | Work queue, dead-letter queue, and the job events exchange. | Job rate. | | **Object storage** | Encrypted extraction results. S3-compatible. | Result volume and retention. | | **Valkey or Redis** | Manager schema cache and connection-test rate limiter. | Manager only. | ## MongoDB *** Both services connect to the same MongoDB deployment. The Manager writes connection and job records, and the Worker updates job state. In multi-tenant mode, each tenant gets its own database, resolved from the JWT claims on the request. That path fails closed: a request that carries a tenant identity with no tenant database returns an error instead of touching the shared database. ## RabbitMQ *** Fetcher uses three exchanges and four queues. The bundled local stack declares them for you. In your own environment, declare them before the services start. | Object | Type | Purpose | | ------------------------------------------------------------ | ------------------------- | ----------------------------------------------------------- | | `fetcher.extract-external-data.queue` | queue | Extraction jobs, from Manager to Worker. | | `fetcher.extract-external-data.exchange` | direct exchange | Bound to the work queue with routing key `fetcher.job.key`. | | `fetcher.dlx` and `fetcher.dlq` | direct exchange and queue | Dead letters from the work queue. | | `fetcher.job.events` | topic exchange | Terminal job events. | | `fetcher.job.completed.queue` and `fetcher.job.failed.queue` | queues | Bound to `job.completed` and `job.failed`. | ### The dead-letter queue The work queue carries `x-dead-letter-exchange: fetcher.dlx` and routing key `fetcher.dlq.key`. A message the Worker rejects lands in `fetcher.dlq`. That queue holds messages for 7 days and caps at 10,000 messages in the bundled configuration. Watch the dead-letter queue. A message there is a job the Worker could not process. ### Job events The Worker publishes `job.completed` and `job.failed` for every terminal job. Both routes are required, so a delivery failure is not silent. Consumers bind to the two queues above, or bind their own. ## Object storage *** The Worker writes each stored result to S3-compatible object storage through the AWS S3 protocol. AWS S3, MinIO, and SeaweedFS all work. Reach SeaweedFS through its S3 gateway, not through its native filer API. Two settings decide the connection posture: * The scheme on `OBJECT_STORAGE_ENDPOINT` controls TLS. Use `https://` outside local development. * `OBJECT_STORAGE_USE_PATH_STYLE=true` is what MinIO and SeaweedFS expect. ### Result retention Set retention on the bucket, with the object store's own lifecycle policy. An S3 lifecycle rule on `OBJECT_STORAGE_KEY_PREFIX` expires results after the window your retention policy requires. Give Fetcher its own bucket or its own prefix, so the rule applies to extraction results and to nothing else. Fetcher encrypts every stored result before it reaches the bucket. See [Security](/en/fetcher/fetcher-security). ## Valkey or Redis *** The Manager uses Valkey or Redis for two things. It caches discovered datasource schemas, and it rate-limits the connection test. The `SCHEMA_CACHE_TTL_SECONDS` variable sets the cache lifetime, and it defaults to 5 minutes. The schema cache falls back to process memory when Redis is unreachable. The Manager keeps serving. Its readiness probe reports the Redis state separately, so the fallback stays visible. The Worker does not use the schema cache. ## Worker concurrency and autoscaling *** `RABBITMQ_NUMBERS_OF_WORKERS` sets how many jobs one Worker process runs in parallel. It defaults to 5. Size it against your datasources, not against the Worker. Each parallel job opens connections to the databases it reads. Every relational datasource pool caps at 25 open and 10 idle connections, and MongoDB caps at 100. Within one extraction, the engine runs at most 4 datasource steps at a time. For horizontal scale, add Worker replicas and drive them from the depth of the extraction work queue. The bundled local stack runs the KEDA operator (2.16.0) against that queue, so the same trigger carries over to Kubernetes. Generic datasource extraction carries fixed Engine limits: 10 datasources, 20 tables per datasource, and 50 fields per table. A run also stops at a 5-minute timeout or a 256 MiB serialized result. Embedded Engine callers can lower, but never raise, those limits with `ExtractionRequest.Overrides`. The standalone Manager job payload has no limit-override field; the Worker accepts only a positive `ENGINE_MAX_RESULT_BYTES` override, and `plugin_crm` uses its explicit compatibility path. ## Deployment mode and TLS *** `DEPLOYMENT_MODE` declares the flavor of the deployment: `saas`, `byoc`, or `local`. It tags the `/readyz` response, and in `saas` it enforces TLS. **SaaS mode requires TLS on every platform dependency.** Set `DEPLOYMENT_MODE=saas` and a plaintext MongoDB, Redis, RabbitMQ, S3, or Tenant Manager URL stops the service. It stops before it opens any connection, and the error names the rule. Your own tenants' datasources stay out of scope for this check. Leave `ALLOW_INSECURE_TLS` unset in production. It permits plaintext connections to MongoDB, Redis, PostgreSQL, and RabbitMQ, and the bundled local environment is the only place for it. ## Startup checks *** Fetcher fails closed. Each check below stops the process before it serves traffic. | Trigger | What the operator sees | | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `APP_ENC_KEY` missing, not base64, or under 32 bytes | The process exits at startup and never binds a port. The log reads `master key too short: got 0 bytes, minimum 32 required`. | | `STREAMING_ENABLED` not `true` on the Worker | Startup aborts with `STREAMING_ENABLED=true is required for mandatory job event notifications`. | | `RABBITMQ_JOB_EVENTS_EXCHANGE` blank on the Worker | Startup aborts with `RABBITMQ_JOB_EVENTS_EXCHANGE is required for mandatory job event notifications`. | | `OBJECT_STORAGE_BUCKET` missing on the Worker | The storage repository does not build, and the Worker does not start. | | `MULTI_TENANT_ENABLED=true` without `MULTI_TENANT_SERVICE_API_KEY` | Startup aborts on both services with an error naming the variable. | | Two per-service tenant keys that normalize to the same token | Startup aborts with an error naming the colliding token. | | Multi-tenant mode with authentication off | Manager startup fails with `PLUGIN_AUTH_ENABLED must be true when MULTI_TENANT_ENABLED=true`. | | `DEPLOYMENT_MODE=saas` with a plaintext platform dependency | Startup aborts before any connection opens. | **A dependency that is down at boot does not produce a silently broken pod.** `/health` returns 503 until the startup self-probe succeeds. The kubelet then restarts the pod instead of sending it traffic. ## Rolling updates *** On `SIGTERM`, both services enter a drain. `/readyz` answers 503 for `READYZ_DRAIN_DELAY_SEC` seconds, which defaults to 12, before connections tear down. Kubernetes removes the pod from Service endpoints while it still serves in-flight work. Set your termination grace period above the drain window. See [Observability](/en/fetcher/fetcher-observability) for what the probes report during a drain. ## Next steps *** Every environment variable, per component. Keys, signing, encryption at rest, and host validation. Probes, drain behavior, metrics, and tracing. Job lifecycle, terminal states, and events. # Embedding the Fetcher Engine Source: https://docs.lerian.studio/en/fetcher/fetcher-embedding-the-engine Import the Fetcher Engine Go module, provide the ports it needs, and construct it with engine.New — with a self-contained example that runs without any infrastructure. Embedding the [Fetcher Engine](/en/fetcher/fetcher-engine-overview) takes three steps: **import it, provide the ports it needs, construct it with `engine.New`.** No infrastructure ships with the import. You wire only the parts your host application actually uses. ## 1. Install *** ```bash theme={null} go get github.com/LerianStudio/fetcher/pkg/engine ``` The Engine is a distinct Go module from the Fetcher services (`github.com/LerianStudio/fetcher/v2`). It carries zero third-party dependencies and has its own version line with path-prefixed tags (`pkg/engine/vX.Y.Z`). The import gives your module graph none of the service dependencies. ## 2. Provide the ports *** The Engine depends only on host-provided interfaces. One port is always required. A second is required only when encrypted persistence is on. The rest are opt-in, and the Engine degrades gracefully without them. | Port | Required? | Without it | | ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `ConnectorRegistry` | **Always** | `engine.New` fails and no Engine exists | | `CredentialProtector` | Only with `WithEncryptedPersistence(true)` | Construction fails when encrypted persistence is on; when it is off, the Engine passes no supplied password to the connection store | | `ConnectionStore` | Optional | Connection-backed operations fail; `Limits()`, `AuthorizeConnectionAccess()`, and `CheckActiveExecutions()` remain available | | `ResultSink` | Optional | Store mode is unavailable, and extraction runs in direct mode | | `SchemaCache` | Optional | Schema discovery always hits the live datasource | | `ExecutionStore` | Optional | No durable execution-state tracking | | `ActiveExecutionChecker` | Optional | No conflict gating on connection updates and deletes | | `Observability` | Optional | Tracing hooks become no-ops | The [port reference](/en/fetcher/fetcher-engine-ports) documents each contract and each degradation in full. Read it before you decide which ports to skip — "optional" does not mean "harmless". For tests and first runs, the **`pkg/engine/memory`** harness provides in-memory implementations of the storage-facing ports: the connector registry, the connection store, the schema cache, the result sink, and the execution store. You need no MongoDB, Redis, RabbitMQ, or object storage to exercise the Engine. The harness ships no `CredentialProtector`, so a test that turns encrypted persistence on must supply one. ## 3. Construct, plan, execute *** This example is self-contained. It uses the in-memory harness, so it runs with zero infrastructure. ```go theme={null} package main import ( "context" "fmt" "log" "github.com/LerianStudio/fetcher/pkg/engine" "github.com/LerianStudio/fetcher/pkg/engine/memory" ) func main() { ctx := context.Background() // Provide ports. In production these are your real adapters (see pkg/enginecompat); // here the in-memory harness stands in so the example runs with zero infrastructure. store := memory.NewConnectionStore() registry := memory.NewConnectorRegistry() // Construct the Engine. WithConnectorRegistry is the only required option. eng, err := engine.New( engine.WithConnectorRegistry(registry), engine.WithConnectionStore(store), ) if err != nil { log.Fatal(err) } // Every operation is scoped to a tenant — the sole isolation dimension. tenant, err := engine.NewTenantContext("tenant-123") if err != nil { log.Fatal(err) } // Register a connector for the datasource type, then persist a connection. conn := memory.NewTemplateConnector(memory.ConnectorBehavior{ Schema: engine.SchemaSnapshot{ ConfigName: "pg-main", Tables: []engine.TableSnapshot{{Name: "public.users", Fields: []string{"id", "email"}}}, }, Rows: map[string][]map[string]any{ "public.users": {{"id": 1, "email": "a@example.com"}}, }, }) registry.Register("postgres", memory.NewConnectorFactory(conn)) if _, err = eng.CreateConnection(ctx, tenant, engine.NewConnectionInput(engine.ConnectionInputParams{ ConfigName: "pg-main", Type: "postgres", Host: "localhost", Port: 5432, })); err != nil { log.Fatal(err) } // Plan validates the request against a cache-first resolved schema and enforces limits. plan, err := eng.PlanExtraction(ctx, tenant, engine.ExtractionRequest{ MappedFields: map[string]engine.FieldSelection{ "pg-main": {"public.users": {"id", "email"}}, }, }) if err != nil { log.Fatal(err) } // Execute. With no ResultSink wired, the Engine runs in Direct mode and returns // inline JSON bytes plus a SHA-256 integrity digest. result, err := eng.ExecuteExtraction(ctx, plan) if err != nil { log.Fatal(err) } fmt.Printf("rows=%d bytes=%d\n", result.Direct.RowCount, len(result.Direct.Data)) } ``` ### What the example shows * **One required option.** `WithConnectorRegistry` is the only port `engine.New` insists on. The connection store here is a convenience, but skip it and connection-backed operations fail; `Limits()`, `AuthorizeConnectionAccess()`, and `CheckActiveExecutions()` remain available. * **Construction-time validation.** `engine.New` rejects a port passed as a typed nil, not only a literal nil. A misconfigured host fails at construction instead of panicking at first use. * **Tenant scope on every call.** `NewTenantContext` builds the only isolation dimension the Engine knows. It carries a tenant ID and an optional request ID — no organization and no product. * **Plan, then execute.** `PlanExtraction` validates the request against a cache-first resolved schema and applies the limits. Use `DiscoverSchemaFresh` when you require an always-live schema snapshot. `ExecuteExtraction` reads the plan. * **Mode by composition.** The example wires no `ResultSink`, so the Engine picks direct mode and returns the bytes inline with a SHA-256 digest. Add a sink and the same code returns a storage reference instead. See [Direct mode and Store mode](/en/fetcher/fetcher-engine-overview#direct-mode-and-store-mode). ## Moving to production *** Swap the memory harness for real adapters. Fetcher's own services are the reference implementation, and their source is public. They bridge the Engine ports to real infrastructure under `pkg/enginecompat`: | Adapter package | Bridges | | ----------------------------------- | ------------------------------------------------------------------------------------- | | `pkg/enginecompat/connectioncompat` | Connection store, connection access, tenant context, and the active-execution checker | | `pkg/enginecompat/schemacompat` | Schema cache, schema connector, and snapshot building | | `pkg/enginecompat/datasource` | The datasource drivers behind the connector contract | | `pkg/enginecompat/tablenorm` | Qualified table-name normalization | Read how the services wire them: * **Connection CRUD** — `components/manager/internal/bootstrap/connection_engine.go` * **Schema discovery and caching** — `components/manager/internal/bootstrap/schema_engine.go` * **Plan and execute extraction** — `components/worker/internal/bootstrap/extraction_engine.go` Two rules carry over from the harness to production: 1. **Your adapters own tenant scope.** The Engine passes a tenant context to every port call and enforces the boundary at its own edge. A store that ignores the tenant ID leaks data across tenants, and the Engine cannot catch that for you. 2. **Your adapters own secrets.** With encrypted persistence enabled and a supplied password, the Engine calls `Protect` and records only the returned key version as metadata. `Reveal` is available to host adapters; the Engine core does not invoke it. Key derivation, rotation, and storage stay in your host. ## Next steps *** Every port, its contract, and the behavior you get without it. The three-layer model, the import boundary, and the two result modes. # Fetcher Engine overview Source: https://docs.lerian.studio/en/fetcher/fetcher-engine-overview The Fetcher Engine is an importable Go module that runs data extraction in-process: the three-layer model, the enforced import boundary, and Direct versus Store mode. The **Fetcher Engine** is the extraction core of Fetcher, packaged as a Go module you can import. Host applications such as Matcher and Reporter run the Engine in-process instead of operating a separate Fetcher deployment. The standalone [Manager and Worker](/en/fetcher/what-is-fetcher) are themselves hosts over the same Engine. The Engine owns the rules of extraction: connection lifecycle, schema discovery and validation, query planning, extraction execution, result and error contracts, limits, and tenant safety. It owns no infrastructure at all. The Engine is a **distinct Go module** from the Fetcher services. Its module path is `github.com/LerianStudio/fetcher/pkg/engine`, and the services module is `github.com/LerianStudio/fetcher/v2`. The Engine has its own version line with path-prefixed tags (`pkg/engine/vX.Y.Z`). An import of the Engine inherits none of the service dependencies. Fetcher is source-available under the Elastic License 2.0, and the Engine carries the same license. You read the extraction rules you embed. ## The three-layer model *** | Layer | Package | Owns | | -------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | **Engine core** | `pkg/engine` | The *rules* of extraction. It depends only on host-provided port interfaces, never on infrastructure. | | **Compatibility adapters** | `pkg/enginecompat/*` | Bridges between the Engine ports and Fetcher's real infrastructure — MongoDB, Redis, and the datasource drivers. | | **Host application** | Manager, Worker, or your own service | The operational shell: authentication, license enforcement, HTTP routes, queues, state stores, storage, telemetry, and process lifecycle. | The guiding principle: **the Engine owns what makes Fetcher *Fetcher*, and host applications own *how* Fetcher runs.** Every product that embeds the Engine therefore shares one canonical owner of datasource and extraction behavior. A rule change in the core reaches every host at the next module bump, and no host re-implements it. ## The import boundary *** The Engine module declares **zero third-party dependencies**. Its `go.mod` has no `require` block at all, and a build-enforced test keeps it that way. Two guards run inside the module on every `go test ./...`, and the module CI workflow runs them with the Go workspace turned off: * **The allowlist.** Every transitive dependency of `pkg/engine` must be either a Go standard-library package or a package local to the Engine module. Any other import family fails the build, even a family that no denylist names. * **A named denylist.** Enumerated classes fail on top of the allowlist. They cover HTTP frameworks, message brokers, database drivers, object-storage SDKs, tenant-runtime middleware, the auth and license libraries, and standard-library shells such as `database/sql`, `net/http`, and `os/exec`. A separate CI step greps the Engine `go.mod` for a `require` line and fails the job when it finds one. ### Why this matters when you embed * **No dependency conflicts.** The Engine cannot pull a driver, a broker client, or an HTTP framework into your module graph. Your host keeps full control of its own versions. * **No hidden I/O.** The core cannot open a socket, a file, or a database on its own. Every byte in and out crosses a port you supplied. * **A stable substitution seam.** Run the whole Engine against the in-memory harness in tests. Swap real adapters in production, with no change to the calling code. ## Direct mode and Store mode *** An extraction plan carries a mode with three values: `direct`, `store`, and the zero value `auto`. The Engine resolves `auto` from the ports you wired. With a `ResultSink` configured it picks store mode, and without one it picks direct mode. So the mode is a consequence of your composition, not a separate switch. An explicit `store` request with no sink fails up front as a validation error, before the Engine touches any datasource. The two modes return different shapes. Exactly one arm of the result is non-nil, and the JSON omits the unused arm entirely. ### Direct mode The Engine runs the plan steps and merges the rows into one map. The map keys are the datasource config name and then the qualified table. The Engine serializes that map once as indented JSON and returns the bytes inline. ```json theme={null} { "pg-main": { "public.users": [ { "email": "a@example.com", "id": 1 } ] } } ``` The inline result carries this metadata: | Field | Value in direct mode | | --------------- | ------------------------------------------------------ | | `data` | The serialized payload above | | `format` | `json` | | `rowCount` | Total rows across all tables | | `plaintextSize` | Byte size of the payload | | `integrity` | Algorithm `SHA-256` plus the hex digest of the payload | | `protection` | `encrypted: false`, applied by `engine` | Direct-mode serialization order is canonical. The serializer sorts the map keys, whatever order the parallel steps finished in. The digest identifies the exact bytes of that execution; two runs give the same digest only when the datasources return rows in a stable order and any host-side processing is controlled. Your host owns everything after that. The Fetcher Worker, for example, signs the plaintext with HMAC-SHA256 and encrypts it before it stores the bytes. ### Store mode The Engine opens a stream on your `ResultSink` and writes the result incrementally, in constant memory. It never holds the whole result. One writer drains the extraction goroutines strictly by ascending plan-step order, and the integrity digest covers exactly the bytes written. The wire shape is NDJSON — one JSON object per line, newline-terminated, with no enclosing array: ```json theme={null} {"config":"pg-main","table":"public.users","row":{"id":1,"email":"a@example.com"}} ``` The call returns a reference instead of bytes: | Field | Meaning | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | The **logical** location in your storage. The reference exposes no physical backend type — your adapter resolves the path. | | `format` | Output format of the persisted bytes | | `rowCount` | Total rows in the persisted result | | `sizeBytes` | Serialized size written | | `integrity` and `protection` | What your sink reported. When the sink reports no integrity, the Engine stamps the SHA-256 digest it computed over the streamed bytes. The Engine validates only that `protection.appliedBy` is one of `engine`, `adapter`, or `host`. | On an abort — a write error, an exceeded size limit, or a canceled context — the Engine abandons the writer and never calls `Close`. A partial result therefore never becomes a returned reference. Treat an unclosed writer as a discarded write. ## Failure and result contracts *** * **Fail-fast across datasources.** The first failing step stops the run. The Engine never returns a partial result. * **Redacted errors.** A driver error can embed a DSN, a credential, or driver internals, so the Engine discards it and returns a fixed message instead. * **Eleven error categories.** `validation`, `not_found`, `unauthorized`, `forbidden`, `limit_exceeded`, `conflict`, `unavailable`, `connect`, `timeout`, `canceled`, and `internal`. Your host maps them to its own transport codes. `connect` stays distinct from `unavailable`, and `timeout` stays distinct from `canceled`. * **Five execution statuses.** `pending`, `running`, `completed`, `failed`, and `canceled`. The last three are terminal. A host cancellation records `canceled`, and a deadline overrun records `failed`. * **Closed connectors.** Every connector the Engine opens closes again, on the success path and on every failure path. ## Limits and tenant scope *** The Engine never runs unbounded. When you supply no limits, it applies these defaults: | Limit | Default | | --------------------------- | --------- | | Datasources per extraction | 10 | | Tables per datasource | 20 | | Fields per table | 50 | | Parallel datasource workers | 4 | | Extraction timeout | 5 minutes | | Serialized result size | 256 MiB | An embedded caller can **lower** any limit with `ExtractionRequest.Overrides` and never raise it. An override above the default fails with a validation error that names the breached field. A zero or negative override keeps the default. The standalone Manager job payload has no limit-override field, and the Worker applies only a positive `ENGINE_MAX_RESULT_BYTES` override. In direct mode, the Engine enforces the result-size ceiling with a cheap per-step lower bound and an authoritative check on the final indented payload before returning it. In store mode, it enforces the NDJSON byte limit incrementally while writing; on overflow it abandons the writer without `Close`, so no finalized result reference is returned and sinks must discard unclosed partial writes. Tenant scope is equally narrow. Every operation carries a tenant ID and nothing else — the Engine has no organization or product concept. It validates the tenant ID before any resource access, and it rejects an empty or malformed value there. ## Next steps *** Import it, provide the ports, and construct it with a runnable example. Every port, whether it is required, and what happens without it. # Fetcher Engine ports Source: https://docs.lerian.studio/en/fetcher/fetcher-engine-ports Reference for the eight capability ports of the Fetcher Engine: what each contract requires, whether it is mandatory, and exactly what the Engine does without it. The [Fetcher Engine](/en/fetcher/fetcher-engine-overview) owns no infrastructure. It reaches the outside world only through **ports** — Go interfaces your host application implements and passes to `engine.New`. This page is the reference for all eight. The **Without it** column is the point of the page. Graceful degradation is the contract you plan against, so read it before you skip a port. ## The ports at a glance *** | Port | Required | Without it | | ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `ConnectorRegistry` | **Always** | `engine.New` fails. No Engine exists. | | `CredentialProtector` | With encrypted persistence | `engine.New` fails when encrypted persistence is on. Otherwise the Engine passes no supplied password to the connection store. | | `ConnectionStore` | Optional | Connection-backed operations fail; `Limits()`, `AuthorizeConnectionAccess()`, and `CheckActiveExecutions()` remain available. | | `ExecutionStore` | Optional | No durable execution-state tracking. | | `ResultSink` | Optional | Store mode is unavailable. Extraction runs in direct mode. | | `SchemaCache` | Optional | Schema discovery always hits the live datasource. | | `ActiveExecutionChecker` | Optional | No conflict gating. Connection updates and deletes always proceed. | | `Observability` | Optional | Tracing hooks become no-ops. | `engine.New` rejects a port passed as a **typed nil**, not only a literal nil. An interface value that wraps a nil pointer fails at construction with a clear validation error, instead of panicking at first use. ## ConnectorRegistry *** **Required: always.** This is the only port the Engine validates unconditionally. The registry resolves a connector factory by datasource type. It performs no I/O, it resolves deterministically by type, and it reports `ok=false` for a type nobody registered. Building and connecting a connector happens later, through the factory it returned. **Without it:** `engine.New` returns a validation error with the message `connector registry is required`. You get no Engine value at all, so extraction is impossible. ## CredentialProtector *** **Required: only with `WithEncryptedPersistence(true)`.** The interface defines `Protect`, which returns protected bytes plus the key version that protected them, and `Reveal`, which lets a host adapter decrypt with a given key version. With encrypted persistence enabled and a supplied password, the Engine calls `Protect` and stores the protected sidecar. `Reveal` is available to host adapters; the Engine core does not invoke it. Your host owns key derivation, rotation, and storage, and the Engine records only the key version returned by `Protect` as secret-free metadata. **Without it:** * With encrypted persistence **on**, `engine.New` fails with `credential protector is required when encrypted persistence is enabled`. The Engine refuses to construct rather than persist plaintext credentials. * With encrypted persistence **off**, the port is genuinely optional, and the Engine does not pass a supplied password to the `ConnectionStore`. ## ConnectionStore *** **Required: optional at construction, load-bearing at runtime.** The store persists and resolves connection descriptors owned by a tenant. It is the only persistence seam the connection operations use — the Engine embeds no MongoDB, no SQL, and no host repository. It exposes nine operations: create, find, find-by-id, update, update-by-id, delete, delete-by-id, list, and list-paged. Two obligations fall on your implementation. It **must** scope every record by the tenant ID, so one tenant never sees another tenant's connections. It **must not** return secret material — the connection descriptor carries none. **Without it:** `engine.New` succeeds, and then nearly everything fails. Every operation that touches a connection returns the validation error `connection store is not configured`. That covers all nine connection operations plus plan, execute, discover-schema, fresh-schema discovery, validate-schema, and test-connection. `Limits()`, `AuthorizeConnectionAccess()`, and `CheckActiveExecutions()` remain callable; the last uses its optional checker or is a no-op when none is configured. Do not read `ConnectionStore` as a CRUD-only convenience. Skipping it disables extraction and schema discovery too. ## ExecutionStore *** **Required: optional.** The store upserts execution lifecycle state for a tenant. The Engine writes the transitions synchronously and inline: `running`, then `completed`, `failed`, or `canceled`. Those writes are **best-effort by design**. The Engine discards a save error, so optional persistence can never corrupt an extraction result. A result-sink write failure behaves differently and does fail the execution. The two are deliberately distinct. **Without it:** the Engine runs with no durable execution tracking, and your host owns execution state externally. ## ResultSink *** **Required: optional. It selects the result mode.** The sink persists result payloads to host-managed storage. Store-mode extraction calls `OpenResultStream`, so the Engine writes the result incrementally in constant memory. `PersistResult` remains for whole-payload writes. The stream shape is contractual NDJSON — one JSON object per line, newline-terminated, with no enclosing array: ```json theme={null} {"config":"","table":"","row":{"":""}} ``` Lines come out in canonical order: steps by ascending plan-step ordinal, rows within a step in cursor order. The SHA-256 digest covers exactly the bytes written by that run; two runs produce the same NDJSON and digest only when the datasources return rows in a stable order. On an abort — a write error, an exceeded size limit, or a canceled context — the Engine abandons the writer and never calls `Close`. Treat an unclosed writer as a discarded write, because a partial result must never become a returned reference. **Without it:** store mode is unavailable. The default `auto` mode resolves to direct, so extraction returns inline bytes and persists nothing. An explicit store-mode request fails up front with `store mode requires a configured result sink`, before the Engine builds any connector. ## SchemaCache *** **Required: optional.** The cache stores and returns schema snapshots per tenant and config name. The Engine treats it as an accelerator, never as a source of truth. A failed cache read degrades to fresh discovery. A failed cache write still returns the discovered schema to the caller. The always-fresh discovery call ignores the cache on every invocation, even when you wired one, so the live-datasource contract of the Manager schema endpoint holds. **Without it:** the Engine discovers schema live from the datasource on every call. ## ActiveExecutionChecker *** **Required: optional.** The checker reports whether a connection currently has active executions. `UpdateConnection` and `DeleteConnection` consult it before they mutate a connection. `UpdateConnectionByID` and `DeleteConnectionByID` deliberately do not; a host using those operations must call `CheckActiveExecutions` with the resolved config name before mutating. The port is deliberately **logical**, not a durable job store. Your host decides how to answer: a job repository, an in-memory tracker, a distributed lock, or always false. The Engine never imports a job repository to ask the question. The connection identity it passes is the config name inside the tenant scope. Your answer **must** be tenant-scoped, so one tenant's running work never blocks another tenant's mutation. **Without it:** the Engine performs no conflict gating, and connection updates and deletes proceed unconditionally. ## Observability *** **Required: optional.** The contract has one method. `StartSpan` takes a context and an operation name, and returns a derived context plus an end function the Engine defers. One method is the whole point: the Engine core never imports a tracing library, and your host adapts its own tracer behind the seam. **Without it:** span creation returns the incoming context and a no-op end function. Tracing hooks disappear with no other change in behavior. ## Next steps *** Import, provide the ports, and construct with a runnable example. The three-layer model, the import boundary, and the two result modes. # Extraction jobs Source: https://docs.lerian.studio/en/fetcher/fetcher-extraction-jobs How a Fetcher extraction job describes datasources, tables, schemas, and fields — and what happens to the job from creation to a stored result. An extraction job is an asynchronous request to read data out of one or more registered databases. The Manager accepts the job and queues it. A Worker runs the extraction and stores the result. This page explains the request you send and the path the job takes. ## The request *** A job carries two parts: `dataRequest` and `metadata`. ```json theme={null} { "dataRequest": { "mappedFields": { "my_postgres": { "public.accounts": ["id", "email", "created_at"], "accounting.invoices": ["*"] }, "my_mongo": { "transactions": ["*"] } } }, "metadata": { "source": "payments", "correlationId": "settlement-2026-07-13" } } ``` `mappedFields` is the heart of the request. It maps each datasource config name to the tables you want, and each table to the fields you want. `metadata.source` is **required**. It names the product that owns the job. The Manager rejects a request without it. The Manager also rejects the request when a referenced connection belongs to a different product, or belongs to no product at all. Internal datasources carry no product, so this check skips them. ## Field projection *** Name the fields you want, or ask for all of them: * A list of field names extracts exactly those fields. * The single entry `["*"]` extracts every column of the table. The wildcard must be the only entry in the list. The generic Engine validates selected fields against its cache-first resolved schema before it queries. An invalid selection fails the job with the safe `extraction request failed schema validation` error. Use the schema-validation workflow for per-field diagnostics. ## Multiple datasources, tables, and schemas *** One job can read from several datasources at the same time. Each datasource can contribute several tables. Each table can sit in a different schema. Write a table name in one of two forms: * **Unqualified** — `accounts`. Fetcher reads it from the engine default schema: `public` on PostgreSQL, `dbo` on SQL Server. * **Schema-qualified** — `accounting.invoices`. The prefix before the dot is the schema. On Oracle it is the owner. Fetcher collects the distinct schema prefixes in `mappedFields` and discovers only those namespaces. If any table name is unqualified, discovery adds the engine default schema as well: `public` on PostgreSQL, `dbo` on SQL Server. Two engines do not take a schema list. MySQL treats the connected database as the namespace. MongoDB has collections instead of schemas, so a collection name is always unqualified. ## Limits *** The extraction engine bounds every job. These are the default values: | Bound | Default | | --------------------------------- | --------- | | Datasources per job | 10 | | Tables per datasource | 20 | | Fields per table | 50 | | Datasources extracted in parallel | 4 | | Extraction timeout | 5 minutes | | Serialized result size | 256 MiB | The Manager rejects a job with more than 10 datasources before it reaches the queue. A result over the size limit fails the job. Fetcher never returns or stores a truncated result. For embedded Engine callers, `ExtractionRequest.Overrides` can lower, but never raise, an Engine limit. The standalone Manager job payload has no limit-override field and maps overrides as nil; the Worker only accepts a positive `ENGINE_MAX_RESULT_BYTES` override. These bounds cover generic datasource work; `plugin_crm` uses the Worker's explicit compatibility path. ## Duplicate jobs *** The Manager computes a SHA-256 hash over the whole request — `dataRequest` and `metadata` together. It then looks for a job with the same hash created in the last **5 minutes**. * A match returns the existing job with HTTP 200. No second extraction runs. * No match creates a new job and returns HTTP 202. * A match that already **failed** does not block a retry. The Manager creates a new job. Metadata is part of the hash. Two requests that read the same data under different correlation IDs are different jobs. ## Job lifecycle *** | Status | Meaning | | ------------ | ----------------------------------------------------------- | | `pending` | The Manager accepted the job and published it to the queue. | | `processing` | A Worker claimed the job and is reading the datasources. | | `completed` | The result is stored, and `job.completed` is published. | | `failed` | The extraction stopped, and `job.failed` is published. | Before it creates the job, the Manager resolves every datasource name to a connection and opens a real connection to each one. A datasource that fails this test rejects the whole request. Internal datasources configured through environment variables skip the test. A Worker performs a best-effort check that a job is `pending`, then moves it to `processing` and starts the extraction. This is not an atomic pending-to-processing compare-and-set, so concurrent deliveries can both observe `pending`. Generic datasource work runs in parallel up to the concurrency bound and is fail-fast; `plugin_crm` uses the Worker's explicit compatibility path. On success the Worker writes the result to object storage and records two values on the job: the result path and the HMAC signature of the result. It then publishes the terminal event. ## Next steps *** The ten filter operators and the value shape each one takes. What behaves differently on each of the five database engines. # Filters Source: https://docs.lerian.studio/en/fetcher/fetcher-filters The ten Fetcher filter operators, their value shapes, and how each datasource applies them. Filters narrow the rows an extraction job reads. You attach them per field, and Fetcher turns them into a `WHERE` clause on a relational engine or a query document on MongoDB. ## Where filters live *** Filters sit next to `mappedFields` in the request, four levels deep: datasource, then table, then field, then the operator object. ```json theme={null} { "dataRequest": { "mappedFields": { "my_postgres": { "transactions": ["id", "amount", "status", "created_at"] } }, "filters": { "my_postgres": { "transactions": { "status": { "in": ["completed", "pending"] }, "amount": { "gt": [100], "lte": [5000] }, "created_at": { "between": ["2026-06-01", "2026-06-30"] } } } } } } ``` Every datasource named under `filters` must also appear under `mappedFields`. The Manager rejects a filter that points at an unknown datasource. ## The ten operators *** Every operator takes a **JSON array**, even when it holds one value. The array is the shape. What changes per operator is the number of elements. | Operator | Value shape | Result | | --------- | -------------------------------- | ------------------------------------------------------------------------------- | | `eq` | One or more values | One value matches with `=`. Two or more become `IN (…)`. | | `gt` | Exactly one value | `field > value` | | `gte` | Exactly one value | `field >= value` | | `lt` | Exactly one value | `field < value` | | `lte` | Exactly one value | `field <= value` | | `between` | Exactly two values, `[min, max]` | `field >= min AND field <= max`, inclusive on both ends. | | `in` | One or more values | `field IN (…)` | | `nin` | One or more values | `field NOT IN (…)` | | `ne` | One or more values | One value gives `field <> value`. Each extra value adds another `<>` condition. | | `like` | One string pattern | `field LIKE pattern`, with the SQL wildcards `%` and `_`. | Examples of each shape: ```json theme={null} { "status": { "eq": ["active", "pending"] }, "amount": { "gt": [100] }, "created_at": { "gte": ["2026-06-01"] }, "total": { "lt": [1000] }, "closed_at": { "lte": ["2026-06-30"] }, "value": { "between": [100, 1000] }, "state": { "in": ["active", "pending", "suspended"] }, "state_two": { "nin": ["deleted", "archived"] }, "kind": { "ne": ["internal"] }, "description": { "like": ["%refund%"] } } ``` ## How operators combine *** On relational engines, non-empty operators on the same field combine with `AND`. The example `{ "gt": [100], "lte": [5000] }` reads as `amount > 100 AND amount <= 5000`. The current MongoDB adapter does not safely combine `eq` with another operator on the same field, or two operators that write the same MongoDB key. A later assignment can replace the earlier condition. Use one non-conflicting operator per MongoDB field. Filters on different fields also combine with `AND`. There is no `OR` between fields. Use `in` when you need `OR` over the values of one field. ## Validation rules *** For non-empty arrays, Fetcher validates `between` as two values and `gt`, `gte`, `lt`, and `lte` as one value. Empty arrays add no condition. `like` applies only when its first value is a string. Extra values, an empty array, and a non-string first value are currently ignored rather than rejected. `eq`, `in`, `nin`, and `ne` use the values they receive when the array is non-empty. ## Fields that look like identifiers *** On the relational engines, Fetcher inspects the field name. A name that contains `id`, `_id`, `uuid`, `template_id`, `organization_id`, `user_id`, or `account_id` is treated as a UUID field. Every string value under `eq`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, and `nin` must then parse as a UUID. A value that does not parse fails the request and names the field. Two operators are outside this check: `ne` and `like`. MongoDB does not run the check at all. ## Dates in a between filter *** On the relational engines, Fetcher extends the upper bound of a `between` filter to the end of the day when three things hold at the same time: 1. The field name looks like a date field. It contains `date`, `time`, `_at`, `created_at`, `updated_at`, `deleted_at`, or `completed_at`. 2. Both bounds satisfy Fetcher's date-like string heuristic: at least ten characters, a hyphen, and either ten characters or a `T`. 3. The upper bound is ten characters long. This is a field-name and string-shape heuristic, not calendar-date validation. When it applies, Fetcher rewrites the upper bound to `YYYY-MM-DDT23:59:59.999Z`. On MongoDB, both bounds apply exactly as you write them. To cover a whole day there, write the upper bound as a full timestamp: `["2026-06-01", "2026-06-30T23:59:59.999Z"]`. ## Filters on MongoDB *** MongoDB takes the same ten operators, and Fetcher translates them into query operators: | Operator | MongoDB form | | ------------------------ | ---------------------------- | | `eq` with one value | `{ "field": value }` | | `eq` with two or more | `$in` | | `gt`, `gte`, `lt`, `lte` | `$gt`, `$gte`, `$lt`, `$lte` | | `between` | `$gte` and `$lte` | | `in` | `$in` | | `nin` | `$nin` | | `ne` with one value | `$ne` | | `ne` with two or more | `$nin` | | `like` | `$regex` with the `i` option | The `like` pattern becomes a regular expression: `%` turns into `.*`, and `_` turns into `.`. Fetcher anchors the pattern at the start unless it opens with `%`, and at the end unless it closes with `%`. The `i` option makes the match case-insensitive. ## Matching the table key *** The table key under `filters` must find its table under `mappedFields`. PostgreSQL and SQL Server accept three forms of the key: the exact table name, the name without its schema prefix, and the name with the default schema added. A filter keyed `transactions` therefore still applies to the table `public.transactions`. MySQL and MongoDB match the key literally. Oracle normalizes identifiers to uppercase, so its table-key matching is case-insensitive; it does not add or remove a default schema. ## Next steps *** The full job request and the path from creation to a stored result. What behaves differently on each of the five database engines. # Getting started with Fetcher Source: https://docs.lerian.studio/en/fetcher/fetcher-getting-started Two paths to a first Fetcher extraction: the in-memory engine harness with no infrastructure, or the full Manager and Worker stack with Docker Compose. There are two ways to see Fetcher work. Pick one. | Path | What it costs | What it proves | | --------------------------- | --------------------------------------------------- | --------------------------------------------------------------- | | **A — Embedded Engine** | One `go get`. No infrastructure. | Extraction rules, planning, limits, and the direct-mode result. | | **B — Standalone services** | Docker Compose. MongoDB, RabbitMQ, storage, Valkey. | The REST API, asynchronous jobs, and stored results. | Path A is the shortest route to a first success. Start there if you only want to evaluate the product. ## Path A — Run the Engine with no infrastructure *** The Engine ships an in-memory harness at `pkg/engine/memory`. It covers the storage-facing ports: the connector registry, the connection store, the schema cache, the result sink, and the execution store. You need no MongoDB, no RabbitMQ, and no object storage. It ships no `CredentialProtector`, so turning encrypted persistence on means supplying your own. ```bash theme={null} go get github.com/LerianStudio/fetcher/pkg/engine ``` The Engine is a separate module from the services. It has no third-party dependencies, so this import pulls in nothing else. ```go theme={null} package main import ( "context" "fmt" "log" "github.com/LerianStudio/fetcher/pkg/engine" "github.com/LerianStudio/fetcher/pkg/engine/memory" ) func main() { ctx := context.Background() store := memory.NewConnectionStore() registry := memory.NewConnectorRegistry() // WithConnectorRegistry is the only required option. eng, err := engine.New( engine.WithConnectorRegistry(registry), engine.WithConnectionStore(store), ) if err != nil { log.Fatal(err) } // Every operation is scoped to a tenant. tenant, err := engine.NewTenantContext("tenant-123") if err != nil { log.Fatal(err) } conn := memory.NewTemplateConnector(memory.ConnectorBehavior{ Schema: engine.SchemaSnapshot{ ConfigName: "pg-main", Tables: []engine.TableSnapshot{{Name: "public.users", Fields: []string{"id", "email"}}}, }, Rows: map[string][]map[string]any{ "public.users": {{"id": 1, "email": "a@example.com"}}, }, }) registry.Register("postgres", memory.NewConnectorFactory(conn)) if _, err = eng.CreateConnection(ctx, tenant, engine.NewConnectionInput(engine.ConnectionInputParams{ ConfigName: "pg-main", Type: "postgres", Host: "localhost", Port: 5432, })); err != nil { log.Fatal(err) } // Plan validates the request against a cache-first schema snapshot and enforces limits. plan, err := eng.PlanExtraction(ctx, tenant, engine.ExtractionRequest{ MappedFields: map[string]engine.FieldSelection{ "pg-main": {"public.users": {"id", "email"}}, }, }) if err != nil { log.Fatal(err) } result, err := eng.ExecuteExtraction(ctx, plan) if err != nil { log.Fatal(err) } fmt.Printf("rows=%d bytes=%d\n", result.Direct.RowCount, len(result.Direct.Data)) } ``` No result sink is wired here, so the Engine runs in **direct mode**. It returns the rows inline as indented JSON, plus a SHA-256 digest over those exact bytes. The bytes are deterministic: the same input always produces the same digest. To move to production, swap the memory harness for your own adapters. Fetcher's own Manager and Worker are the reference implementation. ## Path B — Run the standalone services *** This path gives you the REST API and asynchronous jobs. Everything runs locally under Docker Compose. ### Prerequisites * [ ] **Docker** and **Docker Compose** * [ ] **Make** * [ ] **Go**, for development only. The toolchain version lives in the repo's `go.mod`. ### Set up and run ```bash theme={null} git clone https://github.com/LerianStudio/fetcher.git cd fetcher ``` ```bash theme={null} make set-env ``` This copies each component's `.env.example` to `.env`. ```bash theme={null} make generate-master-key ``` Copy the key into `APP_ENC_KEY` in **both** `components/manager/.env` and `components/worker/.env`. Both services need the same value. The Worker uses it to decrypt credentials and to check message signatures. **Replace the placeholder before startup:** use a valid Base64-encoded 32-byte key. The placeholder created by `make set-env` fails while decoding an invalid Base64 master key. The `master key too short: got 0 bytes, minimum 32 required` message applies to an empty or short value that does decode. Fetcher has no plaintext fallback mode. ```bash theme={null} make up ``` * REST API: `http://localhost:4006` * Scalar API reference, when `SWAGGER_ENABLED=true`: `http://localhost:4006/swagger/docs` * RabbitMQ management: `http://localhost:3008` ### Run your first extraction An extraction has three moves. Register a connection, create a job, then poll the job. #### 1. Register a database connection ```bash theme={null} curl -X POST http://localhost:4006/v1/management/connections \ -H "Content-Type: application/json" \ -H "X-Product-Name: quickstart" \ -d '{ "configName": "my_postgres", "type": "POSTGRESQL", "host": "host.docker.internal", "port": 5432, "databaseName": "mydb", "userName": "postgres", "password": "postgres" }' ``` The `X-Product-Name` header names the product that owns the connection. Use the same value in `metadata.source` on the job in step 2, because Fetcher compares the two. Fetcher encrypts the password before it stores the record. Test the connection before you use it: ```bash theme={null} curl -X POST http://localhost:4006/v1/management/connections/{id}/test ``` #### 2. Create an extraction job Name the fields you want, per table, per datasource: ```bash theme={null} curl -X POST http://localhost:4006/v1/fetcher \ -H "Content-Type: application/json" \ -d '{ "dataRequest": { "mappedFields": { "my_postgres": { "accounts": ["id", "email", "created_at"] } } }, "metadata": { "source": "quickstart" } }' ``` The API answers `202 Accepted` with a job ID. Send the same request twice within 5 minutes and you get `200 OK` with the first job instead of a second one. A failed job does not block a retry. #### 3. Poll the job ```bash theme={null} curl http://localhost:4006/v1/fetcher/{id} ``` A job ends in one of two terminal states: `completed` or `failed`. On completion the Worker has encrypted the result into object storage and published a `job.completed` event. The two states before that are `pending` and `processing`. [Extraction jobs](/en/fetcher/fetcher-extraction-jobs) gives the full four-state lifecycle. Turn on authentication with `PLUGIN_AUTH_ENABLED=true`. Requests then carry an `Authorization: Bearer ` header. This quickstart runs with authentication off. ## Next steps *** Connections, schema discovery, jobs, filters, and results. Every environment variable, per component. Dependencies, queues, scaling, and the fail-closed startup checks. Key derivation, rotation, message signing, and host validation. # Job events Source: https://docs.lerian.studio/en/fetcher/fetcher-job-events Consume the job.completed and job.failed notifications a Fetcher Worker publishes — payload, CloudEvents envelope, deduplication, and result verification. Every extraction job ends in one terminal event. The Worker publishes `job.completed` when the result reaches storage, and `job.failed` when the run stops. Subscribe to those events instead of polling `GET /v1/fetcher/{id}`. This page covers the consumer side: what arrives, what it means, and what to do with it. ## Where the events arrive *** The Worker publishes to a durable **topic** exchange. `RABBITMQ_JOB_EVENTS_EXCHANGE` names it, and the shipped value is `fetcher.job.events`. | Event | Routing key | | ------------------------- | --------------- | | Job finished successfully | `job.completed` | | Job stopped with an error | `job.failed` | Bind your own queue to the routing keys you care about. The local infrastructure definition binds two queues as an example: `fetcher.job.completed.queue` and `fetcher.job.failed.queue`. Job events are a product contract, not an optional feature. The Worker refuses to start without streaming enabled and without an exchange name. See [Deployment](/en/fetcher/fetcher-deployment) for the operator side. ## `job.completed` *** This event means one thing: the extraction ran to the end, and the encrypted result now sits in object storage. The Worker publishes it after two earlier steps. First it writes the result object, then it records the terminal status on the job. Only then does it emit the event. ```json theme={null} { "job_id": "6a5b0f8c-2f1e-4a41-9f0e-3d2c1b0a9e88", "status": "completed", "metadata": { "source": "payments", "correlationId": "settlement-2026-07-13" }, "result": { "path": "external-data/6a5b0f8c-2f1e-4a41-9f0e-3d2c1b0a9e88.json", "size_bytes": 184320, "row_count": 1204, "format": "json", "hmac": "9f1c…", "integrity": { "algorithm": "HMAC-SHA256", "signature": "9f1c…" }, "protection": { "encrypted": true, "applied_by": "adapter", "mode": "adapter-managed" } }, "execution_time_ms": 8421, "completed_at": "2026-07-13T18:04:11.204Z" } ``` Fetcher copies submitted metadata into the event, so `source` and a correlation identifier remain application fields. On `job.failed`, `metadata.error` is reserved for Fetcher's sanitized error details and replaces a caller-supplied value for that key. `size_bytes` and `row_count` describe the plaintext result, before encryption. `path` is the object key of the stored result. ## `job.failed` *** This event means the run stopped. Extraction is fail-fast, so the first datasource that fails ends the whole job. A `job.failed` payload has no `result` or `completed_at`, but it does not prove that no object exists: the Worker writes the encrypted object before it persists `completed`, and a terminal-status persistence failure can produce `job.failed` after storage succeeded. The event fires for any failure along the path: an unresolvable connection name, a datasource error, a schema mismatch, or a storage write that did not complete. ```json theme={null} { "job_id": "8f2c1a9e-4b30-4c02-9a1b-2d5e6f7a1c33", "status": "failed", "metadata": { "source": "payments", "error": { "message": "failed to connect to datasource" } } } ``` A failed event carries no `result` block and no `completed_at`. `metadata.error.message` passes through a redaction step first. Fetcher replaces four leak shapes with `[redacted]`: connection URIs, the address operand of a Go network error, the `Addr:` operand of a MongoDB driver error, and an IPv4 literal. The rest of the text survives, so the message stays actionable. Route it to operators. ## The CloudEvents envelope *** Every message travels in CloudEvents binary mode, version 1.0. The context attributes ride as AMQP headers. | Header | Value | | -------------------- | ----------------------------------------------------------- | | `ce-specversion` | `1.0` | | `ce-id` | `fetcher.job..` — the deduplication key | | `ce-source` | The value of `STREAMING_CLOUDEVENTS_SOURCE` | | `ce-type` | `studio.lerian.job.completed` or `studio.lerian.job.failed` | | `ce-time` | RFC 3339 emission timestamp | | `ce-subject` | The job identifier | | `ce-resourcetype` | `job` | | `ce-eventtype` | `completed` or `failed` | | `ce-schemaversion` | `2.0.0` | | `ce-datacontenttype` | `application/json` | | `ce-tenantid` | The tenant that owns the job | A single-tenant deployment still carries a tenant value. It emits the literal `single-tenant`, so one consumer handles both deployment shapes with the same code. Schema `2.0.0` is a breaking payload version. Update your consumer for the snake\_case Fetcher-owned fields shown above. The `job.completed` and `job.failed` routing keys and the `ce-id` format do not change. ### CloudEvents source configuration *** `STREAMING_CLOUDEVENTS_SOURCE` has no default value. The Worker requires it whenever streaming is on, and it stops at startup when the value is empty. The shipped example uses `//lerian.fetcher/worker`. Fetcher copies the value into `ce-source` verbatim. Give each Worker deployment its own source value when several producers share one broker, and route on that header. ## Delivery contract *** Delivery is **at-least-once**. Deduplicate on `ce-id`. * The Worker writes the event to a durable outbox before it publishes. A broker outage delays the event, it does not lose it. * Every 30 seconds, a repairer scans terminal jobs whose pending-event marker remains set. That includes a failed marker-clear after a successful publish, so it can re-emit an event that already reached the broker. * `ce-id` stays identical across every re-emission of the same job and status. Nothing else is stable enough to key on. * Order is not a guarantee. Two jobs can complete in one order and arrive in another. * The Worker records the terminal job status before it emits. `GET /v1/fetcher/{id}` remains the authority on job state. Treat a repeated `ce-id` as a duplicate and acknowledge it without reprocessing. A consumer that keys on the message identifier of the broker will process the same job twice. ## Verifying what you receive *** Two HMAC-protected artifacts use the **external HMAC key**. HKDF-SHA256 derives that key from the `APP_ENC_KEY` master key. The Fetcher repository ships a small tool that prints it, so a consumer verifies them without holding the master key. **The message.** The Worker signs each published message and stamps three headers on it: `x-message-signature`, `t` for the signing timestamp, and `signature-version`. The envelope signs the timestamp, signature version, tenant, exchange, routing key, and message body. Current v2 events emit `job_id`, while the envelope extractor recognizes `jobId`; do not rely on a separately extracted job-ID field. **The result.** `result.hmac` is the keyed HMAC-SHA256 over the plaintext result JSON, computed before encryption. The `integrity` block states the same value with its algorithm. Verify it after you decrypt and before you trust the rows. The `protection` block describes the stored bytes: `encrypted` is true, the storage adapter applied it, and the mode is `adapter-managed`. It describes the result only, never the datasource credentials. ## Next steps *** What a job requests, and the states it moves through. Create a job, read a job, and manage connections. The streaming, exchange, and encryption variables behind these events. The Manager, the Worker, and the Engine they both run over. # Fetcher observability Source: https://docs.lerian.studio/en/fetcher/fetcher-observability Fetcher health and readiness endpoints, parallel dependency probes, drain behavior on SIGTERM, circuit-breaker reporting, Prometheus metrics, and OpenTelemetry tracing. Fetcher answers three operational questions through HTTP: is the process alive, can it serve traffic right now, and which dependency is at fault. This page covers each surface and what it reports. ## Endpoints *** | Endpoint | Answers | Manager | Worker | | -------------------- | ---------------------------------------------- | ------- | ------ | | `/health` | Is the process alive and did it start cleanly? | Yes | Yes | | `/readyz` | Can this instance serve traffic right now? | Yes | Yes | | `/readyz/tenant/:id` | Can it serve one tenant? | Yes | Yes | | `/metrics` | Prometheus exposition. | Yes | Yes | | `/version` | Build version. | Yes | — | The Manager serves all of these on `SERVER_ADDRESS`. The Worker has no API server, so it runs a health micro-server on `HEALTH_PORT`, which defaults to `4007`. All of them mount before authentication. Kubernetes and load-balancer probes need no token. `/readyz/tenant/:id` is mounted on both services; outside multi-tenant mode it returns HTTP 400 stating that multi-tenant mode is disabled. ## `/health` and the startup self-probe *** `/health` is not a static 200. At boot, Fetcher runs every dependency probe once, in parallel, then flips a process-wide flag on the result. Until that self-probe succeeds, `/health` returns 503. **The kubelet restarts a pod whose dependencies failed at boot.** It does not send it traffic. The flag starts false, so a process that crashes mid-probe never reports healthy by accident. Point your liveness probe at `/health`. Each dependency also emits its self-probe outcome as a metric. A repeated boot failure therefore shows on a dashboard, not only in the logs. ## `/readyz` and the dependency probes *** `/readyz` runs every registered probe on every request, in parallel, one goroutine per dependency. The handler holds no cache and no background state. A cached answer opens a window where Kubernetes keeps routing to a degraded pod. A healthy aggregate returns 200. Anything else returns 503. The response body reports each dependency by name, with its status, its latency, and its TLS posture. ### What each service probes | Dependency | Manager | Worker | | -------------------- | ----------------- | ----------------- | | `mongodb` | Yes | Yes | | `rabbitmq` | Yes | Yes | | `redis` | Yes | — | | `multi_tenant_redis` | Multi-tenant only | Multi-tenant only | | `tenant_manager` | Multi-tenant only | Multi-tenant only | | `s3` | — | Yes | In multi-tenant mode, the shared MongoDB and RabbitMQ entries report `n/a` with the reason `multi-tenant: see /readyz/tenant/:id`, and the per-tenant probes move to that endpoint. ### Per-dependency timeouts Each probe runs under a fixed deadline. The values are not configurable, so every Lerian service has the same readiness latency envelope and one dashboard threshold works across the fleet. | Dependency class | Deadline | | ----------------------------------------------------------------- | -------- | | Databases (`mongodb`, `postgres`, `mysql`, `oracle`, `sqlserver`) | 2s | | Cache (`redis`, `valkey`) | 1s | | Queue (`rabbitmq`) | 2s | | Storage (`s3`, `seaweedfs`) | 2s | | HTTP upstreams (`tenant_manager`, `upstream_*`) | 1s | A probe that ignores its deadline does not block the response. The handler substitutes a `down` result for it. ## Circuit breaker state *** Tenant resolution runs behind a circuit breaker. The `MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD` variable sets how many consecutive failures open it, and it defaults to 5. The `MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC` variable sets how long it stays open, and it defaults to 30 seconds. The global `tenant_manager` check confirms client wiring only. During tenant validation, an open Tenant Manager breaker returns 503. After validation, a tenant-scoped MongoDB or RabbitMQ check can report `down`, `circuit breaker open`, and `breaker_state: open`. That distinguishes a tripped breaker from an ordinary connection failure. ## Drain on SIGTERM *** On `SIGTERM` or `SIGINT`, both services enter a drain before they tear down connections. 1. `/readyz` short-circuits to 503 for `READYZ_DRAIN_DELAY_SEC` seconds, which defaults to 12 and has a minimum of 1. 2. Kubernetes removes the pod from Service endpoints while it still serves in-flight work. 3. Only then do connections close. The service skips the real probes during the drain. The response carries one synthetic dependency named `draining` with status `down`, and it emits metrics like any other. **Alerts keep rating through a rolling deploy.** The synthetic `draining` dependency keeps the metric series alive, so a dashboard shows a drain rather than a gap. Set your termination grace period above the drain window. ## Metrics *** `/metrics` serves Prometheus exposition, including the Go runtime and process collectors. | Metric | Type | Labels | Reports | | -------------------------- | --------- | --------------- | --------------------------------------------------------- | | `readyz_check_duration_ms` | histogram | `dep`, `status` | Duration of each readiness probe, in milliseconds. | | `readyz_check_status` | counter | `dep`, `status` | Outcome count per dependency and status. | | `selfprobe_result` | gauge | `dep` | Last startup self-probe result: `1` for up, `0` for down. | The histogram buckets run from 1 ms to 5,000 ms. Metric names, labels, and buckets are a platform contract, and dashboards across the fleet depend on them. The duration histogram records the wall-clock time the probe contributed to the handler, not the latency the probe reported for itself. That is the number that explains a slow `/readyz`. ## Tracing *** Set `ENABLE_TELEMETRY=true` and point `OTEL_EXPORTER_OTLP_ENDPOINT` at your collector. Fetcher then exports OpenTelemetry traces and metrics over OTLP. Set the OTEL resource attributes for your deployment: `OTEL_RESOURCE_SERVICE_NAME`, `OTEL_RESOURCE_SERVICE_VERSION`, `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT`, and `OTEL_LIBRARY_NAME`. The shipped examples use `fetcher` for the Manager and `fetcher-worker` for the Worker; those values are configurable, not runtime defaults. The engine emits its own spans through a one-method port. A host that supplies no tracer gets a no-op, and behavior does not change. ## What to alert on *** 1. **`/readyz` 503 outside a deploy window.** One dependency name in the response body tells you which. 2. **A rising dead-letter queue.** Each message there is a job the Worker could not process. See [Deployment](/en/fetcher/fetcher-deployment). 3. **`selfprobe_result` at 0 for any dependency.** A pod restarted into a broken dependency. 4. **A tenant-scoped check with `breaker_state: open`.** An open Tenant Manager breaker can also surface as a 503 during tenant validation; the global `tenant_manager` check itself reports client wiring, not breaker state. ## Next steps *** Dependencies, queues, scaling, and startup checks. Every environment variable, per component. Keys, signing, encryption at rest, and host validation. Job lifecycle, terminal states, and events. # Fetcher REST API Source: https://docs.lerian.studio/en/fetcher/fetcher-rest-api Orient yourself in the Fetcher Manager API: bearer authentication, product scope, pagination, filtering, and the RFC 9457 error shape that all 12 operations share. The **Manager** serves the Fetcher HTTP API. It carries 12 operations in two areas: * **Extraction jobs** under `/v1/fetcher` — create a job, read a job. * **Connections** under `/v1/management/connections` — the connection lifecycle, schema reads, connection tests, and the two product-assignment operations. All 12 operations render under the **Fetcher** anchor in the [API Reference](/en/reference/introduction). This page covers what those operations share. It does not restate their request and response shapes. 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 *** Fetcher accepts a JWT bearer token: ```http theme={null} Authorization: Bearer ``` Authentication is a deployment choice. `PLUGIN_AUTH_ENABLED` turns the auth middleware on, and `PLUGIN_AUTH_ADDRESS` points it at the identity service. The Manager refuses to start when you enable auth and leave the address empty. Multi-tenant mode also requires effective authentication — the router refuses to build a tenant middleware without it. See [Configuration](/en/fetcher/fetcher-configuration). Every one of the 12 operations declares `401` and `403`. Fetcher authorizes each request against the `fetcher` application, a resource (`connections` or `fetcher`), and an action that matches the HTTP method. Five routes stay outside authentication so probes keep working: `/health`, `/readyz`, `/readyz/tenant/{id}`, `/metrics`, and `/version`. ## Product scope *** Only create connection, list connections, and connection assignment read `X-Product-Name`. It identifies the product that owns an external connection. * **Create connection requires it.** Fetcher rejects a missing, empty, or whitespace-only value. * **List connections treats it as optional.** With the header, stored connections are scoped to one product. Without it, stored connections remain in the tenant scope. * **Connection assignment requires it.** Other connection endpoints do not consume this header. A job names its owning product in required `metadata.source`. For an external connection, that value must match the connection's product. Internal datasources are excluded from this product check. ## Asynchronous jobs *** `POST /v1/fetcher` answers `202 Accepted` and returns a job identifier with status `pending`. The Worker runs the extraction after the response. Fetcher deduplicates job requests by a request hash over a **5-minute window**. A duplicate inside that window answers `200 OK` and returns the existing job instead of queueing a second one. A job that already failed does not suppress a retry — you can resubmit it. To follow a job, poll `GET /v1/fetcher/{id}`, or subscribe to the terminal events described in [Job events](/en/fetcher/fetcher-job-events). ## Pagination *** Both list operations — connections and unassigned connections — use offset pagination with the same query parameters. | Parameter | Default | Rules | | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `page` | `1` | Page number. Must be 1 or greater. | | `limit` | `10` | Items per page. Must be 1 or greater, and at most `MAX_PAGINATION_LIMIT` (100 as shipped). | | `sortOrder` | `desc` | `asc` or `desc`. Fetcher always sorts by creation date. | | `startDate` | — | Inclusive lower timestamp boundary on creation date, parsed from `YYYY-MM-DD`. | | `endDate` | — | Inclusive upper timestamp boundary, parsed at midnight from `YYYY-MM-DD`. It does not include the rest of that calendar day; use the following date to cover a whole final day. | A page response carries `items`, `page`, `limit`, and `total`. ```json theme={null} { "items": [], "page": 1, "limit": 10, "total": 42 } ``` A list request always applies a creation-date window. Send neither `startDate` nor `endDate`, and Fetcher applies the last month up to tomorrow. Older connections fall outside that window. Set both dates when you want a wider view. `MAX_PAGINATION_MONTH_DATE_RANGE` caps the width of that window at one month as shipped. Ask for a wider range and Fetcher moves `startDate` forward to fit the cap. It does not answer with an error. On page 1, environment-defined internal datasources appear ahead of stored connections and count toward `total`. Product, creation-date, and metadata filters apply to stored connections; only `type` filters the internal list. ## Filtering *** The connection list accepts two filters beyond the date window. * **`type`** — Fetcher normalizes the value to uppercase and uses it as an equality filter. An unrecognized value normally matches no stored connection; the parser does not restrict it to five values. * **`metadata.=`** — an exact match on a metadata entry you stored with the connection. For example, `metadata.region=br`. The unassigned list narrows by the date window alone. It answers one question — which connections still carry no product — so it takes no type or metadata filter. Fetcher ignores an unknown query parameter rather than failing the request. Three cases still fail with `FET-0405`: * A key that starts with `$`, which blocks query-operator injection. * A key that starts with `_`, which blocks internal fields. * A key longer than 64 characters, or a value longer than 256 characters. ## Errors *** Every error answers `application/problem+json` and follows [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457). ```json theme={null} { "type": "/FET-0001", "title": "Bad Request", "status": 400, "detail": "empty request body", "code": "FET-0001", "errors": [ { "location": "body.configName", "message": "is required" } ] } ``` | Field | What it carries | | ---------- | ------------------------------------------------------------------------------ | | `type` | URI reference to Fetcher's coded problem documentation. | | `title` | HTTP status text for this problem. | | `status` | The HTTP status code. | | `detail` | Explanation specific to this occurrence. | | `instance` | URI reference for this specific occurrence. | | `code` | Stable domain error code in the form `FET-NNNN`. | | `errors` | Optional per-field details with `location` and `message`; `value` is optional. | Match on `code`, not on `title` or `detail`. The codes group by range: | Range | Meaning | Examples | | ---------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `FET-000x` | General outcomes | `FET-0001` bad request, `FET-0002` internal error, `FET-0004` conflict, `FET-0005` not found | | `FET-04xx` | Request problems | `FET-0403` invalid header, `FET-0405` invalid query parameter, `FET-0406` pagination limit exceeded, `FET-0414` forbidden host | | `FET-10xx` | Business rules | `FET-1002` entity conflict, `FET-1021` job in progress, `FET-1040` connection down | | `FET-106x` | Schema validation | `FET-1060` validation failed, `FET-1062` object not found | Fetcher redacts driver-level failures at its boundary. It discards the raw database error, so a connection string, a credential, or a driver internal never reaches a caller. ## Reading the spec from a running Manager *** `SWAGGER_ENABLED=true` mounts a Scalar reference at `/swagger/docs` and the OpenAPI 3.1 document at `/swagger/openapi.json` and `/swagger/openapi.yaml`. Keep it off in production. ## Next steps *** The 12 operations, with full request and response shapes. React to `job.completed` and `job.failed` instead of polling. Connections, schema discovery, extraction jobs, filters, and results. The environment variables behind auth, pagination caps, and the API surface. # Schema discovery Source: https://docs.lerian.studio/en/fetcher/fetcher-schema-discovery How Fetcher reads the tables and fields of a datasource, when it serves a cached snapshot, and how it validates an extraction mapping before the first query runs. Fetcher reads the shape of a datasource for you. A **schema snapshot** lists the tables of one datasource and the field names of each table. You maintain no separate catalog, and you upload no schema file. Two uses depend on a snapshot. A caller reads one to learn what fields exist. Generic extraction validates selected tables and fields during planning before it queries data. That validation uses a cache-first snapshot, so a cache hit is not a live-schema check. The `plugin_crm` compatibility path is separate. ## What a snapshot holds *** | Element | Content | | ------------ | ------------------------------------------------------------ | | `configName` | The connection the snapshot describes. | | Tables | One entry per table or collection, under its qualified name. | | Fields | The field names of each table, in sorted order. | Names arrive qualified when the table sits outside the default namespace. PostgreSQL returns `accounting.invoices` for a table in another schema and plain `users` for one in `public`. SQL Server applies the same rule around `dbo`. Oracle returns `OWNER.TABLE` when the owner differs from the connected user. A snapshot carries names and nothing else. It holds no rows, no credentials, and no connection string. System tables never reach it: the database adapter drops `pg_*`, `information_schema`, and the Oracle dictionary views before the snapshot leaves the adapter. ## Live discovery and cached discovery *** The Manager exposes two schema surfaces, and they differ on purpose. | Operation | Freshness | | ------------------------------------------------- | --------------------------------------------------------------------------------------- | | `GET /v1/management/connections/{id}/schema` | Always live. It never reads the cache and never writes to it. | | `POST /v1/management/connections/validate-schema` | Cache first. It serves a cached snapshot when one exists, and discovers live otherwise. | The split follows the two use cases. Somebody who asks for a schema wants the current truth, often right after a migration added a column. Validation runs on the way into every job, so a round trip to the database on each call would cost far more than it returns. Discovery follows a fixed order, and each gate runs before the next one acquires anything: Fetcher validates the tenant scope before it touches any resource. Fetcher resolves the connection inside that scope. An unknown connection — or one that belongs to another tenant — stops here as `404 Not Found`. On the cache-first path, a hit returns at once. Fetcher builds no connector and opens no database session. On a miss, Fetcher resolves the driver for the datasource type, opens a connector, and reads the catalog. It closes the connector on every path, success or failure. Fetcher stores the snapshot under the tenant and the config name, then returns it. ## What the schema cache buys *** The cache turns a database round trip into a lookup. A hit skips the connector build and the catalog read together, so a job that validates twenty tables across three datasources pays for none of them a second time inside the window. * **Key.** Every read and every write is scoped to the tenant and the config name. One tenant never sees another tenant's snapshot and never poisons it. * **Lifetime.** Five minutes by default. `SCHEMA_CACHE_TTL_SECONDS` sets it on the Manager. * **Backing store.** The Manager keeps the cache in Valkey or Redis, and falls back to process memory when that store is unreachable. The cache is an optimization, and Fetcher treats it as one. A failed cache read degrades to a live discovery. A failed cache write still returns the discovered snapshot to the caller. Neither failure reaches your response. ### Running without a cache The Engine schema-cache port is optional. Without one, discovery runs live from the datasource. Validation remains correct; it pays the database round trip each time. Add a cache when validation traffic repeats against stable schemas. Leave it out when the host runs occasional extractions, or when a live read on every call is the behavior you want. ## Discovery per database *** | Datasource | How Fetcher reads the catalog | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PostgreSQL | Reads `information_schema` for base tables and their columns. Falls back to the `public` schema when the connection names none. | | MySQL | Reads `information_schema` for tables, columns, and primary-key constraints. | | Oracle | Reads the `ALL_TABLES` and `ALL_TAB_COLUMNS` dictionary views for the owners you name, and the user's own tables otherwise. The connected user is the default owner. | | SQL Server | Reads `information_schema` and falls back to the `dbo` schema. | | MongoDB | Infers the shape. A collection declares none. | Catalog reads carry a 30-second timeout. ### MongoDB inference MongoDB has no declared schema, so Fetcher builds one in two passes. An aggregation over the collection produces field names. A sample of up to 50 documents then gives each field its type. The field-name pass is bounded by collection size. For collections of up to 10,000 documents, the aggregation processes up to 1,000 documents through `$limit`; it does not guarantee which documents are selected. Above 10,000 documents, it takes a random sample instead: | Collection size | Documents read for field names | | -------------------- | ------------------------------ | | Up to 1,000 | All of them | | 1,001 to 10,000 | Up to 1,000 through `$limit` | | 10,001 to 100,000 | A random sample of 2,000 | | 100,001 to 1,000,000 | A random sample of 5,000 | | Above 1,000,000 | A random sample of 10,000 | The snapshot names fields carried by that limited set or sample. Generic extraction proceeds only when planning validates the selected field in its schema snapshot; do not rely on a field omitted from that snapshot extracting successfully. When aggregation fails on one collection, Fetcher falls back to sampling for that collection and continues discovery of the remaining collections. ## Validation before extraction *** `POST /v1/management/connections/validate-schema` takes the same `mappedFields` map an extraction job carries. Send it before you submit the job. A clean validation returns `200` with `status: success`. Schema inconsistencies return `422 application/problem+json` with code `FET-1060`; each issue is an `errors` detail with a location and message, and can include a structured value. If none of the requested datasources resolves, Fetcher instead returns top-level `400 FET-1062`. The Engine checks selection shape and configured limits before it reads a schema. It then resolves each datasource inside the tenant scope and validates table and field membership against the cache-first snapshot. ## Next steps *** Submit a job, follow it, and read the result. Register, test, update, and delete a datasource connection. What each of the five database engines does differently. The Fetcher model in one place. # Fetcher security Source: https://docs.lerian.studio/en/fetcher/fetcher-security Fetcher's security model: one master key expanded into four derived keys, signed messages bound to tenant and route, credential and result encryption, host validation, and tenant isolation. Fetcher holds credentials for databases it does not own, and it moves rows that came out of them. This page describes what protects each of those, and what an operator has to do. ## One master key, four derived keys *** `APP_ENC_KEY` is the only key you supply. Generate it with `make generate-master-key`, which produces a base64-encoded 32-byte value. Set the same value on the Manager and on the Worker. Fetcher never uses that key directly. It expands it with HKDF-SHA256 (RFC 5869) into four independent keys, one per purpose. | Derived key | Used for | Derivation label | | ------------------ | -------------------------------------------------------------- | ------------------------------- | | Credential | AES-256-GCM encryption of datasource passwords at rest. | `fetcher-credentials-v1` | | Internal HMAC | Signature on every message between the Manager and the Worker. | `fetcher-internal-hmac-v1` | | External HMAC | Signature on extraction results, for third-party verification. | `fetcher-external-hmac-v1` | | Storage encryption | AES-GCM encryption of the stored result. | `fetcher-storage-encryption-v1` | Separation is the point. A consumer that holds the external key can check a result signature. It cannot decrypt a stored credential, and it cannot forge a message between the two services. **A bad master key stops the service.** An unset key, invalid Base64, or a value under 32 bytes exits the process at startup. An invalid Base64 value fails while decoding; `master key too short: got 0 bytes, minimum 32 required` is an empty or short decoded-key case. Fetcher has no plaintext fallback. ## Key version and rotation *** `APP_ENC_KEY_VERSION` labels the key that is in force. Every connection record stores the version that encrypted its password, so an operator can tell which key a record belongs to. Increment the version when you change the master key. Fetcher does not retain prior credential keys. After changing the master key and version, provide each known password again to update and re-encrypt the connection under the current version, or deliberately delete and recreate the connection. Do not expect automatic key migration. The external HMAC key changes with the master key. Derive the new key and give it to every consumer that checks signatures. Earlier results verify against the earlier key. Generate the external key with `make derive-key KEY=""`. The tool also reads `APP_ENC_KEY` from the environment or the key from standard input, and it prints a 64-character hex key. ## Credentials at rest *** A datasource password never reaches MongoDB in the clear. The Manager encrypts it with AES-256-GCM under the derived credential key, then stores the ciphertext and the key version. In single-tenant mode, fixed internal datasources load from `DATASOURCE_{NAME}_*` environment variables. In multi-tenant mode, Fetcher resolves them per tenant through Tenant Manager. In both modes they are internal, in-memory connections with an empty key version and no MongoDB connection record. ## Results at rest *** The Worker protects a stored result in two steps: 1. It signs the plaintext JSON with HMAC-SHA256 under the derived external key, and records the algorithm and the signature with the result. 2. It encrypts the payload with AES-GCM under the derived storage key, with a fresh 12-byte random nonce, and stores the result base64-encoded. The signature covers the plaintext, so a consumer verifies the data it received and not the envelope around it. The repository ships a verification guide at `scripts/crypto/derive-key/verification-guide.md`. Direct mode returns the rows inline without encryption. The engine reports them as plaintext and attaches a SHA-256 digest over the exact bytes. ## Signed messages between the services *** Every RabbitMQ message the Manager publishes to the Worker carries an HMAC-SHA256 signature under the derived internal key. The signature covers the timestamp, signature version, tenant identifier, exchange, routing key, and message body. Context binding prevents a signed message from being retargeted to another tenant or route. Freshness separately rejects messages older than five minutes and messages more than 30 seconds in the future. Context binding alone does not prevent replay of the same message in the same context during that window. By default, the Worker requires this canonical tenant- and route-bound envelope. `RABBITMQ_ALLOW_LEGACY_BODY_SIGNATURE_FALLBACK=true` is a temporary migration setting that also accepts a legacy body-only signature so already-queued messages can drain. The publisher strips caller-supplied security headers before it signs, and the signer compares signatures in constant time. ## Datasource host validation *** A tenant that registers its own connection could point it at your internal network. With `MULTI_TENANT_ENABLED=true`, Fetcher checks the host before it connects. Validation runs at two layers: 1. **At request parse.** Fetcher rejects an IP literal in a blocked range, with no DNS lookup. 2. **In the datasource factory.** Fetcher checks the hostname against a blocklist that covers `localhost`, cloud metadata names, and the `.local`, `.internal`, and `.cluster.local` suffixes. It then resolves the hostname and checks every address it gets back. Fetcher refuses a blocked host with a forbidden-host error. It delegates hostname and IP classification to its `lib-commons` SSRF dependency. Internal datasources are deliberately exempt: in single-tenant mode they come from the operator environment, and in multi-tenant mode Tenant Manager resolves them. ## Tenant isolation *** The Engine scopes every operation by tenant identifier, and that identifier is the sole Engine isolation boundary. A malformed tenant identifier fails before Fetcher touches any resource. Fetcher also enforces product ownership at the host layer: an external connection belongs to one product and a job's `metadata.source` must match it; internal datasources are the exception. In multi-tenant mode, Fetcher resolves metadata resources from the tenant context supplied by the tenant middleware. Worker consumers use their authoritative tenant context. Do not attribute tenant resolution specifically to JWT claims unless the middleware owner documents that contract. Access fails closed: a request with no resolved tenant database returns an error instead of reading a shared database. ## Authentication and probe surfaces *** `PLUGIN_AUTH_ENABLED=true` puts the Access Manager middleware in front of the API. Requests then carry a bearer token, and Fetcher authorizes each operation against a resource and an action. `/health`, `/readyz`, `/readyz/tenant/:id`, `/metrics`, and `/version` mount before that middleware, so Kubernetes and load-balancer probes stay unauthenticated. ## Errors never leak connection material *** Fetcher discards the raw driver error at the engine boundary and returns a fixed message in its place. A raw driver error can embed a DSN or a credential, so a caller sees `failed to connect to datasource` instead of the string the driver produced. Failures arrive classified into stable categories — validation, unauthorized, forbidden, limit exceeded, connect, timeout, and others — so a host maps them to its own status codes without parsing text. ## Next steps *** Every environment variable, per component. Dependencies, storage retention, scaling, and startup checks. Probes, drain behavior, metrics, and tracing. Register, test, and use a connection. # Fetcher use cases Source: https://docs.lerian.studio/en/fetcher/fetcher-use-cases Concrete scenarios for Fetcher: centralizing database access across services, feeding reconciliation and reporting, publishing verifiable extracts, and embedding extraction in your own application. Each scenario below states the problem first, then what changes when Fetcher owns the extraction path. ## Stop rebuilding database access in every service *** **The problem.** Lerian hit this internally. Each product team built its own data-access logic against the same external databases, and each copy aged separately. Fetcher exists to centralize that work. The same pattern appears in any company that runs several internal systems against the same operational databases. Every team writes its own connection pool, its own secret handling, and its own query builder. That is the same work, done five times. **What changes.** An external connection belongs to one product. Consumers of that product can reuse it; a job's `metadata.source` must match the connection's product. Fetcher encrypts the password once, tests the connection on demand, and applies the same ten filter operators to all five database types. A new consumer registers no drivers and holds no credentials. It calls an API. Adding a datasource type becomes one team's work instead of five teams' work. ## Feed reconciliation and reporting from operational databases *** **The problem.** Reconciliation and report generation both need rows that live outside the platform. Those rows sit in a bank's operational PostgreSQL, in a partner's MySQL, or in a MongoDB customer store. The consuming product must reach them without becoming a database client itself. **What changes.** For generic datasources, the Worker validates selected tables and fields during planning before it queries data. That validation uses a cache-first schema snapshot, so a cache hit is not a live-schema check. The `plugin_crm` compatibility path is separate. The Worker writes standalone-service results to object storage and publishes `job.completed` or `job.failed`; the consumer reacts to the event. In single-tenant mode, Fetcher loads fixed internal datasources from `DATASOURCE_{NAME}_*` environment variables. In multi-tenant mode, it resolves them per tenant through Tenant Manager. In both modes they are internal in-memory connections and need no MongoDB connection record. ## Publish an extract that a third party can verify *** **The problem.** You send a data extract to an auditor, a regulator, or a partner. They need proof that the file came from your systems and that nobody edited it in transit. **What changes.** The Worker signs the plaintext JSON with HMAC-SHA256 before it encrypts the payload, then stores the result with AES-GCM encryption at rest. Your recipient derives the external verification key from your master key with the `make derive-key` command, and checks the signature independently. That derived key verifies signatures only. It cannot decrypt stored credentials, and it cannot forge internal messages between the Manager and the Worker. ## Add extraction to a service you already run *** **The problem.** You need extraction inside one Go service. Standing up a Manager, a Worker, MongoDB, RabbitMQ, and object storage costs more than the feature is worth. **What changes.** Your service imports the Engine module and calls it in-process. The Engine has no third-party dependencies, so the import adds no operational surface. With no result sink wired, the Engine runs in direct mode and returns the rows inline as indented JSON, with a SHA-256 digest over the exact bytes. That output is deterministic. Identical input produces byte-identical JSON and the same digest, whatever order the parallel datasource steps finished in. You can persist those bytes and sign them yourself. ## Serve many tenants from one deployment *** **The problem.** You run one Fetcher for many customers. Each customer registers its own database hosts. One customer must never read another customer's rows, and no customer may point a connection at your internal network. **What changes.** Multi-tenant mode resolves metadata resources from tenant context supplied by the tenant middleware. Access fails closed: a request with no resolved tenant database returns an error instead of reading a shared database. The same switch turns on host validation. Fetcher rejects a tenant-supplied host that resolves to a private, loopback, or cloud-metadata address. It checks an IP literal at request-parse time, and a hostname again in the datasource factory. Operator-configured internal datasources stay exempt. ## Next steps *** Run a first extraction with no infrastructure, or with the full Docker stack. Connections, schema discovery, extraction jobs, filters, and results. # What is Fetcher? Source: https://docs.lerian.studio/en/fetcher/what-is-fetcher Fetcher extracts data from PostgreSQL, MySQL, Oracle, SQL Server, and MongoDB through one API. Run it as two services, or embed its engine in your own application. **Fetcher** reads data out of databases that your application does not own. It connects to PostgreSQL, MySQL, Oracle, SQL Server, and MongoDB, discovers tables and fields, and extracts the rows you request. Standalone Fetcher supplies the HTTP API and connection storage. The embedded Engine is a Go API whose host supplies the connector registry and any needed connection or credential ports. For generic extraction, Fetcher validates the selection against a cache-first schema snapshot, discovering live on a cache miss. Embedded direct mode returns plaintext JSON with a SHA-256 digest when no `ResultSink` is wired; the unkeyed digest detects accidental corruption, not tampering. Store-mode persistence and protection are host/sink-defined; the standalone Worker owns encryption, storage, and HMAC. ## Source-available *** Fetcher is source-available under the [Elastic License 2.0](https://github.com/LerianStudio/fetcher/blob/main/LICENSE). The full source code lives on [GitHub](https://github.com/LerianStudio/fetcher), and every engine source file carries an `SPDX-License-Identifier: Elastic-2.0` header. Fetcher and Midaz are the two source-available Lerian products. ## Two ways to run it *** The duality below is the shape of the product. Pick the row that matches your problem. | Shape | What you deploy | When to pick it | | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | **Standalone services** | A **Manager** (HTTP API) and a **Worker** (queue consumer), plus MongoDB, RabbitMQ, object storage, and Valkey/Redis. | You want a shared extraction service with a REST API, asynchronous jobs, and stored results. | | **Embedded Engine** | Nothing. Your Go application imports `github.com/LerianStudio/fetcher/pkg/engine` and runs extraction in-process. | You want extraction inside a service you already operate, with no new infrastructure. | The Engine is a separate Go module from the services module (`github.com/LerianStudio/fetcher/v2`). It currently declares **zero third-party dependencies**. CI enforces the dependency boundary by rejecting non-stdlib and non-module-local dependency graphs, not by checking syntactically for a `require` block. Your host application supplies the parts the Engine needs through small interfaces, called ports. The Manager and the Worker use that same Engine for generic extraction. The Worker retains a separate compatibility path for MongoDB `plugin_crm` extraction. The Engine ships with an in-memory harness (`pkg/engine/memory`) that covers the storage-facing ports: the connector registry, the connection store, the schema cache, the result sink, and the execution store. You can run a real extraction with no MongoDB, no RabbitMQ, and no object storage. The harness does not cover credential protection: encrypted persistence is off by default, and turning it on requires your host application to supply a `CredentialProtector`. See [Getting started](/en/fetcher/fetcher-getting-started). ## What Fetcher gives you *** 1. **Connection management.** Store, validate, and test database connections. Fetcher encrypts each password with AES-256-GCM before it writes the record. 2. **Schema discovery.** Fetcher detects tables, columns, and data types across all five database types, with a cache and an always-fresh read. 3. **Data extraction.** One query interface with field projection, ten filter operators, and multi-table, multi-schema, multi-datasource requests. 4. **Job orchestration.** Asynchronous processing with a 5-minute duplicate window, status tracking, and `job.completed` and `job.failed` events. ## Datasources *** Fetcher accepts five datasource types. Send the identifier upper-case on the API — request validation matches the five values exactly, and `postgresql` is rejected with `400`. Operator-declared internal datasources (`DATASOURCE_{NAME}_TYPE`) and the `type` query filter on the connection list do accept any case. | Type identifier | What is distinctive | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `POSTGRESQL` | Multi-schema. Fetcher parses `JSONB` and `JSON` columns into objects, arrays, or strings. Tables outside `public` come back schema-qualified. | | `MYSQL` | Fetcher parses JSON columns the same way it does for PostgreSQL. | | `ORACLE` | Owner namespaces. Tables owned by the connected user come back bare, and other owners come back as `OWNER.TABLE`. | | `SQL_SERVER` | Multi-schema, with `dbo` as the default. Tables outside `dbo` come back schema-qualified. | | `MONGODB` | Schemaless inference. Fetcher discovers the field set with an aggregation, then infers types from a document sample. | ## Safe by construction *** * **Tenant scoping.** Every engine operation carries a tenant ID, and that ID is the only isolation boundary. A malformed tenant ID fails before Fetcher touches any resource. * **Redacted errors.** Fetcher discards the raw driver error at its boundary, so a DSN, a credential, or a driver internal cannot reach a caller. * **Fail-fast extraction.** The first failing step stops the run. Fetcher never returns a partial result. * **Default limits.** Defaults are 10 datasources, 20 tables per datasource, 50 fields per table, 4 concurrent datasource workers, 5 minutes, and 256 MiB. Hosts configure the ceiling; a request can only lower it. * **Host validation.** In multi-tenant mode, Fetcher rejects a tenant-supplied host that resolves to a private, loopback, or cloud-metadata address. ## Next steps *** Concrete problems Fetcher solves, and what changes when you adopt it. Two paths to a first extraction: zero infrastructure, or full Docker stack. Connections, schema discovery, extraction jobs, filters, and results. Master key, derived keys, message signing, and host validation. # Compliance Source: https://docs.lerian.studio/en/flowker/compliance How Flowker supports regulatory compliance — 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, its status, how long it took, the output it returned, and the error message if it failed. * Use [List executions](/en/reference/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/flowker/get-execution-results) to read 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 *** Learn about Flowker's authentication model, encryption, and resilience features. Query execution history and step-level results through the API. # Configuring a webhook trigger Source: https://docs.lerian.studio/en/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 trigger's `input_contract` decides which payloads Flowker accepts and how it decodes them. Choose it before you write the node: it is required, and it fixes the payload format for the whole route. ## Before you start *** * A workflow in `draft` status. An active workflow is locked, so add the trigger before you activate it. See [Getting started with Flowker](/en/reference/flowker/flowker-api-quick-start) for the create-and-activate path. * With `PLUGIN_AUTH_ENABLED=true` — required in production — grant the `execute` permission on the `webhooks` resource to every system you let call the path. See [Securing a webhook](/en/flowker/integration-guide#securing-a-webhook). A non-production deployment with plugin authentication disabled uses a non-authorizing passthrough. * For the `xsd` contract: an XSD document in the registry. Upload it with [Upload an XSD schema](/en/reference/flowker/upload-xsd-schema) and keep the id it returns. To enforce XSD ingress validation, configure the XML validation service through [`XSD_VALIDATOR_URL`](/en/flowker/flowker-environment-variables). When it is unset, Flowker decodes well-formed XML but skips XSD validation. * For the `openapi` contract: an OpenAPI document in the registry ([Upload an OpenAPI schema](/en/reference/flowker/upload-openapi-schema), covered end to end in [Connecting your own API](/en/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/flowker/derive-openapi-operation-schema) shows you that request body. ## Step 1: Read the trigger contract from the catalog *** Triggers are built in. You discover them in the catalog, and you never create one. [List catalog triggers](/en/reference/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 http://localhost:4021/v1/catalog/triggers | jq . ``` [Get a catalog trigger](/en/reference/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 http://localhost:4021/v1/catalog/triggers/webhook | 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. The validator currently also accepts `format` on `xsd` and `openapi`, but those modes ignore it and force XML or JSON respectively; omit it there so the configuration does not imply that it changes the route. 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. 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 through that service: a document that does not conform is rejected with `XML_VALIDATION_FAILED`, and a verdict Flowker cannot trust with `FLK-0720`. Configure that service before you put an `xsd` route in front of a caller that requires schema enforcement. ## 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/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. | `response_view` is inert on an `async` route. For the full `passthrough` rules and for the `responseStatusCode` override, see [Synchronous response mode](/en/flowker/integration-guide#synchronous-response-mode). 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` | Always | `"open"`, `"xsd"` or `"openapi"`. | | `format` | With `open` | Required and effective only with `open`: `"json"` or `"xml"`. The validator currently accepts it with `xsd` and `openapi`, where it is ignored. | | `xsd_schema_id` | With `xsd` | The id [Upload an XSD schema](/en/reference/flowker/upload-xsd-schema) returned. | | `openapi_schema_id` | With `openapi` | The id [Upload an OpenAPI schema](/en/reference/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"`. | The trigger configuration is a closed contract. Saving a workflow whose webhook trigger omits `path`, `method` or `input_contract`, misses a field its `input_contract` mode requires, names another mode's schema id or operation field, or carries a key or value the schema rejects fails with `FLK-0934`. ```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 *** Send the node with the rest of your workflow to [Create a workflow](/en/reference/flowker/create-workflow). The workflow lands in `draft` status, and Flowker validates the trigger configuration here — a contract error answers `FLK-0934`. Call [Activate a workflow](/en/reference/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 http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/activate | 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/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 http://localhost:4021/v1/webhooks/payments/received \ -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/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/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/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/flowker/trigger-webhook), [GET](/en/reference/flowker/trigger-webhook-get), [PUT](/en/reference/flowker/trigger-webhook-put), [PATCH](/en/reference/flowker/trigger-webhook-patch) and [DELETE](/en/reference/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. This is the caller-facing `` value; Flowker maps it from the internal `FLK-0364` sentinel. | | `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 validation service and the referenced schema are available to your deployment. | | `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/flowker/flowker-error-list) for every code and both shapes. ## What's next *** Connect the workflow to external services, and read the full synchronous-response rules. Build the rest of the graph the trigger enters. # Connecting your own API Source: https://docs.lerian.studio/en/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. When the service you want to call is 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/flowker/integration-guide#authentication) for the methods Flowker supports. * A deployment whose schema registry has blob storage configured. `SCHEMA_REGISTRY_S3_BUCKET` holds the OpenAPI documents you upload — see [Flowker environment variables](/en/flowker/flowker-environment-variables). * 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. The Lerian Console covers the same path. **Providers → + New Provider → Add your own API** selects an uploaded document and sets the base URL and the authentication — see [Adding a provider](/en/flowker/console/adding-a-provider). ## Step 1: Upload the OpenAPI document *** Call [Upload an OpenAPI schema](/en/reference/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" ``` 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, and it is what everything else references — 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 *** [List OpenAPI schemas](/en/reference/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`. [Get an OpenAPI schema](/en/reference/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. [Derive an operation schema](/en/reference/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/flowker/create-provider-configuration) with `kind` set to `external_openapi`. That kind is the bring-your-own-OpenAPI connection: it 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/flowker/integration-guide#authentication) for each type and its fields. Omit it for a service that needs no authentication. | | `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. | ### Where the credential goes The secret inside `config.auth` is write-only on creation and update. Flowker sends it to your secrets backend, removes it from the configuration document before it saves the document, and resolves it from the backend at execution time. For an `external_openapi` configuration, get-by-id does not resolve or return the secret values in `config.auth`. Keep credentials in `config.auth`; other configuration values can be returned by a read. 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/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/flowker/update-provider-configuration) afterwards. `allowedHosts` 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: `type` is `"openapi"`, `schemaId` is the same id you put in `config.openapi_schema_id`, and the optional `operations` array scopes the stored binding and is validated against the document when you save the configuration. It does not verify what workflow nodes call or limit an `external_openapi` node at execution time; the node uses `config.openapi_schema_id`, `operation_path`, and `operation_method`. The binding is what makes the document's dependants visible. With it, [List resources referencing an OpenAPI schema](/en/reference/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 — 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 — answers `FLK-0293`. ```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`. Flowker does not call your target API here. It does read the referenced document and, when `config.auth` contains a secret, writes that secret to the configured secrets backend before it persists the configuration. ## 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/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, and is left out when no value is found. * **The request body** with the default `request_format` (`json`) is what your `inputMapping` builds. `xml_converted` serializes that mapped object as XML; `xml_passthrough` 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/flowker/working-with-request-and-response-data) covers mappings and transformations in full. ```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. 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/flowker/configuring-a-webhook-trigger). ## Step 5: Run it and confirm it worked *** Call [Activate a workflow](/en/reference/flowker/activate-workflow). Activation registers the webhook route and resolves what the trigger's contract references. Call [Execute a workflow](/en/reference/flowker/execute-workflow) with a fresh `Idempotency-Key` header, or send a request to the webhook route. 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/flowker/update-provider-configuration). `config` replaces the stored map rather than merging into it, so 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/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/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/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/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, so a new upload 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/flowker/get-catalog-executor) describes the version you chose. ## Removing a document *** [List resources referencing an OpenAPI schema](/en/reference/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. [Delete an OpenAPI schema](/en/reference/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/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/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/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-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/flowker/flowker-error-list) for every code. ## What's next *** Map values into the operation's request body and read its response back out. Validate an inbound payload against one operation of the same document. Set the authentication, the retries and the circuit breaker every provider configuration shares. Explore the schema registry endpoints. # Activating and deactivating a workflow Source: https://docs.lerian.studio/en/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. Workflow lifecycle management allows you to control whether a workflow is available for execution. You can activate, deactivate, or move a workflow back to draft status depending on your needs. ## Activating a workflow *** Activating a workflow makes it live and available for execution. Only **Draft** workflows can be activated. An **Inactive** workflow must first be moved back to Draft before it can be activated again. To activate a workflow: On the **Workflows** page, find the workflow you want to activate. Click the three dots () on the **Actions** column and select **Activate**. The workflow status will change to **Active**. Make sure your workflow is fully configured before activating 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: On the **Workflows** page, find the active workflow you want to deactivate. Click the three dots () on the **Actions** column and select **Deactivate**. 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. 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: On the **Workflows** page, find the workflow you want to move to draft. Click the three dots () on the **Actions** column and select **Move to Draft**. The workflow status will change to **Draft**. To edit a workflow, first deactivate it (an active workflow can't be edited directly), then use **Move to Draft**; make your changes and activate it again — or **Clone** it to iterate without touching the live version. # Adding a provider Source: https://docs.lerian.studio/en/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 connection to an external system that Flowker can interact with. To add a provider: Go to the **Providers** page and click the **+ New Provider** button. 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 one of the providers from the Lerian catalog. When you select the provider, additional fields will appear for you to configure the connection. Only catalog providers classified with internal credentials are provisioned automatically by the platform. Providers classified with external credentials, such as Tracer and Auth, require connection authentication settings. * **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/flowker/connecting-your-own-api) covers uploading the spec and calling its operations from a node. Review the configuration and click **Add Provider** to save the provider. The connection type is chosen at creation and cannot be changed later — editing a provider keeps its original type. After creation, the new provider will appear in the providers list with status `Active`, ready to be referenced by workflow nodes. # Canvas editor Source: https://docs.lerian.studio/en/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 provides 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 automatically when you create or edit a workflow. Navigate to **Flowker → Workflows**, then either create a new workflow or click **Edit Workflow** on an existing one. ## Canvas basics *** The canvas provides 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 and view its summary. * **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 the delete key, or use the context menu. ## Node types *** Flowker workflows are composed of four node types, each serving a distinct role in the execution flow. ### 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, but 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 and the input contract — see [Configuring a webhook trigger](/en/flowker/configuring-a-webhook-trigger) for what each contract accepts. For the schedule trigger, they are the cron expression, the timezone and the enabled flag — see [Running a workflow on a schedule](/en/flowker/running-a-workflow-on-a-schedule). Available trigger types include: | 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). | Trigger types are loaded from the Flowker catalog and may vary depending on your environment's configuration. The list above reflects the default available triggers. ### Executor Call Node **Executor Call Nodes** perform specific operations by calling executors registered in the Flowker catalog. Each executor represents 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/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 the organization has no provider configuration 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/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. | Executors are loaded from the Flowker catalog and may vary with `OPENAPI_NATIVE_PROVIDERS`, fixture policy, and successful provider and specification resolution. The table is illustrative; inspect the running catalog for the executors available to your deployment. The node's configuration is validated both locally and remotely (via the Flowker backend) against the catalog executor's JSON Schema before saving. 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; cases are evaluated 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). Workflows saved with a legacy free-text condition expression keep the original **true**/**false** output paths. New conditionals use the condition builder. 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 — useful as the terminal step of a workflow, especially with [synchronous webhook responses](/en/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 is 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. 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/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 is created in **Draft** status so you can customize it before activating. To clone a workflow: On the **Workflows** page, find the workflow you want to clone. Click the three dots () on the **Actions** column and select **Clone**. A copy of the workflow will be created with a default name (typically the original name with a suffix). The cloned workflow will appear in the workflows list with **Draft** status. After cloning, you can [edit the workflow](/en/flowker/console/editing-a-workflow) to update its name, modify nodes, and adjust configurations as needed. 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/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 and must be activated before it can be triggered. To create a workflow: Go to the **Workflows** page and click the **+ New Workflow** button. 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. A new workflow will be created and you will be taken to the visual canvas editor. Enter a **name** for the workflow in the **Workflow name...** field. 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`). To add a node, you can drag and drop, double-click, or click the plus (+) that appears when you hover over the node card. Connect the nodes by dragging from one node's output to another node's input to define the execution flow. Configure each node by clicking on it and filling in the required parameters in the side panel. 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/flowker/console/canvas-editor). The workflow will be created in **Draft** status. When your workflow is ready, you can [activate it](/en/flowker/console/activating-deactivating-workflow) to make it available for execution. # Creating Workflows Source: https://docs.lerian.studio/en/flowker/console/creating-workflows Start a new Flowker workflow from the + New Workflow button and choose Blank Workflow to open a fresh canvas. Every Flowker workflow starts from the same entry point — the **+ New Workflow** button on the Workflows page. From there you open a blank canvas and build your logic node by node. ## 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/flowker/console/activating-deactivating-workflow) for production use. ## Next step *** Start with a blank canvas and build your workflow node by node. # Deleting a provider Source: https://docs.lerian.studio/en/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: On the **Providers** page, find the provider you want to delete. Click the three dots () on the **Actions** column and select **Delete**. A **Confirm Deletion** dialog will appear with the provider name. Confirm the deletion. 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. 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/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: On the **Workflows** page, find the workflow you want to delete. Click the three dots () on the **Actions** column and select **Delete**. A **Confirm Deletion** dialog will appear with the workflow name. Confirm the deletion. Deleting a workflow cannot be undone. All associated configuration, nodes, and connections will be permanently removed. 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. Only **Draft** and **Inactive** workflows can be deleted. An **Active** workflow must be deactivated first — the delete request is rejected otherwise. # Editing a provider Source: https://docs.lerian.studio/en/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: On the **Providers** page, find the provider you want to edit. Click the three dots () on the **Actions** column and select **Details**. 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). Click **Save** to apply your changes. Updating a provider configuration affects all workflows that reference it. The configuration is read when each executor call node executes — so runs already in progress pick up the new settings for nodes that haven't executed yet. The provider type selected at creation can't be changed when editing — to switch to a different provider, create a new configuration. # Editing a workflow Source: https://docs.lerian.studio/en/flowker/console/editing-a-workflow Edit a Draft 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. A workflow must be in **Draft** to be edited — an active or inactive workflow can't be edited directly. If it's **Active**, deactivate it first (it becomes **Inactive**), then use **Move to Draft**; if it's already **Inactive**, use **Move to Draft** directly. (See [Activating and deactivating a workflow](/en/flowker/console/activating-deactivating-workflow).) Or **Clone** it to iterate without touching the live version. To edit a workflow: On the **Workflows** page, find the workflow you want to edit. Click the three dots () on the **Actions** column and select **Details**, or click the workflow name to open it. The visual canvas editor will open with the workflow's current configuration. Make your changes: * **Update the name** — edit the workflow name in the details panel. * **Add nodes** — drag new nodes onto the canvas from the node palette. * **Remove nodes** — select a node and delete it. * **Modify connections** — drag to create new connections or click existing connections to remove them. * **Configure nodes** — double-click any node to open its configuration panel and update parameters. Click **Save** to apply your changes. Your changes take effect on future executions. Runs already in progress are unaffected. For more details about the canvas editor and its node types, see [Workflow Canvas](/en/flowker/console/canvas-editor). # Executions overview Source: https://docs.lerian.studio/en/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 represent individual runs of your workflows. The Executions page provides a complete history of all workflow executions, allowing you to monitor progress, investigate failures, and review results. In this section, you'll learn how to manage executions via 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 for the organization in a data table, with the following columns: * **Workflow** — the name of the workflow that was executed (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 the execution was triggered. * **Duration** — how long the execution took to complete. * **Actions** — action menu to view execution details. You can filter executions by workflow name or status using the filter fields above the table. ## 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 *** # Flowker Dashboard Source: https://docs.lerian.studio/en/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 provides an at-a-glance overview of your workflow activity, covering 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 is the default landing page. ## Stat cards *** A row of four summary cards appears at the top of the page: | Card | Description | | -------------------- | ----------------------------------------------------------------------------------- | | **Total Workflows** | Total number of workflows created in the current organization, across all statuses. | | **Active Workflows** | Total number of workflows ready for execution. | | **Completed** | Number of executions that completed successfully. | | **Failed** | Number of executions that ended in failure. | These cards give you a quick health check on your workflow automation activity. ## Recent Workflows *** Below the stat cards, a list displays the most recently created or modified 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 modified date** — when the workflow was last updated. Click any workflow to open it in the canvas editor. ## Recent Executions *** A second list displays the 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`. * **Execution date** — when the execution was triggered. 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/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 within the Lerian Console that provides workflow automation capabilities. It lets you design visual workflows, configure provider integrations, execute automated processes, and monitor execution history — all through a visual interface. This section walks you through how to use the Console to create, edit, and manage workflows, configure providers, and monitor executions. Flowker is only available if you've acquired the Flowker product. Want access? [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. 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**. You can return to the product launcher by clicking **Back to products** in the sidebar. ## Available pages *** ### Workflows
Create, edit, and manage automated workflows using a visual canvas editor with configurable nodes and connections. Learn about the visual canvas editor and its node types: triggers, executor calls, conditionals, and actions.
### Executions
View and monitor the execution history of your workflows, including status tracking and detailed results.
### Providers
Configure and manage provider integrations used by your workflows to connect to external services.
# Managing workflows Source: https://docs.lerian.studio/en/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/flowker/console/activating-deactivating-workflow) for details. ## Available actions *** Modify the workflow's nodes, connections, and configuration in the visual canvas editor. Create a duplicate of an existing workflow to use as a starting point for a new one. Control whether the workflow is live and responsive to triggers, or paused. Permanently remove a workflow that is no longer needed. # Providers overview Source: https://docs.lerian.studio/en/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. In this section, you'll learn how to manage provider configurations via 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 displays all provider configurations for the organization in a data table, with the following columns: * **Name** — the provider configuration name. * **Provider** — the catalog integration type, for a `catalog` configuration. * **Type** — the configuration kind: `catalog` selects a catalog provider, while `external_openapi` references a tenant-provided OpenAPI document. * **Schemas** — XSD or OpenAPI schemas bound to the configuration. OpenAPI bindings can be restricted to operations. * **Status** — the configuration status: `Active` or `Disabled`. * **Last Modified** — the date the configuration was last updated. * **Actions** — action menu. You can filter provider configurations by name or status 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 it is enabled again. * **Delete** — permanently removes the provider configuration. ## Related pages *** # Viewing execution details Source: https://docs.lerian.studio/en/flowker/console/viewing-execution-details Open the Execution Detail 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: On the **Executions** page, find the execution you want to inspect. Click the execution row or select **View Details** from the action menu (). An **Execution Detail** side panel will open with the full execution information. ## Execution detail panel *** The detail panel displays the following information: ### Summary | 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. | The current state of the execution will appear as a tag at the top of the page. 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. * **Node ID** — the identifier of the node that produced the 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. 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/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. Workflows are the core building blocks of the Flowker module. Each workflow defines an automated process composed of trigger, executor call, conditional, and action nodes connected in a visual canvas. In this section, you'll learn how to manage workflows via 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 displays all workflows for the organization 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 date the workflow was created. * **Last Updated** — the date the workflow was last modified. * **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 be executed: | 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 presents actions according to the workflow's lifecycle state and trigger configuration: * **Details** — opens the workflow in the visual canvas editor (clicking the workflow name does the same). * **Schedule** — for a workflow with a valid schedule trigger, 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/flowker/running-a-workflow-on-a-schedule). * **Execute** — available only for an **Active** workflow; triggers an immediate execution. * **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** — available only for a **Draft** or **Inactive** workflow; permanently removes it. ## Related pages *** # Core concepts Source: https://docs.lerian.studio/en/flowker/flowker-concepts Understand the building blocks of Flowker: workflows, nodes, edges, catalog, provider configurations, templates, executions, and dashboard. Flowker is built around a set of interconnected concepts. Understanding how they relate to each other helps you design, configure, and run workflows effectively. The last section on this page shows how everything connects. ## 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/flowker/activate-workflow), [deactivate](/en/reference/flowker/deactivate-workflow), and [move to draft](/en/reference/flowker/move-workflow-to-draft) endpoints. ## Nodes and edges *** Nodes are the individual steps of a workflow — what you might call **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 | Always the first node. 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 lives in the conditional node, not in the edge: the conditional node evaluates its condition and produces an outcome handle, and Flowker 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 read-only registry of all built-in providers, executors, and triggers available in Flowker. You cannot create or modify catalog entries — you discover them. Before configuring any integration, browse the catalog to see what's already available: * **Catalog executors** are the built-in components a workflow node invokes — the generic HTTP connector, and the operations of native providers such as the Midaz ledger and Tracer. You discover them; you never create them. * **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/flowker/list-catalog-executors) and [List catalog triggers](/en/reference/flowker/list-catalog-triggers) to discover executor and trigger types. * [List catalog providers](/en/reference/flowker/list-catalog-providers) (`GET /v1/catalog/providers`) to list all available providers. * [Get catalog provider](/en/reference/flowker/get-catalog-provider) (`GET /v1/catalog/providers/{id}`) to get details for a specific provider. * [List executors by provider](/en/reference/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. Recognized sensitive leaves are written to the secrets backend and replaced by `secretRef`; legacy configurations without `secretRef` can contain inline values. * **`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/flowker/connecting-your-own-api). Provider configurations have two statuses: `active` (in use) and `disabled` (temporarily offline). Use [Disable provider configuration](/en/reference/flowker/disable-provider-configuration) to take a connection out of service and [Enable provider configuration](/en/reference/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/flowker/list-provider-configurations) endpoints to create, read, update, disable, enable, and delete your connections. ## Executor configuration registry *** Flowker keeps a registry of executor configuration records. The registry exposes four operations: | Operation | Endpoint | | --------- | ---------------------------------------------------------------------------------- | | List | [`GET /v1/executors`](/en/reference/flowker/list-executor-configurations) | | Get | [`GET /v1/executors/{id}`](/en/reference/flowker/get-executor-configuration) | | Update | [`PATCH /v1/executors/{id}`](/en/reference/flowker/update-executor-configuration) | | Delete | [`DELETE /v1/executors/{id}`](/en/reference/flowker/delete-executor-configuration) | Every record carries a `status`, which the API reports in each response: | Status | Description | | -------------- | ----------------------------------------- | | `unconfigured` | The record has no connection details yet. | | `configured` | The record carries connection details. | | `tested` | The record was verified. | | `active` | The record is in service. | | `disabled` | The record is out of service. | The update body does not include `status`, but the list operation accepts it as a query filter. Update applies to records in `unconfigured` or `configured` status; delete applies to records in `unconfigured`, `configured`, or `disabled` status. No operation in this version moves a record into `tested`, `active`, or `disabled`; the table lists those values because responses report them and the list filter accepts them. ## 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. The catalog ships the `tracer-midaz-validation` template ("Tracer Validation + Midaz Transaction"): it receives a webhook request, validates the transaction through Tracer, and creates the transaction in Midaz when Tracer approves it. 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. If lookup is unavailable or fails, it returns the original schema instead. To inspect a template: 1. [List the catalog templates](/en/reference/flowker/list-catalog-templates). 2. [Get the template detail](/en/reference/flowker/get-catalog-template) to see its parameter schema. 3. [Validate a set of parameters](/en/reference/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 it returns the accumulated workflow context. Use [Get execution status](/en/reference/flowker/get-execution-status) to monitor progress and [Get execution results](/en/reference/flowker/get-execution-results) to retrieve the full output. The status endpoint returns the execution record, including its current status. The dedicated results endpoint ([`GET /v1/executions/{id}/results`](/en/reference/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 returns HTTP `200` and includes `idempotencyReplayed` 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 — useful for building operational dashboards and monitoring tools. Use it whenever you need a high-level view of system health without querying individual executions. * [Workflow summary](/en/reference/flowker/get-dashboard-workflow-summary) returns totals and breakdowns by status (draft, active, inactive). * [Execution summary](/en/reference/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/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 a clear sequence: 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. Ready to see this in practice? Follow the [Getting started guide](/en/flowker/flowker-getting-started) to run your first workflow end to end. # Environment variables Source: https://docs.lerian.studio/en/flowker/flowker-environment-variables Reference for the Flowker environment variables covering server, scheduler, deployment, authentication, MongoDB, schema registry, secrets, and multi-tenant settings. This reference lists the environment variables that configure Flowker. The **Default** column lists only the values Flowker applies in code when a variable is not set. A dash means Flowker applies no fallback: set the variable explicitly, using the recommended value in the description. The **Required** column marks the variables that stop the server from starting when they are missing. Flowker ships two binaries. The API binary serves the HTTP API. The worker binary runs the scheduler that fires `schedule`-triggered workflows and serves only `/health` and `/readyz`. Variables that apply to one binary say so. ## Server | Variable | Description | Default | Required | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `ENV_NAME` | Environment name. Set one of `production`, `staging`, `uat`, `development`, or `local` — any other value, including an empty one, stops the server from starting. `production` turns on the production guards: request authentication, XSD schema authentication, and OpenAPI schema authentication must all be enabled or the server refuses to start. Recommended: `production` in production, `development` elsewhere. | — | Yes | | `SERVER_ADDRESS` | Listen address of the API binary. When empty, Fiber binds all interfaces on an OS-assigned ephemeral port; set it explicitly (recommended: `:4021`) for a predictable address. | — | No | | `WORKER_SERVER_ADDRESS` | Listen address of the worker binary's `/health` and `/readyz` app. Must differ from `SERVER_ADDRESS` so both binaries can run on the same host. | `:4022` | No | | `VERSION` | Version string reported by `/readyz`. | `dev` | No | | `CORS_ALLOWED_ORIGINS` | Comma-separated list of allowed CORS origins. Empty means no cross-origin access is allowed (restrictive default). | — | No | | `TRUSTED_PROXIES` | Comma-separated CIDR ranges of trusted proxies (your load balancer/ingress subnets). When set, `X-Forwarded-For` is trusted only from these hops, so Flowker resolves the real client IP — including the IP forwarded to Access Manager for IP-allowlist checks. Entries must be CIDR notation; a bare IP fails startup. Empty leaves the feature off. | — | No | ## Deployment | Variable | Description | Default | Required | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | -------- | | `DEPLOYMENT_MODE` | Deployment flavor: `local`, `byoc`, or `saas`. In `saas`, Flowker validates TLS for a non-empty static `MONGO_URI`; multi-tenant MongoDB connections are resolved later and are not TLS-validated at bootstrap. Fixture providers are exposed when `DEPLOYMENT_MODE` resolves to `local` after trimming whitespace and case-insensitive matching, provided the environment is not production. `/readyz` reports `local` when the variable is unset. Recommended: `byoc` or `saas` outside a developer workstation. | — | No | ## Authentication | Variable | Description | Default | Required | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------- | | `PLUGIN_AUTH_ENABLED` | Enable Access Manager plugin authentication. When `false`, the workflow, webhook, and management routes do not require authentication (local development only). With a production `ENV_NAME`, `false` stops the server from starting. | `false` | No | | `PLUGIN_AUTH_ADDRESS` | Address of the Access Manager service. The machine-to-machine token endpoint is derived from it by appending `/v1/login/oauth/access_token`. | — | Yes (if `PLUGIN_AUTH_ENABLED=true`) | | `XSD_SCHEMAS_AUTH_ENABLED` | Require authentication on the `/v1/xsd-schemas` routes. An unset or unrecognized value resolves to `true`. With a production `ENV_NAME`, `false` stops the server from starting. | `true` | No | | `OPENAPI_SCHEMAS_AUTH_ENABLED` | Require authentication on the `/v1/openapi-schemas` routes. An unset or unrecognized value resolves to `true`. With a production `ENV_NAME`, `false` stops the server from starting. | `true` | No | ## Database (MongoDB) | Variable | Description | Default | Required | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------ | | `MONGO_URI` | MongoDB connection URI. In multi-tenant mode no static pool is opened and per-tenant connections come from Tenant Manager instead. | — | Yes (single-tenant mode) | | `MONGO_DB_NAME` | MongoDB database name. Recommended: `flowker`. | — | Yes (single-tenant mode) | | `MONGO_TLS_CA_CERT` | Base64-encoded PEM CA certificate for TLS connections (e.g., AWS DocumentDB) | — | No | ## Scheduler The scheduler fires `schedule`-triggered workflows. It runs in the worker binary and connects through its own Redis settings, separate from the multi-tenant Redis settings. Nothing enforces the separation at startup: point the scheduler at its own Redis instance, or at least at a logical database index that tenant lifecycle events do not use. | Variable | Description | Default | Required | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------- | | `SCHEDULER_ENABLED` | Kill-switch for the scheduler. An unset or unrecognized value resolves to `true`; any valid boolean value that resolves to `false` (for example, `false`, `0`, or `f`) disables it. The worker binary refuses to start while it is `false`. | `true` | No | | `SCHEDULER_REDIS_HOST` | Host of the dedicated scheduler Redis. The scheduler queue lives here, so set it wherever the scheduler is enabled: with no host, the worker binary does not start and no scheduled workflow fires. | — | Yes (if `SCHEDULER_ENABLED=true`) | | `SCHEDULER_REDIS_PORT` | Port of the scheduler Redis | `6379` | No | | `SCHEDULER_REDIS_PASSWORD` | Password of the scheduler Redis. Required only when a worker starts an enabled scheduler with `DEPLOYMENT_MODE=saas`; API-only startup does not validate this setting. | — | Yes (worker; if `SCHEDULER_ENABLED=true` and `DEPLOYMENT_MODE=saas`) | | `SCHEDULER_REDIS_TLS` | Enable TLS for the scheduler Redis connection. Set `true` in production and point the scheduler at a TLS-terminating Redis. | `false` | No | | `SCHEDULER_REDIS_DB` | Logical Redis database index for the queue. When the scheduler shares a Redis instance with tenant lifecycle events, set a distinct index here; both default to `0`. | `0` | No | | `SCHEDULER_CONCURRENCY` | Worker concurrency cap for the scheduler consume server. A non-positive value resolves to `10`. | `10` | No | ## Lerian service URLs Internal providers resolve their base URL from the environment, by the `{PROVIDER}_BASE_URL` convention, instead of from the stored provider configuration. Leave a variable empty to fall back to the value in the stored configuration document. | Variable | Description | Default | Required | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `MIDAZ_BASE_URL` | Base URL for the `ledger` provider. Feeds both the transaction and the onboarding base URL. | — | No | | `TRACER_BASE_URL` | Base URL for the `tracer` provider | — | No | | `CRM_BASE_URL` | Base URL for the `crm` provider | — | No | | `FEES_BASE_URL` | Base URL for the `fees` provider | — | No | | `IDENTITY_BASE_URL` | Base URL for the `identity` provider | — | No | | `AUTH_BASE_URL` | Base URL for the `auth` provider | — | No | | `OPENAPI_NATIVE_PROVIDERS` | Comma-separated allowlist of native provider ids eligible for synthesis from their published OpenAPI spec. Empty makes every native provider eligible; a non-empty list narrows eligibility to the named ids. | — | No | ## Schema registry and XSD validation | Variable | Description | Default | Required | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `SCHEMA_REGISTRY_S3_BUCKET` | S3 storage is optional for boot. If the bucket is empty, Flowker starts with a MongoDB metadata path, but XSD and OpenAPI schema-content operations fail at use time; configure a bucket for a functional schema registry. | — | No | | `SCHEMA_REGISTRY_S3_REGION` | Pins the AWS region of the schema registry bucket. Empty resolves the region through the standard AWS SDK chain (`AWS_REGION` or shared config). | — | No | | `SCHEMA_REGISTRY_CACHE_TTL_SEC` | TTL of the parsed-spec cache behind the native output-schema resolver (seconds). A published spec change is picked up on the next fetch whose ETag differs, regardless of the TTL. | `300` | No | | `XSD_VALIDATOR_URL` | Base URL of the XSD validator sidecar (e.g. `http://xsd-validator:8081`). Leave it empty to run without XML schema validation. | — | No | | `XSD_VALIDATOR_ALLOW_INSECURE_HTTP` | A trimmed, case-insensitive `true` enables cleartext `http://` for `XSD_VALIDATOR_URL`; `1`, `t`, and `yes` do not. | `false` | No | ## Secrets | Variable | Description | Default | Required | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------- | | `FLOWKER_SECRETS_BACKEND` | Backend that resolves external integration credentials. `tenant-manager` selects the AWS Secrets Manager reader; unset disables secret resolution. Any other value stops the server from starting. | — | No | | `FLOWKER_SECRETS_CACHE_TTL_SEC` | TTL of the in-memory secrets cache (seconds) | `30` | No | | `FLOWKER_SECRETS_APPLICATION_NAME` | `applicationName` path segment used when reading credentials from the Tenant Manager store. It must match `WORKOS_TM_SERVICE_NAME` so the read path and the write path resolve the same service identity. | `flowker` | No | ## Tenant Manager token mint Flowker mints a platform-scoped bearer token to call the Tenant Manager write endpoints. Token minting requires `WORKOS_TM_TOKEN_URL`, `WORKOS_TM_CLIENT_ID`, and `WORKOS_TM_CLIENT_SECRET` together. `WORKOS_TM_SCOPE` is optional, but a non-blank value requires those three credential variables. `WORKOS_TM_SERVICE_NAME` only overrides the service name. Leave the token URL, client ID, client secret, and scope unset to disable minting. A partial configuration stops the server from starting. The Tenant Manager base URL comes from `MULTI_TENANT_URL`. | Variable | Description | Default | Required | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------------ | | `WORKOS_TM_TOKEN_URL` | OAuth 2.0 token endpoint that issues the platform-scoped bearer | — | Yes (all three, or none) | | `WORKOS_TM_CLIENT_ID` | Client id for the `client_credentials` grant | — | Yes (all three, or none) | | `WORKOS_TM_CLIENT_SECRET` | Client secret for the `client_credentials` grant. Inject it through your secret manager; it is never logged. | — | Yes (all three, or none) | | `WORKOS_TM_SCOPE` | Space-delimited scope requested with the grant. Setting it alone, without the three credentials, stops the server from starting. | — | No | | `WORKOS_TM_SERVICE_NAME` | Flowker's service name as the Tenant Manager knows it. It must match `FLOWKER_SECRETS_APPLICATION_NAME`. | `flowker` | No | ## Multi-tenant When `MULTI_TENANT_ENABLED=true`, database connections are resolved per tenant through Tenant Manager. When `false` (default), Flowker runs in single-tenant mode with static connections. | Variable | Description | Default | Required | | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------ | | `MULTI_TENANT_ENABLED` | Enable per-tenant connection resolution | `false` | No | | `MULTI_TENANT_URL` | Tenant Manager API URL | — | Yes (if `MULTI_TENANT_ENABLED=true`) | | `MULTI_TENANT_SERVICE_API_KEY` | API key for the Tenant Manager settings endpoint | — | Yes (if `MULTI_TENANT_ENABLED=true`) | | `MULTI_TENANT_ALLOW_INSECURE_HTTP` | Allow a cleartext `http://` Tenant Manager URL. Never enable in production — credentials travel in plaintext. | `false` | No | | `MULTI_TENANT_REDIS_HOST` | Redis host for tenant lifecycle events. In multi-tenant deployments, configure a reachable endpoint so cache invalidation, credential rotation, and connection updates reach Flowker. Omit it only when you accept no lifecycle-event delivery and a per-replica cache for compiled OpenAPI request validators. | — | No | | `MULTI_TENANT_REDIS_PORT` | Redis port | `6379` | No | | `MULTI_TENANT_REDIS_PASSWORD` | Redis password | — | No | | `MULTI_TENANT_REDIS_TLS` | Enable TLS for the Redis connection | `false` | No | | `MULTI_TENANT_MAX_TENANT_POOLS` | Soft limit on cached per-tenant connection pools. Unset leaves the pool count unlimited. Recommended: `100`. | — | No | | `MULTI_TENANT_IDLE_TIMEOUT_SEC` | How long a tenant pool must stay idle before it becomes eligible for eviction (seconds). Recommended: `300`. | — | No | | `MULTI_TENANT_TIMEOUT` | Tenant Manager request timeout (seconds). Unset leaves the HTTP client without a timeout. Recommended: `30`. | — | No | | `MULTI_TENANT_CACHE_TTL_SEC` | Tenant settings cache TTL (seconds). A positive value applies to the local tenant cache and Tenant Manager client cache. When unset or non-positive, their defaults are `43200` seconds (12 hours) and `3600` seconds (1 hour), respectively. Recommended: `120`. | `43200` local / `3600` Tenant Manager client | No | | `MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD` | Consecutive failures that open the circuit. One value governs both the per-provider-configuration breaker and the Tenant Manager HTTP client breaker. Unset leaves the per-provider-configuration breaker at 20 and the Tenant Manager breaker inactive, so set it explicitly in multi-tenant deployments. | `20` (per provider configuration) | No | | `MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC` | Circuit breaker recovery timeout (seconds) | `30` | No | ## Token cache Bounds for the shared OAuth 2.0 token cache used by provider authentication (OIDC and OAuth2 token-endpoint flows). | Variable | Description | Default | Required | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `TOKEN_CACHE_MAX_TTL_SEC` | Maximum time any cached token is trusted, regardless of the provider's `expires_in` | `3600` | No | | `TOKEN_CACHE_JANITOR_INTERVAL_SEC` | How often the background janitor evicts expired tokens (seconds) | `300` | No | | `TOKEN_CACHE_REFRESH_BUFFER_SEC` | How long before expiry a cached token is refreshed proactively (seconds) | `60` | No | | `TOKEN_CACHE_TENANT_SCOPED_DISABLED` | Opt out of tenant-scoped token caching. Tenant scoping is the default in multi-tenant mode; disabling it re-shares one cache across tenants. | `false` | No | ## Observability | Variable | Description | Default | Required | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------- | | `ENABLE_TELEMETRY` | Enable OpenTelemetry instrumentation | `false` | No | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP exporter endpoint | — | Yes (if `ENABLE_TELEMETRY=true`) | | `OTEL_RESOURCE_SERVICE_NAME` | Service name for telemetry. Recommended: `flowker`. | — | No | | `OTEL_RESOURCE_SERVICE_VERSION` | Service version for telemetry | — | No | | `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT` | Deployment environment label | — | No | | `OTEL_LIBRARY_NAME` | Instrumentation library name. The logger requires it on every boot, including when `ENABLE_TELEMETRY` is `false`. Recommended: `flowker`. | — | Yes | | `SKIP_LIB_COMMONS_TELEMETRY` | Skip commons library telemetry | `false` | No | | `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`, `dpanic`, `panic`, or `fatal`; case-insensitive). Unset, the level follows `ENV_NAME`: `development` and `local` log at `debug`, every other value logs at `info`. An unrecognized level stops the server from starting. | — | No | ## Security | Variable | Description | Default | Required | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------- | | `SSRF_ALLOW_PRIVATE` | Allow executor HTTP calls to private IPs. Only the exact string `true` enables it. Never enable in production. | `false` | No | | `ALLOW_INSECURE_TLS` | Skip the TLS requirement on the shared infrastructure clients and log a warning instead. Accepts `true`, `1`, `yes`, or `on`. Leave it unset in production so real TLS stays mandatory. | `false` | No | | `HTTP_MAX_BODY_BYTES` | Cap on the request body sent to and the response body read from outbound provider HTTP calls (bytes). A zero or negative value resolves to the default. | `10485760` (10 MiB) | No | | `FAULT_INJECTION_ENABLED` | Enable fault injection for testing | `false` | No | ## Swagger | Variable | Description | Default | Required | | ----------------- | --------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `SWAGGER_ENABLED` | Serve the generated OpenAPI spec and docs UI at `/openapi/openapi.json`, `/openapi/openapi.yaml`, and `/openapi/docs` | `false` | No | | `SWAGGER_TITLE` | Title of the docs UI page | — | No | # Getting started with Flowker Source: https://docs.lerian.studio/en/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. This page walks you through what Flowker can do for your business. For technical setup instructions, see the [Technical quickstart](/en/reference/flowker/flowker-api-quick-start). ## 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. Each execution is recorded, traceable, and available for inspection through the API. Here's the high-level flow: Browse the catalog to discover the providers, executors, and triggers available in your Flowker deployment. 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. Model your business process as a workflow: define the steps (nodes), the order they run (edges), and the conditions that control branching. 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. Here are common scenarios. ### 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, retry behavior is configured 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 or the provider documents and atomically enforces a stable business key sent in a header as an idempotency key on every attempt; otherwise, keep retries disabled for the node. Every executed executor, conditional, and action node is recorded for reconciliation and review. ### Anti-fraud validation **The challenge:** Fraud checks must happen before a transaction is processed — but they involve external services, scoring logic, and different thresholds depending on the transaction type. Hard-coding this logic creates maintenance nightmares. **How Flowker solves it:** Define a pre-transaction workflow that runs fraud checks before anything reaches the ledger: 1. **Trigger** — A new transaction is submitted. 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 is submitted. 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. When Access Manager authentication is enabled (`PLUGIN_AUTH_ENABLED=true`), Flowker API requests must carry a Bearer token, and each route enforces a per-resource, per-action permission. Provider-specific credentials (for external services) are managed through [provider configurations](/en/reference/flowker/list-provider-configurations). See the [Technical quickstart](/en/reference/flowker/flowker-api-quick-start) for the full bootstrapping flow. ## What's next *** Understand workflows, nodes, edges, and the execution model in detail. Set up Flowker locally and run your first workflow through the API. Deep dive into Flowker's architecture, features, and design philosophy. Explore the full API for workflows, executions, and configurations. # Flowker in Lerian Console Source: https://docs.lerian.studio/en/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. If you've been following the guides and want to move from API calls to a point-and-click experience, the Console is where that happens. ## 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 real-time status tracking and detailed result inspection No curl commands required. ## 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 in real time | 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 *** Full walkthrough of the Flowker module in Lerian Console — accessing the module, navigating the sidebar, and understanding the available pages. Monitor workflow activity at a glance with stat cards, recent workflows, and execution history. Create, edit, clone, activate, and delete workflows through the Console interface. Learn how to use the visual canvas editor — triggers, executor calls, conditionals, and connections. Configure and manage the provider integrations used by your workflows. If Flowker is not available in your Console sidebar, ask your platform administrator to check your organisation's Console setup. # Observability in Flowker Source: https://docs.lerian.studio/en/flowker/flowker-observability-guide Configure Flowker telemetry and use its health endpoints and execution records to operate workflows. Flowker can emit OpenTelemetry data for its HTTP and service operations. Telemetry is opt-in: enable it in the deployment, send it to your collector, and use the execution API for workflow-specific status and results. ## Enable and export telemetry *** Set these environment variables in the Flowker deployment: ```bash theme={null} ENABLE_TELEMETRY=true OTEL_EXPORTER_OTLP_ENDPOINT=https://your-otlp-collector.example:4317 ``` With telemetry enabled, Flowker initializes its telemetry instruments and exports through the configured OTLP endpoint. You do not add instrumentation to individual workflows, but the deployment must provide a reachable collector. Use an `https://` endpoint for a remote collector; reserve a plaintext `http://` endpoint for a collector that is local to the host or reachable only over an isolated network. Flowker does not provision Grafana dashboards, retention, or alert rules; those are decisions in your observability platform. ## Check service health *** Flowker exposes Kubernetes-compatible probes. `GET /health` is a liveness check: after its startup self-probe passes, it returns `200 healthy` and deliberately does not call the database, cache, or downstream services. `GET /readyz` is the readiness check for dependencies and is the endpoint to use when you need to know whether Flowker can serve requests now. ## Use your observability platform *** Route the OTLP stream to the backend your platform operates, such as Grafana, and build dashboards and alerts there. Keep those dashboards separate from execution investigation: API request telemetry does not replace the execution record. For an individual workflow run, use [Get execution results](/en/reference/flowker/get-execution-results) to read 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` | All steps finished successfully | 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. | If you see a significant number of `failed` executions in a short period, collect their execution ids and error details before you escalate. A pattern is more actionable than an isolated failure. ## When to involve engineering *** Escalate to engineering when: * `GET /readyz` reports a dependency as down * the OTLP collector is unreachable or is rejecting the telemetry stream * `failed` executions exceed the alert threshold your deployment defines and the cause is not clear * Flowker is not processing new executions while readiness is successful Share the execution ids, UTC timeframe, relevant API error payloads, and the `/readyz` response. If telemetry is enabled, include the collector or dashboard link as supporting evidence. # Integration guide Source: https://docs.lerian.studio/en/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, reference it from a workflow node, 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 a read-only registry of the providers, catalog executors, and triggers that ship with Flowker. You discover them; you never create them. Call the [List catalog providers](/en/reference/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. Call the [List catalog executors](/en/reference/flowker/list-catalog-executors) endpoint to see the operations a workflow node can invoke. Use [List executors by provider](/en/reference/flowker/list-executors-by-provider) to narrow the list to one provider. Call the [List catalog triggers](/en/reference/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. 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). Think of the catalog as a menu: it shows what Flowker can call. Provider configurations are your specific orders — 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/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/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. The secret inside the `auth` block is held in your secrets backend, not in the configuration document; everything else in the map is stored with the configuration. | | `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. | A `providerId` is a catalog identifier, and it does not always match the product name. Midaz is registered as `ledger`. Always take the value from [List catalog providers](/en/reference/flowker/list-catalog-providers) rather than guessing it from the product name. 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. A workflow that pairs a configuration of one provider with an executor of another is rejected 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. ```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` | Secret leaves in `config.auth` are stored outside the persisted configuration document. An authorized provider-configuration read can resolve those values from the vault and return them in clear; leaves that are not resolved remain masked. Grant read access accordingly. Anything else you place in the configuration document — a header, for example — is stored with the configuration, and a read can return it. Put each credential in `config.auth`. 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. For OAuth 2.0 integrations, use `oidc_client_credentials`. Flowker handles token acquisition and renewal automatically. ```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). They are created in `active` status. Use [Disable provider configuration](/en/reference/flowker/disable-provider-configuration) to take a connection out of service and [Enable provider configuration](/en/reference/flowker/enable-provider-configuration) to bring it back. See the [Provider configurations API](/en/reference/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, and 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 | The same request segment by name, used 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. | | `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 | 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/flowker/connecting-your-own-api) walks that whole path. ### Validate a node configuration before you save Call the [Validate a node configuration](/en/reference/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 *** When workflow data doesn't match the format an external service expects — or when a service returns data in a shape the next step can't consume — use field mappings and transformations to bridge the gap. Field mappings and transformations are defined 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`. ```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) and define Kazaam operations for advanced JSON-to-JSON transformations. [Working with request and response data](/en/flowker/working-with-request-and-response-data) walks the whole path: declaring the mappings, choosing what builds the request body, reshaping values in flight, 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** that receives the payment, an **executor** node that calls the fraud check service, a **conditional** node that evaluates the score, and 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/flowker/create-workflow) endpoint to define the workflow, then [Activate](/en/reference/flowker/activate-workflow) it, and finally [Execute](/en/reference/flowker/execute-workflow) it. ```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" } }, { "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 — and the FraudShield configuration created in 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). ```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 *** Workflow executions are triggered via the [Execute workflow](/en/reference/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 execution request must include an `Idempotency-Key` header. Requests without it are rejected 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 and external services send HTTP requests to that path. ### How it works 1. Add a trigger node of type `webhook` to your workflow with a `path`, a `method`, and an `input_contract` in its `data`. 2. When the workflow is activated, Flowker registers the path in its webhook registry. 3. External systems send requests to [`POST /v1/webhooks/{path}`](/en/reference/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 `input_contract`. [Configuring a webhook trigger](/en/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 is a closed contract. Saving a workflow whose webhook trigger omits `path`, `method` or `input_contract`, misses a field its `input_contract` mode requires, names another mode's schema id or operation field, or carries a key or a value the schema does not accept fails with `FLK-0934`. The schema also declares the optional `response_mode` and `response_view` fields — see [Synchronous response mode](#synchronous-response-mode). ### Securing a webhook Webhook delivery uses the same authentication as the rest of the API. With Access Manager enabled (`PLUGIN_AUTH_ENABLED=true`), every request to `/v1/webhooks/*` must carry a Bearer token (OIDC JWT), and the caller must hold the `execute` permission on the `webhooks` resource. Requests without a valid token are rejected with `401 Unauthorized`. 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 resolved webhook path. | | `_webhook.headers` | Request headers, filtered through a safe allowlist (`Content-Type`, `Accept`, `User-Agent`, `X-Request-Id`, `X-Forwarded-For`, `Idempotency-Key`). All other headers are dropped. | | `_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. ### Important notes * Each webhook path + method combination can only be registered by one active workflow. Activating a second workflow with the same path fails with a conflict error. * Webhook paths support nested segments (e.g., `payments/stripe/received`). * The request body maximum size is 1 MB. * Deactivating a workflow automatically unregisters its webhook routes. See the [Trigger a webhook](/en/reference/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 same `202` receipt (with a `Location` header pointing at the results endpoint) the async mode would have returned. `response_view` selects the shape of the sync response body: | Value | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `full` (default) | The complete execution dump — `executionId`, `workflowId`, `status`, `stepResults`, `finalOutput` — 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`) — 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. If neither applies (no captured provider response — circuit open, timeout, pre-dispatch failure — and no terminal output), it falls back to the full envelope at HTTP `200` so the caller still gets a meaningful outcome. | A failed execution's `finalOutput` (in `full` or `final_output` view) always 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 the terminal `set_output` node 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 is rejected at save time (`FLK-0122`). For `passthrough`, the override applies only when the `set_output` node itself is the terminal step — a terminal executor's relayed provider status always wins, and the no-response fallback always uses a plain `200` so an override never masks a failure. Passthrough detection is strict: only the terminal step counts. A `set_output` terminal downstream of an executor is shaped 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 a provider `4xx` that stopped the workflow is relayed 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 is marked as `failed`. There is no automatic fallback. After retries are exhausted, 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; `output` is optional. Do not promise an `errorCode`, including `FLK-0504` or `FLK-0507`, in every execution-results payload. ## 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, `POST` and `PATCH` calls are treated as non-idempotent and are **not** retried (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: circuit breaker open, context cancelled, configuration errors, secret-resolution failures, a request body over the configured size cap, a provider response body over the same cap, and non-transient `4xx` provider responses (any `4xx` except `408` and `429`). The retry applies per node execution. If all attempts fail, the step is marked as failed and the execution stops. ### Circuit breaker Flowker uses a circuit breaker to protect external services from being overwhelmed by repeated failing calls: | Parameter | Value | | ------------------ | ---------------------------------------------------------------------- | | Failure threshold | 20 consecutive failures opens the circuit (configurable at deployment) | | Recovery timeout | 30 seconds before trying again (half-open state) | | Half-open requests | 1 request allowed to test if the service recovered | Provider `4xx` client/auth errors do **not** trip the circuit: they are the caller's problem, not a sign the provider is down. Only transport-level and `5xx` failures count toward the threshold. 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 The circuit starts in the **Closed** state, where all requests pass through normally. After the failure threshold is reached, it transitions to **Open**, blocking 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. 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 defaults configured at deployment — they cannot be customized per connection in this version. ## Executor configuration registry *** This registry is a third, separate use of the word "executor": its records are not the catalog executors of [Step 1](#step-1-explore-the-catalog), not the workflow nodes of `type: "executor"`, and not the provider configurations of [Step 2](#step-2-create-a-provider-configuration). The engine reads provider configurations to call external services, not these records, and the registry carries its own field vocabulary (`baseUrl`, `endpoints`, `authentication`). The registry exposes four operations: | Operation | Endpoint | | --------- | ---------------------------------------------------------------------------------- | | List | [`GET /v1/executors`](/en/reference/flowker/list-executor-configurations) | | Get | [`GET /v1/executors/{id}`](/en/reference/flowker/get-executor-configuration) | | Update | [`PATCH /v1/executors/{id}`](/en/reference/flowker/update-executor-configuration) | | Delete | [`DELETE /v1/executors/{id}`](/en/reference/flowker/delete-executor-configuration) | Every record carries a `status`, which the API reports in each response: | Status | Description | | -------------- | ----------------------------------------- | | `unconfigured` | The record has no connection details yet. | | `configured` | The record carries connection details. | | `tested` | The record was verified. | | `active` | The record is in service. | | `disabled` | The record is out of service. | `PATCH` accepts `name`, `baseUrl`, `endpoints`, and `authentication`, plus the optional `description` and `metadata`. It does not accept `status`, but the list operation accepts `status` as a query filter. Update applies to records in `unconfigured` or `configured` status; delete applies to records in `unconfigured`, `configured`, or `disabled` status. No operation in this version moves a record into `tested`, `active`, or `disabled`; the table lists those values because responses report them and the list filter accepts them. ## What's next *** Understand workflows, nodes, edges, and executions. Explore the provider configuration API. # Running a workflow on a schedule Source: https://docs.lerian.studio/en/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 every firing as an occurrence, so a firing that could not happen on time is not lost: it 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 [Getting started with Flowker](/en/reference/flowker/flowker-api-quick-start) for the create-and-activate path. * The worker binary running with the scheduler enabled. `SCHEDULER_ENABLED` resolves to `true` unless you set it to `false`, and the queue needs `SCHEDULER_REDIS_HOST`: with no host the worker binary does not start and no scheduled workflow fires. Check that variable first when your schedules never fire. See [Scheduler variables](/en/flowker/flowker-environment-variables#scheduler). * 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 are built in. You discover them in the catalog, and you never create one. [Get a catalog trigger](/en/reference/flowker/get-catalog-trigger) returns the schedule trigger's JSON Schema from the running instance: ```bash theme={null} curl -s http://localhost:4021/v1/catalog/triggers/schedule | 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 or unresolvable timezone is also evaluated as `UTC`. | | `enabled` | Optional | `false` stops the schedule from firing while the workflow stays active. Defaults to `true`. | ### 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 — `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/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 *** Send the node with the rest of your workflow to [Create a workflow](/en/reference/flowker/create-workflow), or to [Update a workflow](/en/reference/flowker/update-workflow) if the draft already exists. Flowker validates the cron here. [List upcoming scheduled occurrences](/en/reference/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 "http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/upcoming?limit=3" | 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`. Call [Activate a workflow](/en/reference/flowker/activate-workflow). The leader-gated producer sweeps on a 60-second default interval. After a successful sweep, Flowker records and queues the next occurrence for its slot. ```bash theme={null} curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/activate | 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: the service was down, the queue was behind, the process restarted. Flowker never runs a parked occurrence on its own — it holds it for your decision, in the `pending-review` state. 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 — not one entry for every slot that went by during an outage — and 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. A skipped occurrence is not run again by the scheduler and carries a `skipReason`; 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. One thing timing does decide: 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: [List parked scheduled occurrences](/en/reference/flowker/list-parked-scheduled-occurrences) returns them oldest first. ```bash theme={null} curl -s http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed | 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`; it can be smaller than the parked list because that list also includes `missed` occurrences. `missed` is the short-lived state a late slot holds while the engine parks it as `pending-review`; the run and discard endpoints do not accept it, 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. [List skipped scheduled occurrences](/en/reference/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 "http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/skipped?limit=10" | 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. [Count pending-review occurrences](/en/reference/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 http://localhost:4021/v1/workflows/schedule/pending-review-counts | 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 one is rejected 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/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. `active-run` means another run of the workflow was still going. `workflow-gone` means the workflow was not active when your run reached the engine. `execution-duplicate` means the work for that slot had already run. After your run, a skipped occurrence is not run again by the scheduler, 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. [Run a parked occurrence](/en/reference/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 by default; actual start time depends on worker availability and queue capacity. ```bash theme={null} curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30/run | 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. [Discard a parked occurrence](/en/reference/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 http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30/discard | jq '{id, status}' ``` ```json theme={null} { "id": "019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30", "status": "discarded" } ``` [Discard all parked occurrences](/en/reference/flowker/discard-all-parked-occurrences) discards, in a single write, every occurrence of that workflow that is pending review, and reports how many it moved. ```bash theme={null} curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/discard-all | jq . ``` ```json theme={null} { "discarded": 7 } ``` With nothing pending review it answers `200` with `"discarded": 0`, so a repeat call is safe. 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, that are pending review. It leaves the schedule itself running, leaves the upcoming occurrences alone, and 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/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/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/flowker/deactivate-workflow). Flowker stops recording new occurrences for it. 2. [Move it to draft](/en/reference/flowker/move-workflow-to-draft). 3. [Update the workflow](/en/reference/flowker/update-workflow) with the new `cron`, `timezone`, or `enabled` value. 4. [Activate it](/en/reference/flowker/activate-workflow). The leader-gated producer sweeps on a 60-second default interval. 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, and the parked occurrences survive all four steps: they stay listed and stay actionable once the workflow is active again. 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-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. | Two failures answer no error code: * **Upcoming occurrences are listed, but nothing ever runs.** The API computes the cadence on its own, while the worker binary is what fires it. Confirm the worker is running and that `SCHEDULER_REDIS_HOST` is set. See [Scheduler variables](/en/flowker/flowker-environment-variables#scheduler). * **Nothing new is recorded for an active workflow.** Check `enabled` on the trigger node: `false` keeps the workflow active and its schedule quiet. ## What's next *** Start the same workflow from an inbound HTTP call instead of a cadence. Build the rest of the graph the trigger enters. # Security Source: https://docs.lerian.studio/en/flowker/security Protect Flowker workflows, data, and integrations with Access Manager authentication, per-connection credentials, and deployment-managed TLS. Flowker protects your workflows, data, and integrations through Access Manager authentication and per-connection credential management. TLS for API traffic is managed by your deployment. This page covers the security model as implemented in the current release. ## Platform authentication *** Flowker delegates platform authentication to **Access Manager**, enabled with `PLUGIN_AUTH_ENABLED`. When enabled, 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 is applied. Enable Access Manager in production. ```bash theme={null} curl -X GET https://your-flowker-instance/v1/workflows \ -H "Authorization: Bearer " ``` **How it works:** * **Access Manager enabled** — each request to a protected API route carries a Bearer token, and every protected route enforces a per-resource, per-action permission. * **Access Manager disabled** — endpoints do not require authentication. The identity from a Bearer token, when present, is still read on a best-effort basis, so the request is attributed to the claimed subject. Use this mode only for local development. * Invalid or missing credentials return `401 Unauthorized`. **Health probe exception:** Liveness and readiness probes are excluded from authentication. They are designed for infrastructure monitoring (Kubernetes probes, load balancers) and do not expose sensitive data. ## 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 are managed separately. **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. Secret leaves in `config.auth` — an API key, bearer token, password, client secret, or HMAC secret — are sent to the secrets backend and removed from the persisted configuration. When secret read-back is configured, an authorized provider-configuration read can resolve them for display. Restrict that permission and treat its response as sensitive. Anything else you place in the configuration document — a header, for example — is stored with the configuration, and a read can return it. Put each credential in `config.auth`. 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..." } } } } ``` 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:** * Configure TLS termination for Flowker API traffic in your deployment. * 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 supports configurable CORS settings: * Allowed origins are configurable per deployment * Credentials are not allowed in cross-origin requests (`AllowCredentials` is disabled) * Preflight responses are cached for performance ## 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 — preventing your workflows from hanging on an unresponsive provider. * Transitions through `closed` → `open` → `half-open` states * The circuit is scoped per provider configuration and per tenant, so failures against one connection do not affect another * Thresholds are configured globally (consecutive failures before opening) * 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, `POST` and `PATCH` are treated as non-idempotent and get a single attempt. `GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE`, and any other verb retry, with 3 total attempts by default. 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, a cancelled execution, a request body over the configured size cap, and a response body over the same cap also stop the loop. 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 *** Learn how to create provider configurations and connect external services. Monitor Flowker with traces, metrics, and structured logs. # Use cases Source: https://docs.lerian.studio/en/flowker/use-cases Common financial scenarios where Flowker orchestrates validation, provider calls, and data routing. Flowker is designed for the operational realities 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 from day one** — 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. Each service instance is configured once as a provider configuration that holds its base URL and credentials, and 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 (`POST` and `PATCH` calls run 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** — transient errors are retried 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 are bolted on 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, and 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. Catalog provider configurations are validated 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 *** Set up Flowker and run your first workflow end to end. Learn how to create provider configurations for your workflows. # What is Flowker? Source: https://docs.lerian.studio/en/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 tracked, traceable, and repeatable. For the full business case, see [Why Flowker?](/en/flowker/why-flowker). ## Core concepts *** At its core, Flowker works with two concepts: * **Workflows**: The overall process — a sequence of steps that define what happens for a given operation (e.g., "process a Pix payment" or "onboard a new customer"). Workflows can branch, and retry policy can retry eligible executor-call failures. * **Tasks**: The individual steps within a workflow — such as checking a fraud score, calling an external approval system, calling another external service, or invoking a ledger integration. In Flowker's API, tasks are represented as *nodes*. Both terms refer to the same concept and are used interchangeably throughout this documentation. 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 Unlike generic workflow tools, Flowker is modeled specifically 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. ### Bounded contexts by design The platform is structured around clear **bounded contexts**, separating concerns such as onboarding, payments, settlement, risk, and reconciliation. This segmentation improves scalability, governance, and domain clarity across complex financial operations. ### 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 Every step inside a workflow records its name, status, duration, output, and error message. This gives you a step-by-step view of what each execution did, available through the API and in Lerian Console. ### End-to-end observability Native support for **distributed tracing, metrics, and structured logging**, enabling real-time monitoring across workflows and external interactions. ### Enterprise-grade security Token-based authentication through Access Manager, with per-resource, per-action authorization. Configure TLS termination and encrypted links in your deployment. ### Developer-first experience Flowker includes OpenAPI definitions and a local development environment with Docker — enabling teams to build, test, and deploy flows with confidence and speed. ## Key features *** ### Workflow orchestration * Run tasks sequentially. * 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. ### DSL engine * Define workflows with a domain-specific language (DSL). * Validate workflow definitions before deployment. * Transform data between steps. ### Authentication and security * Token-based authentication through Access Manager, with per-resource, per-action authorization. * Structured logs. Configure encryption at rest in the deployment services that store data and secrets. ### Observability Flowker emits telemetry through **OpenTelemetry/OTLP** and records structured logs. Connect your deployment's telemetry backend to monitor workflow and external interactions. You can trace each workflow execution, monitor metrics, and review detailed logs for debugging and compliance. ## 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 is divided into three main categories: **Authoring**, **Runtime**, and **Administrative**. Each category has a clear purpose to help developers build, execute, and manage workflows. * **Authoring routes**: create, update, and manage workflow definitions and versions. * **Runtime routes**: start, monitor, and control workflow executions in real time. * **Administrative routes**: manage tenants, providers, and system health. ## Current status *** For additional information about specific integrations or implementation details, [contact us](https://lerian.studio/contact). [Explore the Flowker API](/en/reference/flowker/flowker-api-quick-start). Browse endpoints for workflows, executions, and provider configurations. # Why Flowker? Source: https://docs.lerian.studio/en/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 *** If you work in financial services, these challenges are probably familiar: * **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 These are common integration and compliance concerns for financial engineering teams. ## How Flowker changes this *** Flowker provides one orchestration platform for workflow routing, provider configuration, mappings, and execution records. Here is the operating model: | | Before Flowker | With Flowker | | ------------------------ | ------------------------------------ | ----------------------------------------------------------------------------------- | | **Provider integration** | Provider-specific integration work | Centralize provider configuration and node mappings | | **Step traceability** | Partial, scattered across systems | Every workflow step 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, and they need to do it under audit pressure with tight deadlines. Flowker gives compliance teams: * **Step-level traceability** — every workflow execution records each step with its timestamp, 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 Flowker records execution steps automatically. There is nothing to enable or configure. ## 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 automatically * **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 dashboards provide visibility into workflow success rates, failure points, and provider performance ## What's next *** See how Flowker applies to transaction validation, onboarding, payments, and more. Set up Flowker and run your first workflow end to end. # Workflow Design Guide Source: https://docs.lerian.studio/en/flowker/workflow-design-guide Design Flowker workflows with confidence — node types, edges, real-world patterns, status transitions, and best practices for reliable orchestration. Design workflows in Flowker with clarity and control. This guide walks through node types, edges, real-world patterns, status transitions, technical limits, and best practices to help you build reliable and maintainable orchestrations. ## Node types *** Every workflow is built from 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. When an execution starts through a trigger, the engine enters at that node and routes from it. 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/flowker/configuring-a-webhook-trigger) or [Running a workflow on a schedule](/en/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 is the core building block for integrating with 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/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/flowker/console/canvas-editor). ```json theme={null} { "id": "node-risk-decision", "type": "conditional", "name": "Evaluate Risk Score", "data": { "condition": "node-fraud-check.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. It is not evaluated 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. Understanding these transitions is key to safely deploying and evolving workflows. 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. * **draft** — The initial state. All modifications (adding nodes, editing edges, changing configuration) are only allowed in `draft` status. * **active** — A workflow that has been activated. It can be executed. No modifications are permitted while active. * **inactive** — A workflow that has been deactivated. It can no longer be executed, but it can be moved back to `draft` for editing. ### Rules * Only a `draft` workflow can be activated (transition: `draft → active`). * Only an `active` workflow can be deactivated (transition: `active → inactive`). * Only an `inactive` workflow can be moved 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/flowker/move-workflow-to-draft). This makes the workflow editable without needing to clone it. This is useful when you deactivated a workflow by mistake or when you want to iterate on an existing workflow instead of creating a copy. Only inactive workflows can be moved to draft. If you need to modify an active workflow without taking it offline, 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, test, and activate it without impacting the current version. This is the recommended 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` | Keep these limits in mind when designing complex flows. 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 no branching is required. 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. **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 which outgoing edge is followed, 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. **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.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, a fraud score is retrieved, and execution is routed 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.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 A submission is sent 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 — there is 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.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, making debugging significantly faster. ### Condition expressions Free-text conditions on conditional nodes are evaluated against the execution context at runtime. Keep them simple and explicit: * Use direct field comparisons: `.status == 'approved'` * Use numeric comparisons: `.riskScore < 70` * Use boolean fields: `.reviewRequired == true` * Combine with `AND` / `OR` when needed: `.score < 70 AND .verified == true` Avoid complex expressions that are hard to read or debug. If logic is non-trivial, give the `conditional` node a clear name that encapsulates the decision. A condition that is missing or fails to evaluate at runtime fails the execution (`FLK-0105` identifies an invalid conditional expression). 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. If a cycle is detected 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 a retry or loop is needed, 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: Clone the workflow (creates a new `draft` with all nodes and edges copied). Make your changes in the draft. Validate or preview the draft. Activate it before running execution tests. Activate the new version. Deactivate the old version if it is no longer needed. 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-0105` | Invalid conditional expression | | `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/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: a list of source-to-target entries on the node, applied 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 — a document that must travel without punctuation, an amount that belongs under a nested object, 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/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/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. An active workflow is locked, so use [Move workflow to draft](/en/reference/flowker/move-workflow-to-draft) before you edit a node, then activate it again. 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/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/flowker/integration-guide#webhook-metadata). | | `execution.id` | The execution identifier. | | `execution.startedAt` | When the execution started, in UTC. | | `` | The output of each 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. | A mapping `source` is a plain path. Do not wrap it in `${...}` — braces belong to the node's template fields (`body`, `headers`, `query`, `path`), and inside a mapping a `${...}` string is read 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. ```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.00, "document": "12345678900" } } ``` ### When a source selects nothing A source path that is absent from the context is not an error. The target is still written, 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 stops with `FLK-0504` and the step reports `input transformation failed`. `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: 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 is made — 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. 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. 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 — so one node can 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`, `allowedHosts`, and `allowedPrivateHosts`. A node that keeps transport in `config` by mistake therefore sends none of it to the destination, and 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`, and `request_format`. 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. ```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.00 } } ``` ## 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. | — | These five are the whole set. A `type` outside it is rejected when you save the workflow, with `FLK-0140`. Two rules to write them by: * They act on text. A value that is not text reaches the target unchanged. * `prefix` and `suffix` each need at least one character, and a single space counts. `characters` 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 with `FLK-0504`. ```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`. ```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.00 }, "channel": "web" } ``` An operation may set `require: true` to demand that every path its `spec` names exists, the same way `required` works on a mapping entry. `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` 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 } ``` Downstream nodes then read `${score-transaction.score}` and `${score-transaction.httpStatus}`. 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: the target is stored 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/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; it masks supplied secret values and uses Flowker's catalog to resolve the provider and executor. No credential appears anywhere in what it returns, including the `curl`. 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 or vault read; the server-side catalog must still resolve the provider and executor: ```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 — and no call reached the service. Read it in this order: 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. Every `target` should appear where the service wants it. A field carrying `null` is a `source` path that selects nothing. It lists the `${...}` references in the node's template fields that your sample payload did not resolve. They are left literal in the rendered request. An empty array means every reference found a value. To check a node's fixed fields against the catalog executor's schema as well, call [Validate a node configuration](/en/reference/flowker/validate-executor-config) with 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 `${.}`. 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 a `transformation.type` outside the five supported types. | | `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. | | `FLK-0504` | Run time | The node failed. The step message names the stage: `input transformation failed` for a required source that is absent or a transformation that could not run, `output transformation failed` for the response side. | See the [Flowker error list](/en/reference/flowker/flowker-error-list) for every code. ## What's next *** Create the provider configuration a node calls through, and set its authentication. Choose the payload contract that fills the `workflow` namespace your mappings read. # Accounting and accrual runs Source: https://docs.lerian.studio/en/lender/accounting-and-accrual-runs How Lender books to the ledger — accounting profiles and posting rules, durable posting intents, accrual runs, and journal references. Disbursement and interest accrual reach the ledger, and so does a prepayment that settles a Brazilian loan account against a prepayment quote. Lender's accounting layer is how they get there: a product declares its posting rules once, and 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 legs that fire for each kind of financial event — and the ledger organization and ledger the resulting transactions book into. You create a profile per product: `POST /api/v1/loan-products/{id}/accounting-profiles` Posting rules are validated when the profile is created, so a product 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, it persists a durable **posting intent** in the same database transaction that changes domain state, then a relay posts the balanced transaction to Midaz asynchronously. This is what makes 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, the post is refused rather than written to the wrong place. The full path is described in [Lender in the platform](/en/lender/lender-in-the-platform). ## Accrual runs *** An **accrual run** recognizes interest and the other time-based amounts. One run takes each loan whose anniversary falls on the run's business date, and produces the postings for it. `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. Recognition is idempotent per loan account, per accounting month, and per amount kind. A run started twice for the same month recognizes interest once. ## 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. | 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 *** See how the active jurisdiction shapes disclosures and endpoints. Layer CET, IOF, and PDD staging on top of the accounting model. # Brazil regulatory pack Source: https://docs.lerian.studio/en/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** layers regulated behavior on top of Lender's generic lending baseline. Everything here is reached through the BR-prefixed endpoints (`/api/v1/br/...`) and applies to loans originated under the Brazil jurisdiction. Portuguese domain terms are kept as-is, with a gloss — see [Core concepts](/en/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` ## 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, and the server canonicalizes the text, recomputes the hash, and rejects any mismatch — so the recorded consent is 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, and stage changes are tracked transitions rather than silent overwrites. | 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. | How many days a loan is overdue is what drives staging. The later stages also suspend interest accrual, so the [accrual run](/en/lender/accounting-and-accrual-runs) skips those accounts. ## 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, so the contract and its audit trail stay explicit about what was refused. ## Business calendar *** Brazilian date math (accruals, schedule dates, settlement) uses a Brazilian business calendar with ANBIMA holidays and an Actual/252 day-count convention, so installment and accrual dates land on business days. ## BR products *** Brazilian products are created and versioned 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` ## Next steps *** The flagship Brazilian journey — payroll-deducted lending end to end. # Configuration and deploy Source: https://docs.lerian.studio/en/lender/configuration-and-deploy How Lender is packaged and run — the container shape, its dependencies, a curated environment-variable surface, multi-tenant modes, 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 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 and schema-per-tenant isolation in multi-tenant mode. | | **Valkey 8** (Redis-compatible) | Cache, rate limiting, and the M2M-credential cache. | | **RabbitMQ** | Event publisher transport, driven by a transactional **outbox** dispatcher. | | **RedPanda** | Streaming backbone for the lifecycle event catalog (via Lerian's streaming library). | | **Midaz** | The double-entry ledger, reached through the official SDK. Lender authenticates via [Access Manager](/en/platform/access-manager/access-manager) with per-tenant machine-to-machine (M2M) credentials, retrieved from AWS Secrets Manager and cached in Valkey. | | **Casdoor** | Identity provider backing route authorization (via Lerian's auth library). | | **Systemplane** | Runtime configuration plane, mounted under `/api/v1/systemplane`. | ## Configuration surface *** Configuration is entirely environment-driven. The keys below are a curated subset — the ones that shape behavior — 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 | `MULTI_TENANT_ENABLED`, `DEFAULT_TENANT_ID` | Switch between single- and multi-tenant modes, and the tenant used when single-tenant. | | Tenancy (MT) | `MULTI_TENANT_MAX_TENANT_POOLS`, `MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD` | Per-tenant connection-pool ceiling and circuit-breaker trip point. | | Postgres | `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_NAME`, `POSTGRES_SSLMODE`, `MIGRATIONS_PATH` | Primary database connection and migrations. | | 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` | RabbitMQ transport and the outbox dispatcher cadence. The PostgreSQL outbox is always initialized; it has no enable flag. | | Streaming | `STREAMING_ENABLED`, `STREAMING_BROKERS`, `STREAMING_TLS_ENABLED` | Event streaming toggle, broker addresses, and TLS. | | Auth | `PLUGIN_AUTH_ENABLED`, `AUTH_M2M_INVERSION_ENABLED` | Route authorization 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`. Set `false` only to intentionally unmount the contracting path; existing-contract servicing remains available. When you disable it, also keep `CONSUMER_CONSIGNADO_AVERBACAO_CONFIRMED_ENABLED` and `CONSUMER_CONSIGNADO_AVERBACAO_REJECTED_ENABLED` off; startup rejects that contradictory configuration. See [Consignado privado](/en/lender/consignado-privado). | | Ledger M2M | `AWS_REGION` | Region for the Secrets Manager holding per-tenant Midaz credentials. | | 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`. | ## Multi-tenant modes *** Lender runs in one of two modes: * **Single-tenant** — one tenant (`DEFAULT_TENANT_ID`), one database schema, and a construction-time ledger organization and ledger used as the posting default. * **Multi-tenant** (`MULTI_TENANT_ENABLED=true`) — **schema-per-tenant** persistence, a per-tenant Midaz client pool, and tenant-scoped events and postings. In this mode there is no env ledger default: the ledger target comes per transaction from the product's accounting profile. ## 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/lender/lender-in-the-platform). The operationally important property: **routing fails closed**. A posting books into the ledger organization and ledger resolved from the accounting profile (single-tenant 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. Under multi-tenant mode, a product with no accounting profile therefore cannot post — which is the intended safeguard. ## Next steps *** The posting path, event catalog, and tenant isolation in detail. # Consignado privado Source: https://docs.lerian.studio/en/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/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 is 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 averbação lifecycle *** Because averbação happens at an external payroll rail, Lender models it as an asynchronous conversation over the streaming backbone rather than a synchronous call. Lender publishes its commands on its own `lender.*` topics, and the payroll-deduction gateway reports facts back on `consignado-gw.*` topics. **Subscribe by topic, and publish facts on the gateway topic.** The topic is the address on both sides. | Topic | Direction | Meaning | | ------------------------------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------- | | `lender.consignado_exclusao.requested` | out | Ask the rail to remove a registered deduction. | | `lender.consignado_redirecionamento.requested` | out | Ask the rail to redirect a contract to a replacement vínculo. | | `consignado-gw.consignado_averbacao.confirmed` / `consignado-gw.consignado_averbacao.rejected` | in | The rail confirmed or refused the averbação. | | `consignado-gw.consignado_exclusao.confirmed` / `consignado-gw.consignado_exclusao.rejected` | in | The rail confirmed or refused the removal. | | `consignado-gw.consignado_employment_status.reported` | in | The rail reported a worker's vínculo was suspended or terminated. | | `consignado-gw.consignado_redirecionamento.confirmed` / `consignado-gw.consignado_redirecionamento.rejected` | in | The rail confirmed or refused the redirection. | | `consignado-gw.consignado_reconciliation.received` | in | A reconciliation verdict arrived — payroll escrituração, CEF repasse, portal CSV, or guarantee recovery. | The core catalog Lender publishes around these lives in [Lender events](/en/lender/lender-events). ## Contracting gate *** `CONSIGNADO_ENABLED` defaults to `true`. Set it to `false` only to intentionally remove the contratação route, its CCB signer, and the inbound confirm/reject averbação use cases. Servicing for existing contracts remains available. When you disable it, also keep `CONSUMER_CONSIGNADO_AVERBACAO_CONFIRMED_ENABLED` and `CONSUMER_CONSIGNADO_AVERBACAO_REJECTED_ENABLED` off; startup rejects that contradictory configuration. ## Optional: reconciliation with Matcher *** This integration is **off by default** and gated by configuration. Document and enable it only when your deployment has explicitly turned it on. Consignado contracts can optionally be reconciled against [Matcher](/en/matcher/what-is-matcher). When enabled, a Matcher reconciliation verdict on a contract is translated into a downstream effect on the loan — a PDD stage transition and a corresponding ledger posting intent. With the integration disabled, the reconciliation consumer is a no-op. ## Next steps *** CET, IOF, capitalization consent, PDD staging, and the rest of the BR profile. The wire contract, the topics Lender publishes, and how a handler stays safe. # Core concepts Source: https://docs.lerian.studio/en/lender/core-concepts Learn Lender's vocabulary: loan products and versions, accounting profiles, applications, loan accounts, schedules, accrual runs, and Brazil-specific terms. Lender uses a precise vocabulary. 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 is originated from — 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 was created under. 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. Charges are defined on the product and applied 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. It declares the posting rules — which double-entry legs fire for each accounting event (disbursement, prepayment, accrual) and which ledger organization and ledger the transaction books into. No accounting profile, no posting: the profile is how a product knows how to keep its books. ## 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 before it is decided. | | **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. (The read surface that lists and creates these is also called **servicing**.) ### Schedule and installment The **schedule** is the ordered set of **installments** a loan account owes — each with 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 "this financial event must book to the ledger." Lender persists the intent as part of the same transaction that changes domain state, then hands it 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 the accounting identifier an accrual run records for itself. 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** baseline profile and a deep **Brazil (BR)** profile; the active jurisdiction shapes which disclosures and endpoints apply. See [Jurisdictions](/en/lender/jurisdictions). ## Brazil terms *** The Brazilian profile adds regulated concepts. The Portuguese terms are kept as-is, with a gloss — see the [Brazil regulatory pack](/en/lender/brazil-regulatory-pack) for how each is used. | 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 *** See how these concepts book to the ledger and emit events. Put the product-catalog concepts to work. # Define a loan product Source: https://docs.lerian.studio/en/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 is originated 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 disbursement 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/lender/jurisdictions) the product belongs to — the generic baseline or Brazil. Brazilian products carry extra fields covered in the [Brazil regulatory pack](/en/lender/brazil-regulatory-pack). ## Steps *** `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.) `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 was originated under; you never edit a version, you publish a new one. `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. `POST /api/v1/loan-products/{id}/accounting-profiles` declares how the product books to the ledger: the posting rules (which double-entry legs fire for each event) and the ledger organization and ledger they book into. Lender refuses to disburse a loan whose version has no profile, so bind it before you originate. See [Accounting and accrual runs](/en/lender/accounting-and-accrual-runs). `POST /api/v1/loan-products/{id}/activate` sets the product to active and pins the version you name as its current version. Each application names the version it is originated under, so existing loans keep the version they started on. ## 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. | 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 streaming backbone — see [Lender in the platform](/en/lender/lender-in-the-platform). ## Next steps *** Take an active product and originate a loan against it. # How accrual works Source: https://docs.lerian.studio/en/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 month per loan, on the loan's own anniversary *** A loan's accounting month is its **competência**. The disbursement date anchors it, not the calendar. A loan disbursed on the 12th closes each competência on the 12th of the months that follow. A disbursement late in the month clamps onto the last day of a shorter month. The **business date** you pass to a run selects the competência. A loan recognizes only when the business date is one of its own anniversaries. A run on the 12th therefore recognizes the loans with a 12th disbursement date, and passes over the rest. Plan the calendar around this. To cover a whole book across a month, start a run on every business date. ## 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. The scheduled driver sends `monthly`. 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 scans up to 10,000 loan accounts, so scope a larger book by product and start more than one run. 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. 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. 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, and because Lender derives that id from the mode, the business date, and the products in scope, a read by correlation id returns the most recent run that shares it. [Accounting and accrual runs](/en/lender/accounting-and-accrual-runs) covers the two operations that read it back. ## The run does not write the ledger entry *** This boundary matters. 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. 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. * The outbox is always initialized. Configure the connection to the ledger so the relay can deliver the durable intent. See [Configuration and deploy](/en/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/lender/brazil-regulatory-pack). * The business date is not one of its anniversaries. * 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. It stays off until you enable it, and you set its cron expression, which defaults to the first day of each month. Give it a daily expression: each loan recognizes on its own anniversary, so only a daily driver covers the whole book over a month. Under multi-tenancy the driver runs once for each active tenant, against that tenant's own data. 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 profiles, posting rules, and the journal-reference operations. The five domains, the jurisdiction seam, and the outbox that carries money out. Products, versions, and the accounting profile a run depends on. Provisioning stages, taxes, and the disclosures the Brazilian profile adds. # How origination works Source: https://docs.lerian.studio/en/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 on the generic `XX` profile. A Brazilian regulated loan originates through the Brazil pack rather than through `POST /api/v1/loan-applications`. Read the [Brazil regulatory pack](/en/lender/brazil-regulatory-pack) for that 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. Bind an **accounting profile** to that version before you disburse. The disbursement builds its posting from the profile's legs, and a disbursement without a profile fails. See [Define a loan product](/en/lender/define-a-loan-product). ## Preview, if you want it *** Schedule preview computes the installments and the cost disclosure for prospective terms. It creates nothing and it changes nothing. 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, the requested principal, the requested **monthly** interest rate, the number of installments, and an expected disbursement date. Lender accepts up to 600 installments. One field is not in the body: 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 the generic profile, 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 is a limit, not a payment. 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. The schedule, the transactions, the charges, and the audit trail are all addressed by it. * 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 | Gross must not exceed the approved amount. | | Total | Gross across every tranche must stay within the approved amount. | | Chronology | The disbursement must not predate 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. | The balance rule is the one that surprises integrators. Under the generic profile there are no withholdings, so **net equals gross**. A lower net leaves the posting unbalanced and Lender rejects the disbursement. ## One transaction, four results *** A disbursement is a single database transaction. Four things happen inside it. Lender appends a disbursement event that records the amounts, the date, and the actor who disbursed. 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`. Under the generic profile the pipeline computes nothing, so it adds no withholding to the disbursement. Lender writes a balanced posting intent to the outbox: principal debited at gross, cash credited at net, and one credit for each withholding. 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 streaming backbone for each transition, when streaming is enabled and a broker is configured. Continue in [Service a loan](/en/lender/service-a-loan). ## Next steps *** Record repayments, prepay, reschedule, and correct an active loan account. The domains, the jurisdiction seam, and the outbox that carries money out. Posting rules, accrual runs, and the journal reference that ties a booking back. IOF, CET, capitalization consent, and the rest of the Brazilian profile. # Jurisdictions Source: https://docs.lerian.studio/en/lender/jurisdictions How Lender stays jurisdiction-aware — the profile registry, the generic baseline, the Brazil profile, and the discovery endpoints that expose them. Credit rules differ by market. Rather than hard-code one country's rules, Lender is **jurisdiction-aware**: it carries a registry of jurisdiction **profiles**, each supplying the rules, disclosures, and calendar its market needs, and the active jurisdiction shapes which behavior applies. ## The profile model *** * **Generic** — a baseline profile with the market-neutral lending behavior every jurisdiction shares. It is the fallback when no country-specific profile applies. * **Brazil (BR)** — a deep profile layered on top of the baseline, adding CET disclosure, IOF preview, capitalization consent, PDD staging, the credit-operation descriptor, prepayment quotes, a Brazilian business calendar, and the consignado privado journey. Brazilian behavior is reached through BR-prefixed endpoints (`/api/v1/br/...`), several of which are jurisdiction-aware aliases of the generic operations. See the [Brazil regulatory pack](/en/lender/brazil-regulatory-pack) and [Consignado privado](/en/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. | | `GET /api/v1/jurisdictions/{code}` | Read one jurisdiction profile by its code. | Profiles are part of the deployed service, not runtime configuration — discovery tells you what a given deployment can do, not something you edit through the API. ## Next steps *** Everything the Brazilian profile adds. # Lender architecture Source: https://docs.lerian.studio/en/lender/lender-architecture The parts Lender is built from: five 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, five 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. Those three facts explain most of what follows. ## Five 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. | 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 today: **BR** for Brazil, and **XX**, a generic baseline with no taxes and no caps. The jurisdictions table in the database is a projection of what the service carries, so no API call creates a jurisdiction. See [Jurisdictions](/en/lender/jurisdictions). ## How a request resolves *** Every request passes the same three gates before a handler runs. Lender expects a bearer JWT. The two jurisdiction-discovery reads are the only public operations. 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. 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 worth knowing: 1. A disbursement, an interest accrual, and a settled Brazilian prepayment quote each write 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/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. Two migration sets apply: the core set and the Brazil set. | | Valkey or Redis | Idempotency records, and the jurisdiction cache. | | RedPanda | Lifecycle events, when streaming is enabled. | | Midaz | The destination of every posting. | [Configuration and deploy](/en/lender/configuration-and-deploy) lists the settings behind each one. ## Time-driven work *** Not everything starts with a request. The accrual run also runs as a scheduled job. Every job stays off until you enable it, and you set its schedule. See [Accounting and accrual runs](/en/lender/accounting-and-accrual-runs). ## Next steps *** One application from submitted to disbursed, and the transaction that does the work. The vocabulary the whole product shares. The posting path, the event catalog, and tenant isolation. Dependencies, the container, and the settings that shape a deployment. # Lender events Source: https://docs.lerian.studio/en/lender/lender-events Subscribe to the credit journey: the fixed CloudEvents source, the topic and ce-type shapes, the 21 events of the documented journey, the outbox durability guarantee, and what a payload carries. When streaming is enabled and a broker is configured, Lender publishes a business event whenever a product, an application, a loan account, or a Brazilian regulatory record changes state. Subscribe to those events and your service reacts to each change as it happens, with no polling loop. Every event is outbox-backed and tenant-scoped. This page covers the consumer side: what arrives on the wire, which events exist, and how to make a handler safe. ## Turn publication on *** Event publication is a deployment choice. Origination works without it. | Setting | Value | | ------------------------------ | ------------------------------------------------------------------------- | | `STREAMING_ENABLED` | `true` | | `STREAMING_BROKERS` | The RedPanda addresses. | | `STREAMING_CLOUDEVENTS_SOURCE` | `lender`. Lender requires exactly this value and validates it at startup. | The PostgreSQL outbox is initialized on every deployment; it has no enable switch. Lender refuses to start when publication is on and any streaming setting is wrong, so a misconfigured deployment fails at startup instead of dropping events quietly. ## The wire contract *** The CloudEvents source is the fixed literal `lender`, and the source also namespaces every topic. A subscriber therefore reads: | Wire element | Value | | ------------------- | ---------------------------------------------------- | | Kafka topic | `lender..` | | `ce-source` header | `lender` | | `ce-type` header | `studio.lerian..` | | `ce-subject` header | The identifier of the record that changed. | | Partition key | The tenant, so one tenant's events keep their order. | For a v2 loan-application disbursement that resolves to: ``` topic lender.loan_application.disbursed.v2 ce-type studio.lerian.loan_application.disbursed ``` Messages travel in CloudEvents binary mode, version 1.0. The context attributes ride as message headers: `ce-specversion`, `ce-id`, `ce-source`, `ce-type`, `ce-time`, `ce-schemaversion`, `ce-resourcetype`, and `ce-eventtype` on every message, plus `ce-subject`, `ce-datacontenttype`, and `ce-tenantid` when Lender has a value for them. `ce-schemaversion` is `2.0.0` for every `loan_application.*` event. Their topics carry the `.v2` suffix. The v1 events retain `ce-schemaversion: 1.0.0` and plain topic names. Bind to the topic and schema version in the manifest for your deployed catalog. **Subscribe by topic.** The topic is the address, and it composes from the resource type and the event type — not from an internal catalog name. Read the topic column in the tables below rather than deriving one from an event description. ## The events *** This page covers the **21 events of the documented credit journey**. They group by the part of the journey they report. The streaming manifest declares your deployment's whole catalog, which can carry more definitions than this page lists. ### Products | Topic | Emitted when | | -------------------------------------- | ------------------------------------------------------- | | `lender.loan_product.created` | A product is created as a draft definition. | | `lender.loan_product.activated` | A product moves to active and pins its current version. | | `lender.loan_product_version.created` | A version snapshot is appended as immutable terms. | | `lender.accounting_profile.configured` | A profile and its posting rules are bound to a version. | | `lender.loan_charge.applied` | A product-version charge is applied to a loan account. | ### Origination | Topic | Emitted when | | -------------------------------------- | ------------------------------------------------------------------- | | `lender.loan_application.submitted.v2` | An application is accepted and waits for approval. | | `lender.loan_application.approved.v2` | An application is approved, with the decision facts. | | `lender.loan_application.rejected.v2` | An application is rejected, with the decision facts. | | `lender.loan_application.withdrawn.v2` | A pending or approved application is withdrawn before disbursement. | | `lender.loan_application.disbursed.v2` | An approved application is disbursed and a loan account goes live. | ### Servicing | Topic | Emitted when | | ----------------------------------------- | ----------------------------------------------------- | | `lender.repayment.recorded` | A repayment is recorded, with its cash allocation. | | `lender.repayment_reversal.recorded` | A reversal is recorded as a compensating transaction. | | `lender.loan_schedule.prepayment_applied` | A prepayment produces a successor schedule version. | | `lender.loan_schedule.rescheduled` | A reschedule produces a successor schedule version. | ### Brazil pack | Topic | Emitted when | | -------------------------------------------- | ------------------------------------------------------------------------- | | `lender.loan_account.pdd_stage_transitioned` | A PDD stage transition or cure is recorded, with its accrual eligibility. | | `lender.prepayment_quote.created` | An immutable prepayment quote is created. | | `lender.prepayment_settlement.recorded` | An accepted quote settles, with the final breakdown. | ### Consignado privado | Topic | Emitted when | | ---------------------------------------------- | ------------------------------------------------------------------------------- | | `lender.consignado_exclusao.requested` | Lender asks the payroll rail to cancel an averbação. | | `lender.consignado_redirecionamento.requested` | Lender asks the payroll rail to redirect collection to another employment link. | | `lender.payroll_deduction.refund_required` | Confirmed payroll cash arrived after payoff and the borrower is owed a refund. | | `lender.guarantee_recovery_cash.allocated` | A confirmed guarantee-recovery receipt is allocated. | The consignado journey also **consumes** facts from the payroll-deduction gateway, on that product's own topics. Read [Consignado privado](/en/lender/consignado-privado). ## Durability and delivery *** Every event in the catalog carries the same delivery policy. Nothing publishes straight to the broker. Lender writes the event to a transactional outbox in the **same database transaction** as the state change, and a dispatcher relays it afterwards. The event therefore survives a crash, and it survives a broker outage while the dispatcher retries. The retry budget is bounded. The dispatcher gives one event `OUTBOX_MAX_DISPATCH_ATTEMPTS` attempts, and the default is `10`. It waits `OUTBOX_RETRY_WINDOW_SEC` seconds between attempts, and the default is `300`. Past that budget the dispatcher stops retrying the event. Raise the budget when you expect outages longer than the default window. Events ride the same outbox as the ledger relay, so 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 ledger posting lands afterwards. Read [Accounting and accrual runs](/en/lender/accounting-and-accrual-runs). **Delivery is at-least-once.** An event can arrive more than once, and a redelivery carries the same facts. Make each handler idempotent on the record identifier in the payload, which also travels in `ce-subject`. ## What a payload carries *** The body is JSON and carries the facts of the change, not a diff. A `loan_application.disbursed` body: ```json theme={null} { "loan_application_id": "018f2a1c-7d40-7b31-9d40-2f1e8c5a4b77", "loan_product_version_id": "8f2c1a9e-4b30-4c02-9a1b-2d5e6f7a1c33", "borrower_id": "borrower-0001", "assigned_officer_id": "officer-0007", "status": "disbursed", "loan_account_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "disbursement_event_id": "019826f4-6a9c-7b31-8a02-11c3d4e5f678", "disbursement_transaction_id": "019826f4-6a9c-7b31-8a02-11c3d4e5f679", "gross_requested_amount": "48000.00", "net_delivered_amount": "48000.00", "disbursed_at": "2026-08-01T13:00:00Z", "profile_version": "v1", "preview_jurisdiction_code": "XX", "updated_at": "2026-08-01T13:00:00Z" } ``` **Money and rates travel as decimal strings**, never as JSON numbers, exactly as they do over REST. Parse them with a decimal type. Timestamps are RFC 3339 in UTC. Each event carries the facts its own record holds, so read the shape of the event you subscribe to. A Brazilian event adds what its regulation needs: a PDD transition carries the stage and the accrual eligibility, and a prepayment quote carries the rebate and the IOF reconciliation. ## Check the contract at startup *** `GET /api/v1/streaming/manifest` returns the catalog manifest: every event definition with its resource type, event type, schema version, and delivery policy. It needs a bearer token with `streaming_manifest` `read`. It reports the deployment's whole declared catalog, not one tenant's view. Read it when your consumer starts. Check each event you subscribe to against the manifest: the resource type, the event type, and the schema version. That catches a name or version mismatch at startup instead of at runtime. ## Next steps *** The base path, authentication, idempotency, and the operations by job. The parts of the service, the outbox path, and the time-driven work. Where Lender sits next to Midaz, Access Manager, and the streaming backbone. The payroll-deduction journey and the facts it consumes. # Lender in the platform Source: https://docs.lerian.studio/en/lender/lender-in-the-platform See how Lender posts to the Midaz ledger, emits lifecycle events on the streaming backbone, isolates tenants per schema, and reports telemetry data. Lender is a Lerian primitive: it runs on the platform, not beside it. Four platform seams matter when you operate it — the ledger posting path, the event stream, multi-tenancy, and observability. ## The Midaz posting path *** Lender is the source of truth for the credit journey; [Midaz](/en/midaz/about-midaz) is the source of truth for balances. A disbursement and each interest accrual become a balanced double-entry transaction bound for the ledger. So does a prepayment that settles a Brazilian loan account against a prepayment quote. 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. When a loan is disbursed, interest accrues, or a Brazilian prepayment quote settles, Lender persists a durable **posting intent** in the same database transaction that changes domain state. The intent is written to a transactional **outbox**, so it survives a crash between "state changed" and "ledger booked." When the Midaz ledger relay is 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**. 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. 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. When the ledger relay is configured, Lender reaches Midaz through [Access Manager](/en/platform/access-manager/access-manager): it authenticates with machine-to-machine (M2M) credentials and a short-lived JWT, per tenant. ## Lifecycle events *** When streaming is enabled and a broker is configured, Lender publishes the **21 business events of the documented credit journey** over the platform streaming backbone (RedPanda, through Lerian's streaming library). Every event is **outbox-backed** and **tenant-scoped**, so downstream products and your own services can react to the credit journey as it unfolds. Lender's CloudEvents source is the fixed literal `lender`. Set `STREAMING_CLOUDEVENTS_SOURCE=lender` — streaming requires that value, and the source also namespaces every topic. A subscriber therefore reads: | Wire element | Value | | ------------------ | ------------------------------------------------------------------------------------------- | | Kafka topic | `lender..` — for example `lender.loan_application.disbursed.v2` | | `ce-source` header | `lender` | | `ce-type` header | `studio.lerian..` — for example `studio.lerian.loan_application.disbursed` | | Partition key | the tenant | Subscribe by topic. The topics group by domain: | Domain | Topics | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Products | `lender.loan_product.created`, `lender.loan_product.activated`, `lender.loan_product_version.created`, `lender.accounting_profile.configured`, `lender.loan_charge.applied` | | Origination | `lender.loan_application.submitted.v2`, `lender.loan_application.approved.v2`, `lender.loan_application.rejected.v2`, `lender.loan_application.withdrawn.v2`, `lender.loan_application.disbursed.v2` | | Servicing | `lender.repayment.recorded`, `lender.repayment_reversal.recorded`, `lender.loan_schedule.prepayment_applied`, `lender.loan_schedule.rescheduled` | | Brazil pack | `lender.loan_account.pdd_stage_transitioned`, `lender.prepayment_quote.created`, `lender.prepayment_settlement.recorded` | | Consignado privado | `lender.consignado_exclusao.requested`, `lender.consignado_redirecionamento.requested`, `lender.payroll_deduction.refund_required`, `lender.guarantee_recovery_cash.allocated` | Because the events ride the same outbox as the ledger relay, 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 — so Lender's state and the Midaz booking are eventually consistent rather than committed in the same transaction. The **consignado privado** journey also consumes facts from the payroll-deduction gateway, on that product's own topics. See [Consignado privado](/en/lender/consignado-privado). ## Multi-tenancy *** Like every Lerian product, Lender is built for full tenant isolation from the ground up. * **Schema-per-tenant** persistence when multi-tenancy is enabled, so tenant data never shares a table. * **Tenant-scoped** events and ledger postings — every emitted event and every Midaz transaction carries the tenant it belongs to. * Under multi-tenant mode, the ledger relay resolves a **per-tenant** Midaz client, and the accounting profile supplies the per-tenant ledger target (which is why routing fails closed when a target is missing). See [Multi-tenancy](/en/multi-tenancy) for the platform-wide model. ## 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 *** Go deeper on posting rules, accrual runs, and journal references. See the dependencies and configuration the posting path and event stream require. # Prerequisites Source: https://docs.lerian.studio/en/lender/lender-prerequisites What must be true before your first Lender call — the services it depends on, the two migration sets, 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/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 streaming backbone for the event catalog. Optional. | PostgreSQL and Valkey are required to originate. An identity provider is required when route authorization is enabled; production requires it. A first loan needs no Midaz and no RedPanda. 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. `make set-env` copies `config/.env.example` to `config/.env`. Edit that file for every setting on this page. `make up` starts the Compose dependencies and the service on port `8080`. `make migrate-up` applies both migration sets. See the next section. `make dev` runs the service with Air. ## Apply both migration sets *** Lender does not migrate the database when it starts. You apply migrations out of band, and there are two sets: | Setting | Default | Contents | | -------------------- | -------------------------------------- | ------------------------------------------------------------------------ | | `MIGRATIONS_PATH` | `migrations` | The core schema — products, applications, schedules, accounting, outbox. | | `MIGRATIONS_BR_PATH` | `internal/jurisdictions/br/migrations` | The Brazil jurisdiction schema. | Apply both. `make migrate-up` reads both paths. The core set also seeds the **jurisdiction registry**: two active jurisdictions, `BR` for Brazil and `XX`, the generic profile. 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/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 — when `PLUGIN_AUTH_ENABLED=true` 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. Set three variables: | Variable | Value | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `PLUGIN_AUTH_ENABLED` | `true` | | `PLUGIN_AUTH_HOST` | The address of your identity provider. | | `AUTH_REQUIRED` | `true`, so protected routes refuse with `503` when the Auth client is disabled or misconfigured, instead of passing the request through. | Then send a bearer token on every call. The token needs a non-empty subject claim, and under the generic `XX` profile the same subject must create, approve, and disburse the application. `make generate-casdoor` writes Lender's roles and permissions to `config/casdoor/init_data.json`. 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/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 in single-tenant mode. | 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. ## Single-tenant defaults *** `MULTI_TENANT_ENABLED` is `false` by default. In that mode `DEFAULT_TENANT_ID` identifies the one tenant, and it must be a valid UUID. ## Multi-tenant additions *** Multi-tenant mode gives each tenant its own schema and its own ledger client pool. It adds four requirements: 1. Set `MULTI_TENANT_ENABLED=true` and point Lender at the tenant manager. 2. Every token carries a `tenantId` claim. 3. `public.tenant_jurisdictions` carries a row for each tenant and jurisdiction pair. 4. Each accounting profile declares its own `midazOrganizationId` and `midazLedgerId`. Multi-tenant mode does not fall back to the environment defaults. ## Event streaming *** Streaming is off by default and origination does not need it. To publish the lifecycle event catalog, set `STREAMING_ENABLED=true`, point `STREAMING_BROKERS` at RedPanda, and set `STREAMING_CLOUDEVENTS_SOURCE=lender`. Lender validates that source value when it starts. ## Checklist *** * PostgreSQL 17 and Valkey 8 reachable. * Both migration sets applied. * When route authorization is enabled, `PLUGIN_AUTH_ENABLED=true`, `AUTH_REQUIRED=true`, the identity provider reachable, and a bearer token with a subject and every permission in the table above. * `DEFAULT_TENANT_ID` a valid UUID, in single-tenant mode. ## Next steps *** Six calls from an empty database to a disbursed loan. The full container shape, the wider environment surface, and the ledger posting path. # Quick start Source: https://docs.lerian.studio/en/lender/lender-quick-start Six calls from an empty database to a disbursed loan — a product, a version, an accounting profile, an application, an approval, and a disbursement. This page takes you from an empty database to one disbursed loan in six 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/lender/lender-prerequisites) first. Send the same bearer token on all six calls. Lender takes the officer from the token subject. Under the generic profile, only that officer can approve and disburse the application. Use jurisdiction `XX` for this walkthrough. `XX` is Lender's generic profile, and it computes no withholdings — which keeps the amounts in step 6 simple. A Brazilian regulated loan carries CET disclosure and capitalization consent. Read the [Brazil regulatory pack](/en/lender/brazil-regulatory-pack) for that path. A payroll-deducted contract belongs to the consignado privado bounded context. Read [Consignado privado](/en/lender/consignado-privado) for its vocabulary and its lifecycle topics. ## The six 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-applications → pending_approval 5. POST /api/v1/loan-applications/{id}/approve → approved 6. POST /api/v1/loan-applications/{id}/disburse → disbursed ``` ## 1. Create the product *** `POST /api/v1/loan-products` ```json theme={null} { "name": "Personal loan — generic", "loanType": "personal", "jurisdictionCode": "XX" } ``` `loanType` takes `personal`, `commercial`, or `card`. `jurisdictionCode` takes a code the registry carries: `XX` or `BR`. Any other code returns 422. The response carries the new `id`. The product is in `draft`, which is all this walkthrough needs: an application binds to a *version*, so you do not activate the product to originate. ## 2. Create a version *** `POST /api/v1/loan-products/{productId}/versions` ```json theme={null} { "jurisdictionCode": "XX", "changeReason": "initial version", "currency": "USD", "rateMode": "fixed", "fixedAnnualRateBps": 1800 } ``` 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. * **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. Lender needs it at disbursement, 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 **seven or eight rules**, one per accounting event. Seven events need a rule: `disbursement`, `repayment`, `prepayment`, `accrual`, `collection_unapplied`, `collection_reapply`, and `collection_refund`. `accrual_tax` is the optional eighth. Fewer than seven rules is rejected before the handler runs. 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 the generic profile computes none, so the two-leg rule is the one that balances under `XX`. In multi-tenant mode the profile also needs `midazOrganizationId` and `midazLedgerId`. Read [Define a loan product](/en/lender/define-a-loan-product) for the wider product surface. ## 4. Create the application *** `POST /api/v1/loan-applications` ```json theme={null} { "loanProductVersionId": "", "borrowerId": "borrower-0001", "requestedPrincipalAmount": "50000.00", "requestedInterestRate": "0.01500000", "requestedInstallments": 24, "expectedDisbursementDate": "2026-08-01T12:00:00Z", "previewScheduleSnapshotId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8" } ``` * `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: `"0.01500000"` a month is `1800` bps a year. * `requestedInstallments` is between 1 and 600. * `previewScheduleSnapshotId` is a UUID **you** generate to identify the quote you showed the borrower. Lender records it on the application. Compute the schedule you show with `POST /api/v1/loan-applications/preview-schedule`. That call persists nothing and returns no identifier. Mint the identifier on your side and keep it with your own quote record. * 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. ## 5. Approve *** `POST /api/v1/loan-applications/{id}/approve` ```json theme={null} { "approvedAmount": "48000.00", "decisionAt": "2026-08-01T12:00:00Z", "note": "within policy" } ``` `approvedAmount` becomes the ceiling on everything you disburse. Under `XX`, only the officer who created the application may approve it. The application moves to `approved` and carries the decision record. ## 6. Disburse *** `POST /api/v1/loan-applications/{id}/disburse` Send the header **`X-Idempotency`**. Lender requires it. A retry with the same value replays the first response, and records no second disbursement. ```json theme={null} { "loanAccountId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "grossRequestedAmount": "48000.00", "netDeliveredAmount": "48000.00", "disbursedAt": "2026-08-01T13:00:00Z" } ``` **Under `XX`, `netDeliveredAmount` must equal `grossRequestedAmount`.** The disbursement posting balances as net equals gross minus withholdings, and the generic profile computes no withholdings. Any lower net leaves the posting unbalanced and Lender rejects the disbursement. `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. Lender also checks that: * `netDeliveredAmount` does not exceed `grossRequestedAmount`. * `grossRequestedAmount`, and the running total across tranches, does not exceed `approvedAmount`. * `disbursedAt` is not before the approval decision. * The jurisdiction and profile version still match the pair Lender resolved at step 4. `originationFeeAmount` is optional. It carries cost metadata that accrual reads. Lender does not treat it as a withholding, so it does not change the net-to-gross relationship. ## 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. Keep that value with your own product record. If you configured Midaz, the disbursement posting reaches the ledger once the outbox dispatcher relays the intent. That happens shortly after the call, not inside it. ## Next steps *** Record repayments, prepay, reschedule, and correct a live loan account. Interest recognition runs on its own schedule. Start an accrual run, or enable the heartbeat. # Lender REST API Source: https://docs.lerian.studio/en/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 is the map, not the territory. It 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. 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 `false`; when enabled, protected routes require a JWT bearer token: ```http theme={null} Authorization: Bearer ``` The default only fits single-tenant deployments: startup rejects `MULTI_TENANT_ENABLED=true` with `PLUGIN_AUTH_ENABLED=false`, because Lender resolves the tenant from the `tenantId` claim of the validated identity. Two reads are public and take no token: [list jurisdictions](/en/reference/lender/list-jurisdictions) and [get a jurisdiction](/en/reference/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` | `preview:schedule`, `create`, `approve`, `reject`, `withdraw`, `disburse`, `capitalization-consent:ingest`, `preview:tax` | | `loan_accounts` | `read`, `audit:read`, `charge:apply`, `repayment:preview`, `repayment:record`, `repayment:reverse`, `prepayment:record`, `reschedule`, `cet:read`, `pdd:transition` | | `accounting` | `read`, `write` | | `streaming_manifest` | `read` | Grant the officer role only the actions its job needs. `make generate-casdoor` writes Lender's roles and permissions to a seed file you load into your identity provider — [Prerequisites](/en/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.** In single-tenant mode Lender uses `DEFAULT_TENANT_ID`; in multi-tenant mode it resolves the tenant from the validated identity. See [Multi-tenancy](/en/multi-tenancy). 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 *** The money and schedule writes accept an `X-Idempotency` request header. | Operation | Header | | ------------------------------------------------------------------------------ | ---------------------------------------------------- | | [Disburse an application](/en/reference/lender/disburse-loan-application) | `X-Idempotency` required | | [Apply a product charge](/en/reference/lender/create-loan-product-charges) | `X-Idempotency` required | | [Prepay a loan account](/en/reference/lender/prepay-loan-account) | `X-Idempotency` required | | [Prepay under the Brazil pack](/en/reference/lender/prepay-loan-account-br) | `X-Idempotency` required | | [Reschedule a loan account](/en/reference/lender/reschedule-loan-account) | `X-Idempotency` required | | [Record a repayment](/en/reference/lender/record-repayment) | `X-Request-ID`, with `X-Idempotency` as the fallback | | [Reverse a transaction](/en/reference/lender/reverse-loan-account-transaction) | `X-Request-ID`, with `X-Idempotency` as the fallback | Send your own key. With a reachable idempotency store, the five operations that **require** `X-Idempotency` share one behaviour: * A retry of a **completed** call replays the first response and stamps `X-Idempotency-Replayed: true` on it. Nothing is recorded a second time. * A retry while the first call is still **in flight** answers `409`. * The key is scoped to your tenant and expires after the window `IDEMPOTENCY_RETRY_WINDOW_SEC` sets, which defaults to 300 seconds. The shared middleware fails open on transient idempotency-store errors. During an outage, do not rely on replay or middleware-level at-most-once protection. Repayment and reversal work differently. Both 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. | ## 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/lender/list-loan-products) and [list Brazilian loan products](/en/reference/lender/list-loan-products-br) | `limit` (default 25, ceiling 100) and `offset` (default 0, ceiling 10000) | | [Audit events](/en/reference/lender/list-loan-account-audit-events-huma) | `limit` alone (default 50, ceiling 100) | A value outside the range is rejected rather than clamped. 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/lender/create-loan-product) and [append a version](/en/reference/lender/create-loan-product-version) build the terms an application binds to. The version is immutable. [Bind an accounting profile](/en/reference/lender/create-loan-product-accounting-profile) maps each accounting event onto general-ledger accounts, and Lender needs it at disbursement. [Apply a charge](/en/reference/lender/create-loan-product-charges) and [read floating rates](/en/reference/lender/list-loan-product-floating-rates) complete the surface, alongside [list](/en/reference/lender/list-loan-products), [get](/en/reference/lender/get-loan-product), and [activate](/en/reference/lender/activate-loan-product). Read [Define a loan product](/en/lender/define-a-loan-product). ### Originate Six operations carry an application from submitted to disbursed: [create](/en/reference/lender/create-loan-application), then one of [approve](/en/reference/lender/approve-loan-application), [reject](/en/reference/lender/reject-loan-application), or [withdraw](/en/reference/lender/withdraw-loan-application), then [disburse](/en/reference/lender/disburse-loan-application). [Preview a schedule](/en/reference/lender/preview-loan-schedule) computes installments for a quote and persists nothing. The create and decide responses are the only reads of an application, so keep the body each call returns. Read [How origination works](/en/lender/how-origination-works) for the state machine and [Quick start](/en/lender/lender-quick-start) for the six calls end to end. ### Service a live loan Five reads describe the account: [the account](/en/reference/lender/get-active-loan-account), [its schedule](/en/reference/lender/get-active-loan-schedule), [its transactions](/en/reference/lender/list-active-loan-transactions), [its charges](/en/reference/lender/list-active-loan-charges), and [its audit history](/en/reference/lender/list-loan-account-audit-events-huma). Five writes move money or the schedule: [preview a repayment](/en/reference/lender/preview-repayment) before you [record it](/en/reference/lender/record-repayment), [prepay](/en/reference/lender/prepay-loan-account), [reschedule](/en/reference/lender/reschedule-loan-account), and [reverse a transaction](/en/reference/lender/reverse-loan-account-transaction). Nothing rewrites history. A reversal posts a new transaction that compensates the original. Read [Service a loan](/en/lender/service-a-loan). ### Account and book [Start an accrual run](/en/reference/lender/create-accrual-run) recognizes interest for a competence period. [List journal references](/en/reference/lender/get-journal-reference) by correlation id and [read one](/en/reference/lender/get-journal-reference-by-id) to find the accounting record a run wrote. Read [Accounting and accrual runs](/en/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/lender/jurisdictions). ### Brazil The Brazil pack adds regulated reads and writes under `/api/v1/br`: [CET disclosure](/en/reference/lender/get-loan-account-cet-disclosure), [the credit-operation descriptor](/en/reference/lender/get-loan-account-credit-operation-descriptor), [PDD stage](/en/reference/lender/get-loan-account-pdd-stage) and [its transitions](/en/reference/lender/apply-loan-account-pdd-stage-transition), [a prepayment quote](/en/reference/lender/create-prepayment-quote) with [its payoff statement](/en/reference/lender/get-payoff-statement), [tax preview](/en/reference/lender/preview-tax), and [capitalization consent](/en/reference/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/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/lender/consignado-privado). ## Next steps *** Six calls from an empty database to a disbursed loan. Subscribe to the credit journey instead of polling. Every operation, with full request and response shapes. The services, migrations, and configuration a first call needs. # Lender SDK and embedding Source: https://docs.lerian.studio/en/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/lender/lender-rest-api) for the operations themselves. 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; `fixedAnnualRateBps`, `floatingSpreadBps`, and `annualRateBps` 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 lib-auth 401/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 the money writes with your own key Disbursement, product charges, prepayment, Brazil-pack prepayment, reschedule, repayment, and reversal each accept an idempotency key that you generate. Derive it from your own request identifier, and a retry costs nothing while the idempotency store is available. The five 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 and reversal key on `X-Request-ID` instead, and fall back to `X-Idempotency` when it is absent: a retry with the same facts replays, and 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 open during an idempotency-store outage, so an ambiguous failure may already have executed the operation; confirm the outcome before you retry a money write in that window. [Lender REST API](/en/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, because 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. In the generic human flow, 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**. In single-tenant mode Lender uses `DEFAULT_TENANT_ID`. In multi-tenant mode the tenant comes from the validated identity, never from a header, a query parameter, or a body field: each token carries a `tenantId` claim, so your service holds one credential for every tenant it serves. Read [Multi-tenancy](/en/multi-tenancy). **Learn about state changes from events**. When streaming is enabled and a broker is configured, Lender publishes business events through its outbox. When streaming is disabled, it uses a no-op emitter; when streaming is enabled with no broker configured, Lender refuses to start rather than fall back to the no-op emitter. Enable and configure streaming before treating event delivery as an integration contract. Read [Lender events](/en/lender/lender-events). 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/lender/accounting-and-accrual-runs). ## Next steps *** The base path, authentication, idempotency, and the operations by job. The wire contract and the events you can subscribe to. Six calls from an empty database to a disbursed loan. The services, migrations, and configuration a first call needs. # Originate a loan Source: https://docs.lerian.studio/en/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 so every decision is recorded and every disbursement is 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 *** `POST /api/v1/loan-applications/preview-schedule` computes the amortisation schedule for prospective terms **without originating anything**. Use it to show installments and disclosures before anyone commits. `POST /api/v1/loan-applications` creates the application against a product version. Resolve the application with exactly one of: * `POST /api/v1/loan-applications/{id}/approve` — accept it; a decision record is kept. * `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. `POST /api/v1/loan-applications/{id}/disburse` records the funds delivered. Send an `X-Idempotency` key: while the idempotency store is reachable, a retry with the same key replays the first response and records no second disbursement. The middleware fails open during an idempotency-store outage, so confirm the outcome before you retry an ambiguous failure. 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. When the ledger relay is 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/lender/lender-in-the-platform). ## 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/lender/service-a-loan). ## What happens downstream *** When streaming is enabled and a broker is configured, the lifecycle emits `loan_application.submitted.v2`, `loan_application.approved.v2`, `loan_application.rejected.v2`, `loan_application.withdrawn.v2`, and `loan_application.disbursed.v2`. Disbursement records a durable posting intent and relays it to the ledger only when the ledger relay is configured. Read [the posting path](/en/lender/lender-in-the-platform). Brazilian origination adds regulated steps — CET disclosure and capitalization consent — covered in the [Brazil regulatory pack](/en/lender/brazil-regulatory-pack). The payroll-deducted **consignado** flow forms its own Brazilian bounded context. See [Consignado privado](/en/lender/consignado-privado). ## Next steps *** Record repayments, prepay, reschedule, and correct an active loan account. # Service a loan Source: https://docs.lerian.studio/en/lender/service-a-loan Operate an active loan account — read its schedule and transactions, record and preview repayments, prepay, reschedule, and correct with reversals. Once a loan is disbursed it becomes a **loan account** — the servicing view of the live contract. Servicing is everything that happens across its life: money comes in, the schedule changes, and mistakes get corrected without ever rewriting 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 of lifecycle events. | ## Record money *** `POST /api/v1/loan-accounts/{id}/preview-repayment` shows how a repayment would be allocated 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. `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/lender/lender-rest-api#idempotency). `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/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 transaction being reversed, the loan account, the reversal effective date, the reason, the profile version, and the jurisdiction code. Any difference in those facts answers `409`. Servicing never edits the past. Repayments, reschedules, and reversals all append new facts. The current state is re-derived from that history, so the account stays explainable from its own record. ## What happens downstream *** When streaming is enabled and a broker is configured, servicing emits `repayment.recorded`, `repayment_reversal.recorded`, `loan_schedule.prepayment_applied`, and `loan_schedule.rescheduled`. A prepayment settled against a Brazilian prepayment quote records a durable posting intent and relays it to the ledger only when the ledger relay is configured. Read [the posting path](/en/lender/lender-in-the-platform). ## Next steps *** Recognize interest over time and read the journal reference each run records. Read the PDD stage of an overdue loan and apply a stage transition. # What is Lender? Source: https://docs.lerian.studio/en/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 — from defining the loan product, through origination and disbursement, to servicing, repayment, and audit-ready history. The journey it runs is **consumer credit (CDC)**. Lender is available to licensed customers; its repository is maintained internally. ## What a credit journey engine is *** A credit operation is not a single event — it 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, so every contract traces back to the exact product version it was originated under. * **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 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 five domains *** Under the hood, Lender is organized into five bounded domains. Every credit journey — whatever the product — flows through them: | 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. | Start with [Core concepts](/en/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 built jurisdiction-aware from the start: a generic baseline for any market, and a deep Brazilian profile layered on top. 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/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/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**: Disbursement, interest accrual, and a prepayment settled against a Brazilian prepayment quote record durable posting intents through the accounting profiles and posting rules you configure. Lender relays them to the double-entry ledger only when the ledger relay is configured. 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**: When streaming is enabled and a broker is configured, lifecycle events publish on the platform's streaming backbone, so 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/lender/lender-in-the-platform) for the posting path, the event catalog, and tenant isolation. ## Next steps *** Learn the vocabulary: loan products, accounting profiles, applications, loan accounts, accrual runs, and the Brazil terms. See how Lender posts to the ledger, emits events, and isolates tenants. Walk an application from intake through preview, approval, and disbursement. Explore the flagship 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). # Multi-tenant configuration Source: https://docs.lerian.studio/en/matcher/configuration/matcher-multi-tenant Configure Matcher for tenant-aware authentication and tenant-specific PostgreSQL pools. For supported multi-tenant requests, Matcher first derives tenant context from the authorized JWT and then uses it to resolve tenant-specific PostgreSQL infrastructure through Tenant Manager. It is a deployment mode, not a runtime toggle: validate it in a non-production environment before enabling it for a shared installation. ## Requirements *** Before enabling multi-tenant mode: * Set `MULTI_TENANT_ENABLED=true` and `PLUGIN_AUTH_ENABLED=true`. Matcher rejects multi-tenant startup without authorization enforcement. * Use `AUTH_PROVIDER=plugin-auth`. The current `workos` provider resolves verified requests to the default tenant and is not supported for tenant selection. * Set `MULTI_TENANT_URL` to an origin-only HTTPS URL in staging and production, plus a non-empty `MULTI_TENANT_SERVICE_API_KEY`. `MULTI_TENANT_ENVIRONMENT` is optional and falls back to `ENV_NAME` when unset. Plaintext `http` is allowed for local development; in other environments it also requires an explicit `MULTI_TENANT_ALLOW_INSECURE_HTTP=true`. * Set `ENVIRONMENT_NAME` (or `ENV_NAME`) to `staging` or `production`. * Provide a valid `tenant_id` or `tenantId` claim in requests authenticated through `plugin-auth`. * Keep the default tenant database available on the root pool for default-tenant workloads and operational tools. Matcher resolves dedicated PostgreSQL pools for non-default tenants; the default tenant uses the root pool. It does not switch tenant schemas through PostgreSQL `SET search_path`; tenant-specific credentials, network boundaries, and Tenant Manager configuration remain part of the isolation boundary. ## Tenant identity *** With `AUTH_PROVIDER=plugin-auth` in multi-tenant mode, Matcher derives tenant identity from a valid `tenant_id` or `tenantId` JWT claim. It does not accept a caller-controlled tenant selector from request bodies, query parameters, or arbitrary headers. `workos`, single-tenant, and authentication-disabled deployments use the configured default tenant. ## Connection-pool controls *** | Control | Scope | Effect | | ---------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MULTI_TENANT_MAX_OPEN_CONNS_PER_TENANT` | Bootstrap environment | Per-tenant PostgreSQL open-connection default and hard ceiling. It defaults to `0`; when both connection-limit variables are `0`, lib-commons uses 25 open / 5 idle defaults and 200 open / 50 idle ceilings. It is independent of the root pool's `POSTGRES_MAX_*` settings. Change it through deployment configuration and restart Matcher. | | `MULTI_TENANT_MAX_IDLE_CONNS_PER_TENANT` | Bootstrap environment | Companion idle-connection default and hard ceiling; it shares the `0` fallback behavior above. Change it through deployment configuration and restart Matcher. | | `MULTI_TENANT_MAX_TENANT_POOLS` | Systemplane runtime configuration | Maximum number of tenant pools Matcher may keep open; the default is `100` and the value must be positive. | | `MULTI_TENANT_IDLE_TIMEOUT_SEC` | Systemplane runtime configuration | Idle-pool timeout used by the tenant-pool manager; the default is `300` seconds and the value must be positive. The new value applies through Systemplane without a restart. | At the configured limits, Matcher's tenant-pool manager evicts an idle least-recently-used pool when resolving a new tenant would exceed `MULTI_TENANT_MAX_TENANT_POOLS`; the evicted tenant is re-resolved on demand. Validate migration and failure behavior against the deployed Tenant Manager integration. ## Shared infrastructure *** Matcher delegates tenant-aware infrastructure resolution to the multi-tenancy platform service. Do not assume a fixed RabbitMQ virtual-host name, message-header convention, Redis key format, cache TTL, or S3 prefix from Matcher alone. Those conventions are component- and deployment-specific; review the corresponding infrastructure and platform documentation before building an integration around them. ## Enabling the mode *** 1. Provision and verify the default tenant and the tenants that Matcher must serve. 2. Configure the authentication provider, Tenant Manager, PostgreSQL connectivity, and bootstrap environment variables. 3. Start Matcher and confirm the health checks and a tenant-scoped authenticated request. 4. Observe the tenant-pool count and database connection use under expected load. 5. Roll out the deployment only after isolation and failure behavior have been exercised in the target environment. Changing tenant topology, database credentials, or per-pool PostgreSQL connection limits is an infrastructure change. Apply it through the deployment process; Systemplane cannot change those bootstrap values without a restart. ## Next steps *** Review the values that Matcher can change through Systemplane. Review authentication, tenant isolation, and dependency TLS controls. # Runtime configuration (Systemplane) Source: https://docs.lerian.studio/en/matcher/configuration/matcher-systemplane Read and update Matcher's runtime configuration through Systemplane's key-value API — adjust rate limits, worker intervals, and the maximum tenant-pool count without restarting the service. Systemplane lets you view and modify supported Matcher configuration without restarting the service. Application behavior varies by key: request-time settings may apply on the next request, while a configuration reload stops and restarts a running worker when its configuration changes. ## Why use Systemplane *** In a traditional deployment, changing a configuration value means updating environment variables and restarting the service. Systemplane eliminates that downtime for many settings: * **Adjust rate limits** during traffic spikes without a redeploy — avoiding service disruption during peak transaction volumes * **Tune worker intervals** based on observed workload; a configuration reload reconciles the affected worker and restarts it when its running configuration changes * **Update the maximum number of tenant pools** as traffic patterns change; PostgreSQL connections-per-pool settings require an environment change and restart * **Inspect current runtime values** to diagnose production issues without log diving ## How it works *** Systemplane provides a flat key-value management API. All configuration keys are in a single namespace under `/system/matcher`. ### Endpoints | Endpoint | Method | What it does | | ---------------------- | ------ | -------------------------------------- | | `/system/matcher` | `GET` | List all keys and their current values | | `/system/matcher/:key` | `GET` | Get a specific key's current value | | `/system/matcher/:key` | `PUT` | Update a specific key's value | These endpoints are served directly by the running Matcher instance. They are not versioned under `/v1` — use the paths above exactly as shown. ## Permissions *** Systemplane configuration and catalog routes are protected by the same authentication used by Matcher API routes. When authentication is enabled, these routes require the `system-runtime-config:admin` RBAC permission (resource `system-runtime-config`, action `admin`). `GET /system/matcher/streaming/manifest` is a separate route and requires `streaming-manifest:read`. When authentication is disabled, all endpoints are accessible without restriction. ## Apply behaviors *** Not every configuration value can be changed at runtime. Each key has an **apply behavior** that tells you when changes take effect: | Behavior | What happens | | -------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Bootstrap only** | The value is read once at startup. You must restart the service for changes to take effect. | | **Live read** | Changes take effect immediately on the next request. | | **Bundle rebuild** | Changes trigger an internal state refresh. Takes effect within seconds. | | **Worker reconcile** | A configuration reload reconciles workers. If a running worker's configuration changed, Matcher stops and restarts it. | Most bootstrap-only keys are NOT registered in the systemplane API — they are managed exclusively through environment variables. This prevents a footgun where an admin PUT would appear to succeed but the running process would silently continue using the boot-time value. The registered Swagger keys are an exception: they are visible in Systemplane but remain bootstrap-only (see the note below). ## Common configuration keys *** Below are the most commonly adjusted keys, organized by category. For a complete list, call `GET /system/matcher`. ### Runtime-adjustable keys These keys can be changed without restarting Matcher: `swagger.enabled`, `swagger.host`, and `swagger.schemes` are registered and visible in Systemplane, but they are not live controls. Swagger mounting and handler values are captured at bootstrap, so a runtime `PUT` does not change the live UI or specification behavior. Change their startup configuration and restart Matcher instead. | Key | Env variable | Description | | --------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `server.body_limit_bytes` | `HTTP_BODY_LIMIT_BYTES` | Maximum request-body size for buffered routes (positive and no greater than 128 MiB). Streaming uploads use `ingestion.max_upload_bytes`. | | `cors.allowed_origins` | `CORS_ALLOWED_ORIGINS` | Allowed CORS origins | | `cors.allowed_methods` | `CORS_ALLOWED_METHODS` | Allowed CORS methods | | `cors.allowed_headers` | `CORS_ALLOWED_HEADERS` | Allowed CORS headers | | `rate_limit.enabled` | `RATE_LIMIT_ENABLED` | Enable or disable global rate limiting | | `rate_limit.max` | `RATE_LIMIT_MAX` | Maximum requests per rate limit window | | `rate_limit.expiry_sec` | `RATE_LIMIT_EXPIRY_SEC` | Rate limit window duration (seconds) | | `rate_limit.export_max` | `EXPORT_RATE_LIMIT_MAX` | Export endpoint rate limit | | `rate_limit.dispatch_max` | `DISPATCH_RATE_LIMIT_MAX` | Dispatch endpoint rate limit | | `rate_limit.admin_max` | `ADMIN_RATE_LIMIT_MAX` | Admin plane (`/system`) rate limit | | `idempotency.retry_window_sec` | `IDEMPOTENCY_RETRY_WINDOW_SEC` | Window for retrying failed idempotent requests | | `idempotency.success_ttl_hours` | `IDEMPOTENCY_SUCCESS_TTL_HOURS` | How long completed idempotency keys are cached | | `fetcher.discovery_interval_sec` | `FETCHER_DISCOVERY_INTERVAL_SEC` | TTL basis for the distributed lock that serializes manual Fetcher discovery refreshes; the runtime lease is 2× this value | | `export_worker.enabled` | `EXPORT_WORKER_ENABLED` | Enable or disable the export worker | | `export_worker.poll_interval_sec` | `EXPORT_WORKER_POLL_INTERVAL_SEC` | How often the export worker checks for new jobs | | `cleanup_worker.enabled` | `CLEANUP_WORKER_ENABLED` | Enable or disable the cleanup worker | | `cleanup_worker.interval_sec` | `CLEANUP_WORKER_INTERVAL_SEC` | Cleanup worker interval | | `scheduler.interval_sec` | `SCHEDULER_INTERVAL_SEC` | Scheduler polling interval | | `archival.enabled` | `ARCHIVAL_WORKER_ENABLED` | Toggle the archival worker created at boot. If archival was disabled at boot, Systemplane cannot create the worker | | `webhook.timeout_sec` | `WEBHOOK_TIMEOUT_SEC` | Timeout for webhook/callback dispatch | | `callback_rate_limit.per_minute` | `CALLBACK_RATE_LIMIT_PER_MIN` | Maximum callbacks per external system per minute | | `deduplication.ttl_sec` | `DEDUPE_TTL_SEC` | Deduplication TTL in seconds | ### Multi-tenant keys (runtime adjustable) These keys control multi-tenant behavior and can be adjusted without restart. See [Multi-Tenant Mode](/en/matcher/configuration/matcher-multi-tenant) for details. Enabling multi-tenant mode itself (`tenancy.multi_tenant_enabled` / `MULTI_TENANT_ENABLED`) is **bootstrap-only** — it is read once at startup and requires a restart. It is not registered in the Systemplane API and cannot be toggled at runtime. See the bootstrap-only table below. | Key | Env variable | Description | | --------------------------------------- | ------------------------------- | ------------------------------------- | | `tenancy.multi_tenant_url` | `MULTI_TENANT_URL` | Multi-tenancy service URL | | `tenancy.multi_tenant_max_tenant_pools` | `MULTI_TENANT_MAX_TENANT_POOLS` | Maximum concurrent tenant pools | | `tenancy.multi_tenant_idle_timeout_sec` | `MULTI_TENANT_IDLE_TIMEOUT_SEC` | Idle timeout for tenant pool eviction | | `tenancy.multi_tenant_cache_ttl_sec` | `MULTI_TENANT_CACHE_TTL_SEC` | Tenant config cache TTL | ### Bootstrap-only keys (require restart) These keys are not registered in the systemplane API. Change them via environment variables and restart: | Key | Env variable | Description | | ------------------------------ | ---------------------- | --------------------------------------------------------------------------------------- | | `tenancy.multi_tenant_enabled` | `MULTI_TENANT_ENABLED` | Enable multi-tenant infrastructure. Read once at startup; requires a restart to change. | | `app.env_name` | `ENV_NAME` | Application environment name | | `telemetry.enabled` | `ENABLE_TELEMETRY` | Enable OpenTelemetry | | `app.log_level` | `LOG_LEVEL` | Application log level (debug, info, warn, error, fatal) | | `server.address` | `SERVER_ADDRESS` | HTTP server listen address | | `postgres.primary_host` | `POSTGRES_HOST` | Primary database host | | `postgres.primary_port` | `POSTGRES_PORT` | Primary database port | | `postgres.primary_db` | `POSTGRES_DB` | Primary database name | | `redis.host` | `REDIS_HOST` | Redis host | | `rabbitmq.host` | `RABBITMQ_HOST` | RabbitMQ host | | `auth.enabled` | `PLUGIN_AUTH_ENABLED` | Enable authentication middleware | | `auth.host` | `PLUGIN_AUTH_ADDRESS` | Auth service address | ## Best practices *** Call `GET /system/matcher` to see all current runtime values before making any changes. This confirms what the process is actually using, which may differ from environment variables if previous PUT calls were made. Runtime application behavior varies by key, and worker changes can restart the affected worker. Test in a staging environment before applying to production. If a key is not visible in `GET /system/matcher`, it is bootstrap-only. Update the environment variable and restart the service — there is no runtime path for those values. A visible key can still be bootstrap-only when its documentation says so: the registered Swagger keys accept a runtime `PUT` but only take effect after a restart. ## Next steps *** Enable and configure tenant isolation. Configure how exceptions are dispatched to external systems. Configure transaction matching rules. Authentication, authorization, and data protection. # Matcher MCP server Source: https://docs.lerian.studio/en/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, so an AI assistant — or any MCP client — can operate Matcher on your behalf: inspect contexts, start reconciliation runs, work exceptions, and pull reports, all with the same permissions you already have. Matcher is only available if you've acquired the Matcher product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more. ## How it connects *** The server speaks **Streamable HTTP**. It runs as its own service alongside the Matcher API and exposes a single MCP endpoint (`POST /mcp`) plus a plain liveness probe (`GET /healthz`). 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 your platform team 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 " ``` For local development, the relay is also published as a public npm package — start it pointed at your Matcher API and connect to `http://localhost:4019/mcp`: ```bash theme={null} MATCHER_API_URL=https://your-matcher-api npx @lerianstudio/matcher-mcp ``` Any MCP client that supports Streamable HTTP works the same way: give it the URL and the `Authorization: Bearer ` header. ## Auth posture *** The server is a **stateless credential relay** — it adds no identity of its own: * **Bearer token for API calls.** Tools that dispatch Matcher API requests fail closed without a bearer token and 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, but it requires a bearer credential to report that one was relayed; without it, 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, so the relay can be scaled and restarted freely. To verify your client is wired correctly, call `mcp_whoami` with a bearer credential after connecting — it reports only that the credential arrived, never its value. ## What you can do with it *** The server exposes curated tool families covering configuration, reconciliation runs, exceptions and disputes, ingestion, and reporting, plus a generic bridge for operations with JSON request bodies. See [Matcher MCP tools](/en/matcher/mcp/matcher-mcp-tools) for the catalog. # Matcher MCP tools Source: https://docs.lerian.studio/en/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 — plus 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 deliberately 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 *** When a curated tool doesn't cover the operation you need, use the generic tools to inspect the API contract embedded when the server starts and, when the operation accepts a JSON body (or needs no body), 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. ## 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; returns a tool error when none is present. | # Governance Source: https://docs.lerian.studio/en/matcher/reference/matcher-governance Manage actor PII mappings, list audit-log archives and download archive objects, and re-verify Matcher's tamper-evident 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), and **audit logs** (immutable, hash-chained history with a read-only integrity check). With `AUTH_PROVIDER=plugin-auth` in multi-tenant mode, tenant identity comes from the JWT; Matcher rejects startup when `MULTI_TENANT_ENABLED=true` and `PLUGIN_AUTH_ENABLED=false`. `workos` currently resolves verified requests to the configured default tenant, so do not use it for tenant selection. Every governance route is scoped to the caller's tenant. Actor-mapping reads are split into two authorization tiers: the list (which omits `displayName` and `email`) versus the single-record de-anonymization read, so identity resolution stays separable 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`). Outside local, development, and test environments, set `ACTOR_PII_ENCRYPTION_KEY` to a base64-encoded 32-byte key before using actor mappings. If it is unset, Matcher continues to run, but PII-bearing mapping operations (upsert, single-record read, and pseudonymization) return an encryptor-required error; the PII-free list and delete paths do not require an encryptor. Mapping PII is never stored 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. It does not impose an opaque-ID format or redact the value, so an `actorId` that itself contains PII (such as an email address) appears in list rows as its stored value and is preserved by pseudonymization; use opaque identifiers if list access must stay PII-free. The `PUT` response and the single-record `GET` return cleartext identity. Only the single-record `GET` is gated behind the `deanonymize` permission: the `PUT` response is gated by write access alone and echoes the full stored record, including any stored field the caller did not submit, so treat actor-mapping write access as PII-revealing. 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. At least one of `displayName` or `email` must be supplied. ```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 it is gated behind the narrower `deanonymize` permission 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` they were written with (which can itself be an email address), so historical audit logs and archived files are not redacted. 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" ``` Pseudonymize keeps the record (PII scrubbed); delete removes it entirely. Choose pseudonymize when you must retain the audit linkage, delete when the record itself must not persist. ## Archives *** The archival worker is disabled by default (`ARCHIVAL_WORKER_ENABLED=false`). When you enable it and configure archival storage, aging audit-log partitions are compressed and moved to object storage. Archive retrieval routes are registered when archival object storage is available, independently of whether the worker is enabled. The list endpoint returns completed archives; the download endpoint issues time-limited URLs for archive objects. ### 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_q1", "dateRangeStart": "2024-01-01T00:00:00Z", "dateRangeEnd": "2024-03-31T23:59:59Z", "rowCount": 150000, "compressedSizeBytes": 10485760, "storageClass": "GLACIER", "checksum": "sha256:abc123def456...", "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`. Only `COMPLETE` archives are listed — in-progress and failed archives are never surfaced. ### 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": "sha256:abc123def456..." } ``` The download handler confirms tenant ownership but does not check `COMPLETE`; it presigns the stored `archiveKey`. That key and its checksum are assigned when an archive reaches `UPLOADED`, before `COMPLETE`. Treat `GET /v1/governance/archives/{id}/download` as a direct object-key path, not proof that archival completed. If you need only completed archives, select IDs from the list endpoint. Archive availability depends on the configured S3-compatible storage backend and lifecycle policy. Confirm any restore requirement with the owner of that storage deployment before relying on a download URL. ## Audit logs *** Instrumented governance workflows write immutable, per-tenant audit records. Each record is linked into a tamper-evident SHA-256 hash chain (`recordHash` = `SHA-256(prevHash || canonical content)`), so inconsistent changes are detectable. Use the verify endpoint below for the server-side 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": "reconciliation_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: the raw actor identifier captured when the record was written (an email address in this example). Audit records store that identifier as-is; it is not required to be an actor-mapping `actorId`. When a diff exceeds the outbox payload cap, `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 links to the previous one and matches its stored hash. Verification walks a contiguous span from the chain start, bounded by `maxRecords`, so `intact` speaks only for that inspected span. At the HTTP endpoint, a supplied `maxRecords` must be 1–10,000 (values outside that range return `422`); when omitted, Matcher uses the 10,000-record default. The check is strictly read-only: it detects tampering, never mutating a record. ```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, "verifiedCount": 1024, "truncated": false } ``` `intact` is `true` when the whole inspected span is unbroken; if a break is found, `firstBrokenSeq` reports the `tenantSeq` of the first failing record and `verifiedCount` reports how many held before it. `truncated` is `true` when the chain holds more records than the `maxRecords` inspection bound allowed. ### Get one audit log ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/{id}" \ -H "Authorization: Bearer $TOKEN" ``` 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 you are auditing. ## 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) | # Security Source: https://docs.lerian.studio/en/matcher/reference/matcher-security Configure Matcher's authentication, tenant isolation, transport posture, audit trail, and outbound-integration controls. Matcher's security behavior is deployment-configured. This page describes controls implemented by Matcher and the boundaries that remain the responsibility of your identity, network, and storage platforms. It is not a compliance certification. ## Authentication and authorization *** Matcher uses `AUTH_PROVIDER` to select authentication behavior: | Provider | Behavior | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `plugin-auth` | Matcher delegates token trust and permission decisions to the configured `plugin-auth` service through `lib-auth`. Matcher holds no local JWT signing secret. | | `workos` | Matcher verifies bearer-token signatures against WorkOS JWKS and evaluates its RBAC policy locally. | | `disabled` | Matcher does not enforce bearer-token authentication or authorization. | `AUTH_PROVIDER` is derived from `PLUGIN_AUTH_ENABLED` when it is unset: enabled selects `plugin-auth`; disabled selects `disabled`. `PLUGIN_AUTH_ADDRESS` is required for the `plugin-auth` provider. The `workos` provider requires `PLUGIN_AUTH_ENABLED=true` and its WorkOS configuration. Legacy `AUTH_ENABLED` and `AUTH_SERVICE_ADDRESS` remain aliases for the current `PLUGIN_AUTH_*` variables; conflicting aliases prevent startup. When authentication is enabled, protected API operations require a bearer token: ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/contexts" \ -H "Authorization: Bearer $TOKEN" ``` Do not maintain a static permission inventory in deployment runbooks. Permission requirements are defined with the route and provider policy, and can evolve with the product. Use the API reference and the authorization-provider configuration when assigning roles. ## Tenant isolation *** In authentication-disabled configurations, Matcher uses `DEFAULT_TENANT_ID` and `DEFAULT_TENANT_SLUG`. With `AUTH_PROVIDER=plugin-auth` in single-tenant mode, an authenticated request uses a valid `tenant_id` or `tenantId` claim when present and falls back to the default identity only when that claim is absent; a malformed or invalid tenant ID is rejected. In multi-tenant mode (`MULTI_TENANT_ENABLED=true`), `PLUGIN_AUTH_ENABLED` must also be true and the authenticated request must carry a valid `tenant_id` or `tenantId` claim; Matcher rejects startup when `MULTI_TENANT_ENABLED=true` and `PLUGIN_AUTH_ENABLED=false`. `AUTH_PROVIDER=workos` currently resolves verified requests to `DEFAULT_TENANT_ID`; do not use it for tenant selection. Matcher does not accept a caller-controlled tenant selector from query parameters or request bodies. Tenant Manager resolves a dedicated PostgreSQL pool for each tenant. The default tenant uses the root pool; Matcher does not use PostgreSQL `SET search_path` to switch tenant schema. Treat database credentials, network boundaries, and Tenant Manager configuration as part of the isolation boundary and verify them in your deployment. ## Transport and infrastructure connections *** Matcher can terminate TLS with `SERVER_TLS_CERT_FILE` and `SERVER_TLS_KEY_FILE`, or operate behind a trusted TLS-terminating proxy with `TLS_TERMINATED_UPSTREAM=true`. The certificate and key must be configured together. TLS enforcement for dependencies is opt-in. Set the applicable flag to make startup fail when its connection configuration does not declare TLS: * `POSTGRES_TLS_REQUIRED` * `POSTGRES_REPLICA_TLS_REQUIRED` * `REDIS_TLS_REQUIRED` * `RABBITMQ_TLS_REQUIRED` * `OBJECT_STORAGE_TLS_REQUIRED` These flags protect the configured dependency connections; they do not replace ingress, network, certificate, or storage-security controls supplied by the deployment. ## Audit trail and actor mappings *** Matcher writes audit records for instrumented mutation workflows. Audit records are append-only and connected through a per-tenant, tamper-evident hash chain. The verification endpoint is read-only and reports the result for the records it inspected. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/verify" \ -H "Authorization: Bearer $TOKEN" ``` Actor mappings can associate an opaque actor ID with a display name and email. Outside local, development, and test environments, set `ACTOR_PII_ENCRYPTION_KEY` to a base64-encoded 32-byte key before using them. When it is unset, actor-mapping operations return an encryptor-required error; Matcher never stores actor-mapping PII in plaintext. Audit records are handled separately and can retain the raw `actorId`, which can be an email address. With a key, Matcher encrypts stored actor-mapping PII and supports pseudonymize and delete operations. Determine retention, privacy, and legal obligations for your deployment separately. ## Outbound integrations *** Exception-dispatch connectors use SSRF controls that reject private, loopback, and link-local destinations by default. Review any configuration that permits private targets before using it in production. Webhook and callback flows have their own verification and idempotency mechanisms. Configure shared secrets or trusted source IP ranges only through the relevant connector settings; do not rely on this page as a protocol contract. Use the API reference for the headers and payload contract of a specific integration. ## Operational checklist *** * Select and test the intended authentication provider before exposing Matcher. * Enable authentication before enabling multi-tenant mode. * Require TLS for each dependency that must not accept plaintext connections. * Keep authorization roles least-privileged and review them in the identity provider. * Monitor audit records and independently investigate a failed chain verification. * Keep storage, backups, certificates, and secrets protected by deployment controls. ## Next steps *** Review which runtime values can change without restarting Matcher. Manage actor mappings, audit logs, and archives. # Context schedules Source: https://docs.lerian.studio/en/matcher/ui/context-schedules Automate reconciliation from the Matcher UI by adding cron-based matching schedules to a context, with a live preview of the expression. Use the **Schedules** tab in a context's setup page to automate reconciliation execution. Each matching schedule defines when the Matcher engine automatically runs matching for the selected context. Matching schedules set **when automated matching runs** for the reconciliation context. They do not pull any data — to schedule when a source ingests data, open a source and manage its ingestion schedules. ## Accessing schedules *** 1. Navigate to **Configure → Contexts** in the left sidebar. 2. Click a context to open its setup page. 3. Select the **Schedules** tab. The Schedules tab applies to an existing context. For new contexts, save the context first. ## Schedule list *** The list shows every matching schedule configured for the context. Each row displays: * The cron expression * An **Enabled** or **Disabled** badge * **Last run · Next run** timestamps in UTC; the UI shows `–` for **Last run** until a schedule has run and for **Next run** when it is disabled * Inline edit (pencil) and delete (trash) buttons ## Creating a schedule *** In the **Schedules** tab, click the **Add matching schedule** button. A dialog titled **Add matching schedule** opens. Enter the schedule in the **Cron expression** field (placeholder `0 2 * * *`, up to 100 characters). As you type, a live preview appears below the field: a human-readable phrase such as "Runs: Every day at 02:00" for common shapes, "Valid schedule expression." for valid but uncommon shapes, or an error if the expression is malformed. The preview is a hint and does not prevent submission. It recognizes interval and named-macro syntax such as `@every 15m` even though Matcher matching schedules do not accept it; the server accepts only a standard five-field cron expression and applies its cadence policy when you save. The **Enabled** checkbox is selected by default. Leave it selected to create an active schedule, or clear it to create a disabled one. A disabled schedule remains configured, has no next-run time, and is not selected in later worker polls. Click **Create matching schedule**. Matcher evaluates a **cron expression** in UTC. It uses five space-separated fields: `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 = Sunday) | Common special characters: * `*` — any value * `,` — list of values (e.g., `1,15,30`) * `-` — range (e.g., `9-17`) * `/` — step (e.g., `*/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 `@every …` syntax and named macros are not supported. When both day-of-month and day-of-week are restricted (not bare `*`), Matcher uses standard cron OR semantics: a match in either field triggers the schedule. The server requires at least five minutes between firings: `*/5 * * * *` is the smallest permitted cadence; for example, `* * * * *` and `*/2 * * * *` are rejected. Need to validate an expression? Use [crontab.guru](https://crontab.guru) to preview when it will run. ## Editing a schedule *** Click the pencil button on a schedule row. The **Edit matching schedule** dialog opens with the current cron expression and **Enabled** state pre-filled. Update the fields and click **Save changes**. If the schedule is enabled, saving recalculates **Next run** from the current UTC time; disabling it removes that timestamp. The edit dialog shows only the cron expression and the Enabled checkbox. **Last run** and **Next run** timestamps appear on the list row. ## Deleting a schedule *** Click the trash button on a schedule row. A **Delete matching schedule?** confirmation dialog appears; click **Delete** to remove the schedule permanently. Deleting a schedule is irreversible. It stops the schedule from being selected in future worker polls, but it cannot cancel a matching run that a worker has already picked up. # Context sources Source: https://docs.lerian.studio/en/matcher/ui/context-sources Add and edit data sources from the Sources tab of a context's setup page in the Matcher UI — pick a type and side, set file dialect and ingestion policies. Use the **Sources** tab in a context's setup page to configure and manage data sources for reconciliation. Sources feed the two sides of the context: each source declares which side it belongs to and how the engine should treat its data. ## Accessing the Sources tab *** Navigate to **Configure → Contexts** in the left sidebar. Select a context from the list to open its setup page. Select the **Sources** tab. The global **Active context** control in the sidebar lets you change the active reconciliation context at any time. ## Sources list *** This view shows up to 100 sources and has no paging controls. Sources are grouped by side (**LEFT side** / **RIGHT side**). Each active source row shows its name and type. Depending on your permissions, it can also offer actions to manage ingestion schedules, run a manual fetch, extract from a document, edit, or archive. A **Show archived** toggle includes archived sources; they carry an **Archived** badge and offer only restore. At least one live `LEFT` source and one live `RIGHT` source are required for setup readiness; archived sources do not count. ## Creating a source *** In the **Sources** tab, click the **Add source** button. A dialog opens. Fill in the source details: | Field | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------ | | **Name** (required) | A descriptive name for the source (up to 50 characters) | | **Type** (required) | The source type. New sources start with `BANK` selected: `LEDGER`, `BANK`, `GATEWAY`, `CUSTOM`, or `FETCHER` | | **Side** (required) | Which side of the reconciliation the source feeds. New sources start with `LEFT` selected: `LEFT` or `RIGHT` | Configure how the engine parses and ingests the source's files: | Section | Description | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **File dialect** | **Encoding** (`UTF-8`, `UTF-8 with BOM`, `Windows-1252`, `ISO-8859-1`), **Delimiter** (comma, semicolon, tab, pipe), **Decimal style** (dot `1234.56` or comma `1.234,56`), and **Date style** (ISO, ISO with offset, day first, month first). Leave on **Auto** to use the server defaults (UTF-8 / comma / dot / ISO); Auto does not detect a file's dialect | | **Duplicate handling** | What to do when a file repeats an external ID already present for this source: **Keep first (default)** silently keeps the original and drops the repeat, **Reject as errors** records an import error, and **Flag as exception** opens a duplicate exception on the surviving transaction | | **Blank reference handling** | What to do when a row has no mapped external ID: **Reject as errors (default)** records an import error; **Import for review** creates a pending-review transaction in the Unmatched workbench | | **camt.053 options** | For camt.053 uploads: **Date basis** (Booking date `BookgDt` or Value date `ValDt`) and which field to use as the **Entry reference** (`NtryRef`, `EndToEndId`, or `TxId`). Leave both controls on **Auto** for booking date and `NtryRef` | | **Advanced config (JSON)** | Optional source configuration as a JSON object. Leave empty for defaults. To fail an ingestion job only when failed rows strictly exceed a threshold, set `fail_on_error_rate_percent` to an integer from 1 through 99; omit it to never auto-fail because of row-error rate | Click **Create source**. Field mapping is not part of the source form — configure how source columns map to reconciliation fields in the separate **Field maps** tab. For CSV, JSON, and XML uploads, each live source needs a field map for setup readiness. **camt.053 exception:** selecting a non-**Auto** camt.053 option declares the source self-mapped for readiness. On every camt.053 upload, the parser uses its embedded ISO 20022 mapping and ignores a field map; if both camt.053 controls remain **Auto**, no `camt053` declaration is saved, so readiness still expects a field map. ## Source types *** | Type | Description | | ----------- | --------------------------------- | | **LEDGER** | Data from the internal ledger | | **BANK** | Data from a bank statement feed | | **GATEWAY** | Data from a payment gateway | | **CUSTOM** | Data from a bespoke feed | | **FETCHER** | Data from a discovery-engine pull | ## Editing a source *** Click the edit button on a source to open the dialog. Update the fields as needed and click **Save changes**. ## Archiving and restoring a source *** Sources are never hard-deleted — archiving is the only removal path. Click the archive button on a source. An **Archive source?** confirmation dialog appears; click **Archive**. The source moves out of the active list. Turn on **Show archived** to see it with its **Archived** badge. To bring it back, click the restore button on the archived source and confirm in the **Restore source?** dialog. # Exception details Source: https://docs.lerian.studio/en/matcher/ui/exception-details Open a reconciliation exception in the Matcher UI — inspect the available transaction data, review candidate matches, resolve or dispatch the exception, and view its comments and history. The **Exception details** page in the Matcher UI brings together the exception record, its available transaction data, resolution controls, comments, and action history. ## Accessing exception details *** On the [Exceptions](/en/matcher/ui/managing-exceptions) page, select the linked **Reason** in the exception row to open its detail page. You can also focus a queue row and press Enter. Both paths carry the queue's context scope when one is active. Matcher exception detail page showing the transaction information, candidate matches, comments, and action history ## Page layout *** The page header always uses the title **Exception details**; it does not use the exception's reason or ID as its title. It includes a back button and, depending on your permissions, can show **View audit trail**, **Assign**, **Open dispute**, **Dispatch**, **Adjust entry**, **Resolve**, and **Force match**. These controls are permission-gated, not selected by a configured workflow. For an exception in **RESOLVED** or **PENDING\_RESOLUTION**, the UI hides **Open dispute**, **Dispatch**, **Adjust entry**, **Resolve**, and **Force match**. **Assign** can still be shown to an operator with assignment permission, even in those states. The **Match to this** action belongs to the candidate panel rather than the header and requires manual-match permission. **View audit trail** appears only to an administrator or to a user with audit-log read permission; it opens a slide-over with the exception's immutable governance audit events. Below the header, a status rail shows the exception ID, its severity and status badges, and the **Updated** timestamp. The body is a three-column sheet: the **Transaction**, **Information**, **Candidate matches** or **Resolution**, and **Comments** panels fill the two left columns, while the **History** panel runs the full height of the right column. ### Queue navigation The queue controls work only within the currently loaded page of the filtered Exceptions queue. When the exception is present in that session-scoped snapshot and the page has more than one item, **Previous exception** / **Next exception** buttons and a position indicator let you move among those siblings. They do not traverse to another cursor page; the boundary button is disabled. The **Advance to next after resolving** checkbox appears only for a non-terminal exception that has a next sibling. After a successful **Resolve**, **Force match**, **Adjust entry**, or **Match to this** action, it opens that recorded next sibling when selected. At the last item, there is no next sibling and the checkbox is not shown. If the browser cannot persist the session snapshot, the page simply omits sibling navigation. ## Panels *** ### Transaction The exception record itself supplies its transaction ID. With a resolved reconciliation context, the page enriches that ID from the unmatched report and shows **Amount**, **Currency**, **Transaction date**, and **Source**. It is not an arbitrary full transaction-payload viewer. If the page has no context from the route or the current session, use the context selector in the panel header. If enrichment remains unavailable—for example, because the transaction is already matched or outside the report window—the panel still shows the transaction ID and explains why the fuller data is unavailable instead of showing a zero amount. ### Information The **Information** panel displays read-only details about the exception: | Field | Description | | ------------------- | ---------------------------------------------------------------------------------- | | **Reason** | The localized exception-reason code or free-text reason recorded for the exception | | **Assigned to** | The person responsible for resolving the exception, if assigned | | **Due date** | The resolution deadline, if set | | **External system** | Reference to the external target, if dispatched | | **External issue** | External issue identifier, if available | | **Transaction** | The related transaction ID (copyable) | | **Originating run** | The most recent match run that surfaced the exception (copyable when present) | | **Created** | When the exception was created | | **Updated** | When the exception was last updated | ### Candidate matches For an exception that is not **RESOLVED** or **PENDING\_RESOLUTION**, the **Candidate matches** panel requests counterpart candidates for the exception transaction in one reconciliation context. It requests up to 50 ranked proposals by default; the candidate endpoint accepts a limit from 1 through 200. Each proposal has a required 0–100 score, displayed with **Strong**, **Plausible**, or **Weak** confidence bands. The panel also shows the candidate's amount, currency, identifier, rule type, and date; its amount and date deltas; and the score rationale for amount, currency, date, reference similarity, and any configured composite match fields. Candidates are 1:1 opposite-side counterparts ranked over raw amounts: fee normalization and FX-variance bands are not applied here. The panel requires a context. If none is available, it asks you to select one; an empty result is distinct from an error, and a request failure shows **Retry**. If you have manual-match permission, **Match to this** opens **Match to this candidate?** for confirmation. Confirming links the two transactions into a confirmed match group, removes them from the unmatched queue, and refreshes the exception. ### Resolution For **RESOLVED** and **PENDING\_RESOLUTION** exceptions, a **Resolution** panel replaces the candidate panel. It can show the **Resolution type**, **Resolution reason**, **Resolution notes**, **Resolved by**, and **Resolved at** values recorded for the exception. ### Comments The **Comments** panel shows each comment's author, relative timestamp, and content. Comments are returned oldest first. Enter text and click **Send**, or press Ctrl+Enter / Cmd+Enter. A comment must contain text and the service accepts up to 5,000 characters. Only your own comments show a delete control. Deleting one requires confirmation and permanently removes it. Adding comments is disabled for exceptions in **RESOLVED** or **PENDING\_RESOLUTION**, but existing comments remain visible. ### History The **History** panel shows an action timeline. Each entry contains the action description, the actor (or "system" for automated actions), and a relative timestamp. New entries are published asynchronously, so an action can take roughly two to three seconds to appear. ## Actions *** The header contains the lifecycle actions; their visibility follows the permissions and status rules above. Manual matching is available from **Candidate matches**. **Assign** Opens the **Assign exception** dialog. The **Assignee** field starts with the current assignee when there is one; enter a nonempty assignee of up to 255 characters and click **Assign**. **Open dispute** Opens the **Open dispute** dialog. Select a **Category** — Bank Fee Error, Unrecognized Charge, Duplicate Transaction, Amount Mismatch, or Other — and provide a **Description**. The dialog requires both fields and limits the description to 2,000 characters. Then click **Open dispute**. Evidence submission and dispute closure happen on the dispute's own detail page. See [Managing Disputes](/en/matcher/ui/managing-disputes). **Dispatch** Opens the **Dispatch to External System** dialog to send the exception to an external system for handling: | Field | Description | | ------------------- | ---------------------------------------------------------------------------------------------- | | **Target system** | Required destination: JIRA, ServiceNow, Webhook, or Manual | | **Queue / Project** | Optional queue or project identifier, up to 255 characters (for example, `OPS-RECONCILIATION`) | If the selected target system does not have a connector configured, dispatch fails. Verify that the integration is set up in [Exception Routing](/en/matcher/configuration/matcher-exception-routing) before dispatching. **Adjust entry** Opens the **Adjust entry** dialog to record a monetary correction that resolves the exception. Every field is required; these fields are not conditional on a workflow configuration: | Field | Description | | ------------------ | --------------------------------------------------------------------------------------------------------- | | **Reason code** | Amount correction, Currency correction, Date correction, or Other | | **Amount** | A positive decimal amount; zero and negative amounts are rejected by the service | | **Currency** | A three-letter currency code | | **Effective date** | Required date; the dialog defaults it to the current UTC date | | **Notes** | Required explanation. The dialog accepts up to 2,000 characters, while the service accepts at most 1,000. | Click **Record adjustment** to submit the adjustment and resolve the exception. **Resolve** Opens the **Resolve exception** dialog to mark the exception resolved directly. **Resolution** is required and limited to 255 characters. Add an optional **Reason** of up to 1,000 characters for the record, then click **Resolve**. **Force match** Opens the **Force match** dialog to override normal matching and resolve the exception: | Field | Description | | ------------------- | --------------------------------------------------------------------------------------------------------- | | **Override reason** | Policy Exception, Ops Approval, Customer Dispute, or Data Correction | | **Justification** | Required explanation. The dialog accepts up to 2,000 characters, while the service accepts at most 1,000. | Click **Confirm force match** to resolve the exception. # Executing a reconciliation Source: https://docs.lerian.studio/en/matcher/ui/executing-reconciliation Trigger the Matcher engine from the Reconciliations screen — pick Dry run to preview or Commit to persist, then review matches, unmatched counts, and exceptions. Use the **Run match** panel on the **Reconciliations** screen to trigger the matching engine manually for the selected context. The Console submits this action synchronously: the run completes inside the request rather than being queued. Use a dry run to evaluate rules and inspect its summary statistics without writing matching artifacts; use a commit run to persist the results. ## Accessing the Run match panel *** Navigate to **Operate → Reconciliations** in the left sidebar. The Reconciliations screen shows the run history for the selected context, with the **Run match** panel for starting a new run. If you have not selected a context, the Console selects the first available one automatically; use the context selector to choose another. If no contexts are available, the screen shows an empty state instead of the panel. ## Run modes *** Choose a run mode from the **Mode** dropdown before starting: | Mode | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Commit** | Persist match groups, transaction-result changes, and exceptions. This is the default mode | | **Dry run** | Evaluate rules and show preview statistics without persisting match groups, transaction-result changes, or exceptions. Use it to test rules before committing | ## Starting a run *** Click **Run match**. The Console sends `mode` without `async`, so the run executes synchronously; it is not queued. Its response carries a terminal run status. Status badges can show the following states: | Status | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **QUEUED** | Only an API caller that explicitly sends `async: true` sees this state. The run is awaiting pickup by the enabled matching worker | | **PROCESSING** | The matching engine is actively processing the run | | **FINALIZING** | Match results are durable while deferred unmatched-break records are being written. An asynchronous polling client can briefly observe this non-terminal state before **COMPLETED** | | **COMPLETED** | The run finished successfully | | **FAILED** | The run encountered an error. The failure reason is displayed below the status badge | The Console fetches the run status after every launch. Its synchronous launch reaches a terminal status on that first fetch, so it does not continue refreshing. An API run submitted with `async: true` responds **QUEUED** and needs continued polling until **COMPLETED** or **FAILED**; asynchronous submission requires the match-run worker to be enabled, otherwise the service rejects the request instead of queueing it. On the run-detail page for a non-terminal run—for example, an asynchronous run opened from run history—the Console refreshes every two seconds for up to 90 attempts (about three minutes). **Stop watching** stops only the local refresh, not the server-side run; **Recheck status** restarts the refresh. The Console blocks the launch only when its pre-run check successfully returns a total of zero transactions. It then shows a warning and an **Import data** button. While that check is loading or fails, the Console does not claim that the context is empty and leaves the launch available. If a run still completes with zero candidates on both sides, the Console shows an import warning instead of the statistics or **View match groups**. ## Reviewing results *** For a completed, non-empty run with statistics, the panel shows a headline **Matches** figure and a breakdown of per-side statistics: | Statistic | Description | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | **Candidates (left)** / **Candidates (right)** | Transactions evaluated on each side | | **Auto-matched (left)** / **Auto-matched (right)** | Transactions matched automatically | | **Pending review (left)** / **Pending review (right)** | Transactions in matches awaiting manual review | | **Proposed (left)** / **Proposed (right)** | Transactions in proposed (not yet confirmed) matches | | **Unmatched (left)** / **Unmatched (right)** / **Unmatched (external)** | Transactions that could not be matched | | **Exceptions raised** / **Exceptions updated** | Newly created exceptions / pre-existing exceptions re-touched by the run. Both are zero for a dry run | Above the breakdown, the Console can show summary visualization for the statistics returned by the run. ## Dry-run results *** A dry run creates and completes a persisted run record with aggregate statistics, including calculated candidate, match, and unmatched counts. Its two exception statistics are explicitly zero. It does not persist match groups or items, transaction-result changes, or exceptions, so the Console presents the run as a statistical preview rather than persisted group-by-group results. Although **View match groups** can still open the run detail, no match groups were written by a dry run. Use the statistics to adjust rules, then switch to **Commit** and run again to persist results. ## Viewing detailed results *** After a completed **Commit** run, click **View match groups** to open the run detail and inspect the persisted groups for that run. A dry run has no persisted groups to inspect; use its summary statistics instead. # Managing actor mappings Source: https://docs.lerian.studio/en/matcher/ui/managing-actor-mappings Link opaque actor IDs to a human-readable display name and/or email in the Matcher UI, then pseudonymize or delete mappings. The **Actor mappings** page in the Matcher UI lets you associate opaque actor IDs with a human-readable display name and/or email. With the appropriate permissions, you can pseudonymize or delete mappings directly from the UI. ## Accessing the Actor mappings page *** Navigate to **Matcher > Settings > Actor Mappings** in the left sidebar. To open this page, you need `actor-mappings:read`; administrators are also admitted. Without this access, Matcher redirects you to the main Matcher page. These checks apply only when authentication is enabled: with `AUTH_PROVIDER=disabled`, Matcher enforces no authentication or authorization, so the detail and edit operations can expose actor-mapping PII to anyone who can reach the deployment. ## Browsing actor mappings *** Matcher Actor Mappings page, used to link an opaque actor ID with human-readable details The page uses cursor pagination and requests up to 20 mappings per UI page: | Column | Description | | ------------ | --------------------------------------------------------------------------------------------------------------- | | **Actor ID** | The opaque identifier maintained by Matcher | | **Created** | Date and time the mapping was created | | **Updated** | Date and time of the last update | | **Actions** | Appears only when you may edit, pseudonymize, or delete; it contains only the buttons you are authorized to use | Type in the **Actor ID prefix** field to filter the table live — there is no separate search button. Use the **Previous** / **Next** buttons below the table to move through the available pages. Display name and email do not appear in the table. Encrypted PII is loaded only when you open a mapping's detail operations, such as editing. ## Permissions *** `actor-mappings:read` is the prerequisite for opening the page. Once you have that access, the header control and row actions are independently permission-gated: * **New mapping** requires `actor-mappings:write`. * **Edit** requires both `actor-mappings:write` and `actor-mappings:deanonymize`, because the edit sheet loads decrypted PII through the detail operation. * **Pseudonymize** requires `actor-mappings:pseudonymize`. * **Delete** requires `actor-mappings:delete`. If you have none of the row-action permissions, the **Actions** column is omitted. Individual buttons for permissions you lack are hidden. ## Creating an actor mapping *** Click the **New mapping** button in the page header. The **New actor mapping** sheet opens with the following fields: | Field | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Actor ID** | The opaque identifier (e.g. `user:550e8400-e29b-41d4-a716-446655440000`). Its maximum length is 255; longer values are rejected. Leading and trailing whitespace is automatically trimmed. It cannot be changed after creation. | | **Display name** | The human-readable name (e.g. "John Doe"). Optional, but you must provide a Display name, an Email, or both. | | **Email** | The email address (e.g. "[john.doe@example.com](mailto:john.doe@example.com)"). Optional, but you must provide a Display name, an Email, or both. If supplied, it must be a valid email address. | Click **Save**. ## Editing an actor mapping *** Click **Edit** on a mapping row. The **Edit actor mapping** sheet opens with the current values pre-filled. The Actor ID field is locked — actor ID cannot be changed after creation; only the display name and email can be modified. Leaving an existing **Display name** or **Email** field blank does not clear it: the UI omits blank values and the update preserves omitted stored values. The form also does not allow you to save when both fields are blank. Click **Save** to apply changes. ## Pseudonymizing an actor mapping *** Click **Pseudonymize** on a mapping row. The **Pseudonymize actor mapping?** confirmation dialog warns that the action changes audit deanonymization behavior. This operation replaces the display name and email with `[REDACTED]` while preserving the actor ID link. The UI has no dedicated undo action, and the original values are not restored automatically. A user with both write and deanonymize permission can later edit the mapping and manually enter replacement values. Click **Pseudonymize** to proceed. ## Deleting an actor mapping *** Click **Delete** on a mapping row. The **Delete actor mapping?** confirmation dialog warns that the action changes audit deanonymization behavior. Click **Delete** to proceed. Deletion permanently removes the mapping between the opaque actor ID and its stored display name and email. Assess any legal or retention obligation in your deployment separately. # Archives Source: https://docs.lerian.studio/en/matcher/ui/managing-archives Browse completed audit-log archives produced by Matcher archival jobs, filter them by their covered date range, and download them when you have access. The **Archives** page in the Matcher UI lets you browse completed audit-log archives for the current tenant. It is a retrieval page: Matcher archival-worker configuration controls archive creation, retention, and storage lifecycle; those settings are not changed here. ## Accessing the Archives page *** Navigate to **Matcher → Reporting → Archives** in the left sidebar. You need archive read access to open the page; administrators also have access. ## Filters *** Use the covered-date filters at the top of the page to find archives whose **entire** covered period is inside a specific window. This is containment, not overlap: an archive is returned only when its covered start is on or after **Covered from** and its covered end is on or before **Covered to**. | Filter | Description | | ---------------- | ------------------------------------------------------------------------------------------ | | **Covered from** | Lower bound for the archive's covered start date (required) | | **Covered to** | Upper bound for the archive's covered end date (required; a date includes that entire day) | Click **Apply filters** to run the search. Both dates are required, and **Covered from** must be before or equal to **Covered to**. Applying a new range returns to the first results page. When the page loads, the default covered window ends on the current UTC date and starts 90 days earlier. Adjust the dates to narrow or widen it. ## Archive table *** Matcher Archives list showing completed audit-log archives filtered by date range The table lists only **Complete** archives and displays the following columns: | Column | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Partition** | Name of the database partition that was archived | | **Date range** | Start and end of the archive's covered period, shown as UTC dates | | **Rows** | Number of audit-log rows captured in the archive | | **Size** | Compressed archive-object size | | **Storage** | Object-storage class recorded for the archive | | **Status** | Archive status; this list contains only **Complete** archives | | **Archived** | UTC timestamp when archival completed, not when its metadata record was created; `·` is shown if the timestamp is absent | | **Actions** | **Download** control, shown only to administrators or users with archive download access | The page requests 25 archives at a time. Use **Previous** and **Next** below the table to page through results; **Next** is available only when the service reports another page. ### Storage metadata The **Storage** column identifies the archive's object-storage class. **STANDARD**, **STANDARD\_IA**, and **ONEZONE\_IA** are immediately readable; **INTELLIGENT\_TIERING** moves objects between tiers; and **GLACIER\_IR** is instant-retrieval cold storage. **GLACIER** and **DEEP\_ARCHIVE** require a restore before download. ## Downloading an archive *** If you have archive download access, click **Download** on a row to prepare that archive's download. Matcher makes this request only after you click the control. On success, it returns a time-limited presigned URL, its UTC expiration timestamp, and the archive's `sha256:` checksum; the UI opens a safe download URL and displays the checksum so you can verify the downloaded bytes. The default presign duration is one hour (`archival.presign_expiry_sec` defaults to 3,600 seconds). It can be changed through archival configuration and is capped at seven days. For **GLACIER** and **DEEP\_ARCHIVE**, restore the object before downloading it. If object storage is unavailable, Matcher cannot prepare the download and the UI shows an error. # Audit logs Source: https://docs.lerian.studio/en/matcher/ui/managing-audit-logs Track instrumented reconciliation configuration and operational changes with a filterable, tamper-evident record for troubleshooting. The **Audit Logs** page in the Matcher UI provides a filterable record of instrumented changes made to reconciliation configuration and operations. Use it to investigate the actor, action, and recorded time for those workflows. ## Accessing the Audit Logs page *** Navigate to **Matcher → Reporting → Audit Logs** in the left sidebar. You need an administrator role or audit-log read access to open this page. ## Verifying chain integrity *** The **Tamper-evidence** panel sits above the filters. Click **Verify integrity** to re-verify the tenant audit hash chain. The check is read-only — it never alters a record — and returns one of two verdicts: | Verdict | Meaning | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **Chain intact** | Every inspected record links to the previous one and matches its stored hash. The panel reports how many records were verified. | | **Chain TAMPERED — integrity broken** | The hash chain is broken at a reported position. The audit log can no longer be trusted as tamper-evident — escalate immediately. | If the chain is longer than the inspection bound, the intact verdict notes that only the first records from the chain start were inspected and the most recent entries were not checked. A failed request shows an error with a **Retry** button — it is not a verdict. ## Filters *** Use the filter bar to narrow results. **Actor** is a free-text field of up to 255 characters. **Action** and **Entity type** are free-text fields of up to 50 characters each; none are dropdowns: | Filter | Description | | --------------- | --------------------------------------------------------------------------- | | **Actor** | The raw actor identifier recorded with the change (for example, `user-123`) | | **Action** | The operation performed (for example, `CREATE`, `UPDATE`, `DELETE`) | | **Entity type** | The type of resource that was changed (for example, `context`) | | **Date from** | Start date for the time range | | **Date to** | End date for the time range | Click **Apply filters** to run the search. Applying filters resets pagination to the first page. ## Audit log table *** Matcher Audit Logs table listing recorded user and system actions Each row in the table displays the following columns: | Column | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **Entry** | The audit log record ID, shown as a link to a dedicated detail page for that entry — useful for pasting into an incident ticket | | **Recorded** | When the change was recorded | | **Action** | The operation performed | | **Entity type** | The type of resource that was changed | | **Entity ID** | Identifier of the affected resource | | **Actor** | The raw actor identifier recorded with the change (for example, `user-123`), not a resolved person or system name | | **Chain** | The record's chain position (`#`) and its record hash. Records written before the hash chain existed show **No chain proof** | | **Changes** | A **View** button that opens the recorded JSON audit payload. Rows with no JSON changes show a placeholder | Use the **Previous** and **Next** buttons in the table footer to page through results. The footer also shows the current page number and the number of events on that page. ## Viewing change details *** Audit log entry showing the JSON audit payload recorded for the change Click **View** in the Changes column to open the **Audit log changes** panel. The panel describes the recorded change (action, entity type, and entity ID), then shows the recorded JSON audit payload as a formatted block. Its fields depend on the operation; it does not guarantee a before-and-after schema. You can also click the ID in the **Entry** column to open a dedicated detail page for that single entry. If the original diff was larger than the audit payload cap, the row shows a **Diff truncated** marker and the panel explains that the full change set was replaced with a marker envelope. The complete diff is not recoverable from that record. # Managing contexts Source: https://docs.lerian.studio/en/matcher/ui/managing-contexts Create and manage reconciliation contexts in the Matcher UI — set cardinality, interval, fee tolerance, aging, auto-match, then wire up sources, field maps, rules, and schedules. Use the **Contexts** page in the Matcher UI to create and manage reconciliation contexts. A context groups together the sources, field maps, rules, and schedules that define a specific reconciliation workflow. ## Accessing the Contexts page *** To open the **Contexts** page, select **Contexts** under the **Configure** section of the left sidebar. ## Context list *** Matcher Contexts list page showing the configured reconciliation contexts The table displays all reconciliation contexts with the following columns: | Column | Description | | ------------ | --------------------------------------------- | | **Name** | The context name | | **Type** | Matching cardinality (`1:1`, `1:N`, or `N:M`) | | **Interval** | How often reconciliation runs | | **Status** | Current state of the context | | **Created** | When the context was created | ### Context statuses | Status | Description | | ------------ | ---------------------------------------------------------- | | **Active** | The context is operational and accepts reconciliation runs | | **Paused** | The context is temporarily suspended | | **Draft** | The context is being configured and is not yet active | | **Archived** | The context is no longer in use and has been archived | Click a context name to open its detail page. Contexts are archived and restored rather than deleted — see [Context actions](#context-actions) below. ## Creating a new context *** Click **New context** on the Contexts page to open the guided setup wizard. The wizard is the single path for creating a context; the first step collects the context configuration: Fill in the general fields: | Field | Description | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Name** (required) | A descriptive name for the context (e.g., "Bank Reconciliation Q1") | | **Type** (required) | Matching cardinality: `1:1` (one-to-one), `1:N` (one-to-many), or `N:M` (many-to-many) | | **Interval** (required) | How often reconciliation runs. Choose the `Daily`, `Weekly`, or `Monthly` preset, or select `Custom` and type any cadence label (e.g., "every 4 hours"). The backend stores the interval as a free label | Optionally configure **Fee normalization** — how fees are reconciled and the allowed variance: | Field | Description | | ----------------- | ----------------------------------------------------------------------- | | **Mode** | Method used to normalize fee values before comparison: `NET` or `GROSS` | | **Abs tolerance** | Maximum absolute difference allowed between matched amounts | | **Pct tolerance** | Maximum percentage difference allowed between matched amounts | Optionally configure **Open-item aging** to flag a carried open-item residual as aged once it stays open past a number of days: | Field | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------- | | **Aging threshold (days)** | Number of days after which an open item is flagged as aged. Leave empty to disable aging for this context | | **Aging calendar** | How the age is measured: `Raw calendar days`, `US Federal`, or `Brazil ANBIMA` business days | Optionally enable **Auto-match on upload**. When turned on, a match run is triggered automatically when a file is ingested into this context. Click **Create context** in the wizard footer. The wizard then guides you through adding sources, field maps, and match rules. ## Context detail page *** Click a context name in the list to open the context detail page. The detail page is organized into six tabs: Matcher context detail page with its Sources, Rules, Fee Rules, and Schedules sections | Tab | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **General information** | The context's current settings: name, type, interval, fee tolerance, aging, and auto-match configuration | | **Sources** | Data sources configured for this context. See [Context sources](/en/matcher/ui/context-sources) | | **Field maps** | Field mappings that bind each source's columns to the reconciliation fields | | **Rules** | Match rules configured for this context. See [Context rules](/en/matcher/ui/context-rules) | | **Fee rules** | Fee rules that bind fee schedules to this context's transactions | | **Schedules** | Reconciliation schedules that define when runs execute automatically. See [Context schedules](/en/matcher/ui/context-schedules) | ### Context actions The page header shows the context name, status badge, type, interval, and creation date, along with the following actions: | Action | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Edit context** | Opens a dialog to update the context name, interval, status (`ACTIVE`, `PAUSED`, or `ARCHIVED`), auto-match, fee normalization, and aging settings. Click **Save changes** to apply. To pause or resume a context, change its status here | | **Clone context** | Creates a new context from this setup. You can choose whether to include sources and rules in the clone | | **Archive context** | Retires the context. Archiving replaces deletion — the context and its history are preserved | | **Restore context** | Brings an archived context back | Sources, field maps, rules, fee rules, and schedules are all managed from within the context detail page. There are no separate standalone pages for these entities. # Managing disputes Source: https://docs.lerian.studio/en/matcher/ui/managing-disputes Track formal disputes raised against reconciliation exceptions in the Matcher UI — filter by state and category, submit evidence, and record dispute outcomes. Use the **Disputes** page in the Matcher UI to view and manage all disputes across your reconciliation exceptions. Disputes are formal challenges raised against exceptions, allowing you to submit evidence, track state changes, and record resolutions. ## Accessing the Disputes page *** Navigate to **Matcher > Exception Handling > Disputes** in the left sidebar. The page displays a paginated list of all disputes for the selected context, with a status rail summarizing Open, Pending, Won, and Lost counts. ## Filters *** Use the filter bar at the top of the list to narrow results: | Filter | Options | | --------------------------- | ----------------------------------------------------------------- | | **State** | Draft, Open, Pending Evidence, Won, Lost | | **Category** | Bank Fee Error, Unrecognized Charge, Duplicate Transaction, Other | | **Date from** / **Date to** | Restrict results to a date range | | **Sort by** / **Order** | Sort results, newest or oldest first | A **Clear filters** button appears when any filter is active. Amount Mismatch is a create-only category: you can open a dispute with it, but the list filter does not offer it because the disputes API does not accept it as a filter value. ## Disputes table *** Matcher Disputes list page showing disputes filtered by status and category Each row in the data table displays the following columns: | Column | Description | | ------------- | ------------------------------------------------------------------ | | **Dispute** | The dispute identifier, shown as a link to the dispute detail page | | **Exception** | The related exception identifier | | **Category** | The dispute category | | **State** | Current state | | **Opened by** | Who opened the dispute | | **Created** | When the dispute was created | Click a dispute ID to open the dispute detail page. ## Dispute states *** | State | Description | | -------------------- | ------------------------------------------------------------ | | **Draft** | The dispute has been created but not yet formally opened | | **Open** | The dispute has been opened and is awaiting review | | **Pending Evidence** | Additional evidence has been requested or is being collected | | **Won** | The dispute was resolved in favor of the challenger | | **Lost** | The dispute was resolved against the challenger | ## Dispute categories *** | Category | Description | | ------------------------- | ---------------------------------------------------------------------------------------------- | | **Bank Fee Error** | A discrepancy caused by an incorrect bank fee | | **Unrecognized Charge** | A charge that cannot be identified or attributed | | **Duplicate Transaction** | A transaction that appears to have been processed more than once | | **Amount Mismatch** | The amounts between source and target do not match (create-only; not offered as a list filter) | | **Other** | Any other dispute category not covered above | ## Dispute detail page *** Click a dispute ID to open its detail page. The page header shows the dispute with the **Submit evidence** and **Close dispute** actions, followed by a status rail (Opened by, Updated, State) and three panels: **Information**, **Resolution**, and **Evidence**. ### Information The **Information** panel displays read-only details about the dispute: | Field | Description | | --------------- | ----------------------------------- | | **Exception** | The related exception ID (copyable) | | **Category** | The dispute category | | **Opened by** | Who created the dispute | | **State** | The current state | | **Description** | Full description of the dispute | | **Created** | When the dispute was created | | **Updated** | When the dispute was last updated | ### Evidence The **Evidence** panel lists all submitted evidence items, including who submitted each item, when, the comment, and an **Open attachment** link when a file is attached. If nothing has been submitted, the panel reads "No evidence submitted yet." To add evidence, click **Submit evidence** in the page header. The dialog has two tabs: | Tab | Fields | | --------------- | --------------------------------------------------------------------------------------- | | **Paste URL** | **Comment** (required) and **File URL (optional)** — an HTTPS link to a supporting file | | **Upload file** | **Comment** (required) and a file chooser — PDF, PNG, JPEG, or CSV, up to 10 MB | Evidence can be submitted while the dispute is in any non-terminal state: Draft, Open, or Pending Evidence. Once a dispute is Won or Lost, it is closed to new evidence. ### Resolution The **Resolution** panel shows the recorded outcome as plain text: the **Resolution** explanation and, if the dispute was reopened, the **Reopen reason**. To close a dispute, click **Close dispute** in the page header and fill in the dialog: | Field | Description | | -------------- | -------------------------------------------------- | | **Outcome** | Select **Won** or **Lost** | | **Resolution** | Free-text explanation of the resolution (required) | A dispute can be closed from any non-terminal state (Draft, Open, or Pending Evidence). # Managing exceptions Source: https://docs.lerian.studio/en/matcher/ui/managing-exceptions Review unmatched reconciliation exceptions in the Matcher UI — filter by status, severity, reason, or assignee, run bulk actions, and open items for resolution. Use the **Exceptions** page in the Matcher UI to view and manage reconciliation exceptions — transactions that could not be matched automatically and require human review. ## Accessing the Exceptions page *** Navigate to **Matcher > Exception Handling > Exceptions** in the left sidebar. The page displays a paginated list of all exceptions for the selected context. ## Filters *** Use the filter bar at the top of the list to narrow results: | Filter | Options | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Search** | Free-text search over transaction ID, reason, or external issue | | **Status** | Open, Assigned, Pending Resolution, Resolved | | **Severity** | Low, Medium, High, Critical | | **Reason** | Why the exception was raised — for example Unmatched, FX Rate Unavailable, Split Incomplete, Validation Failed, Source Mismatch, Duplicate Transaction, Fee Variance, Fee Data Missing, Fee Currency Mismatch, FX Rate Variance, Aged Break, or Over Settled | | **Assigned to me** | Show only exceptions assigned to you | | **From** / **To** | Filter exceptions by date range | | **Min age** | Show only exceptions at least this many days old — useful for surfacing aged breaks | You can sort results by severity, creation date, last update, or status using **Sort by** and **Order**; leave them unset for the default queue ordering. A **Clear filters** button appears when any filter is active. ## Exception table *** Matcher Exceptions list with status filters, showing unresolved reconciliation exceptions Each row in the data table displays the following columns: | Column | Description | | --------------- | ------------------------------------------------------ | | **Checkbox** | Select individual exceptions for bulk operations | | **Reason** | Why the exception was created (truncated in the list) | | **ID** | Unique exception identifier (copyable) | | **Status** | Current status badge | | **Severity** | Severity level badge | | **Assigned to** | The person responsible for resolving the exception | | **Updated** | Date and time of the last update | | **Actions** | Dropdown menu with a link to the exception detail page | Click on any row to view its full details on the [Exception Details](/en/matcher/ui/exception-details) page. ## Bulk operations *** Select multiple exceptions using the checkboxes in the first column. The **Bulk actions** bar shows how many are selected and offers **Select all N matching** to extend the selection to every exception matching the current filters. The following bulk actions are available: * **Assign** — Assign all selected exceptions to a team member by entering their email address. * **Resolve** — Resolve all selected exceptions with a shared resolution note. * **Dispatch** — Send all selected exceptions to an external system for handling. * **Clear** — Deselect all currently selected exceptions. After a bulk action runs, the bar reports how many succeeded and how many failed; failed exceptions stay selected so you can retry. ## Exception severity *** | Severity | Description | | ------------ | ----------------------------------------------------------------------- | | **Critical** | Requires immediate attention — high financial impact or compliance risk | | **High** | Important discrepancy that should be resolved promptly | | **Medium** | Moderate discrepancy that should be reviewed | | **Low** | Minor discrepancy with limited impact | ## Resolving exceptions *** From the [Exception Details](/en/matcher/ui/exception-details) page, you can resolve an exception using: * **Force Match** — Override normal matching with a policy justification * **Adjust Entry** — Correct the transaction amount, currency, or date to align the records * **Dispatch** — Send the exception to an external system for handling * **Add a comment** — Document your investigation for audit purposes For full details on each resolution option and the sections of the exception detail page, see [Exception Details](/en/matcher/ui/exception-details). # Managing fee schedules Source: https://docs.lerian.studio/en/matcher/ui/managing-fee-schedules Define flat, percentage, tiered, or expression-based fees so the reconciliation engine can automatically verify and flag fee discrepancies on matches. The **Fee Schedules** page in the Matcher UI lets you define expected fees — flat, percentage, tiered, or expression-based — that the reconciliation engine uses to automatically verify and flag fee discrepancies between matched transactions. The page also hosts the context-scoped **Fee rules** that bind schedules to transactions. ## Accessing the Fee Schedules page *** Navigate to **Matcher > Settings > Fee Schedules** in the left sidebar. ## Fee schedules table *** Matcher Fee Schedules list showing the expected-fee definitions used to flag fee discrepancies Each row in the data table displays the following columns: | Column | Description | | --------------------- | --------------------------------------------------------------------------- | | **Name** | The fee schedule name | | **Currency** | The currency code (e.g. BRL) | | **Application order** | How fee items are applied, shown as a badge: **PARALLEL** or **CASCADING** | | **Rounding** | The rounding mode and scale — e.g. "HALF\_UP @ 2" | | **Items** | The number of fee items in the schedule | | **Used by** | How many contexts reference the schedule through fee rules, or "Not in use" | | **Created** | Date the schedule was created | | **Actions** | Inline **View**, **Simulate**, **Edit**, and **Delete** buttons | ### Application order | Mode | Description | | ------------- | ------------------------------------------------------------------------------------------------------------- | | **Parallel** | All fee items are calculated independently against the original gross amount | | **Cascading** | Fee items are applied sequentially — each item uses the net amount after the previous item's fee was deducted | ### Rounding modes | Mode | Description | | ------------ | ---------------------------------------------------------------------------------- | | **Half Up** | Rounds to the nearest neighbor; rounds up on a 0.5 boundary | | **Bankers** | Rounds to the nearest neighbor; rounds to the nearest even digit on a 0.5 boundary | | **Floor** | Always rounds toward negative infinity | | **Ceil** | Always rounds toward positive infinity | | **Truncate** | Drops excess decimal places without rounding | ### Structure types | Type | Description | | -------------- | -------------------------------------------------------------------------------------------------- | | **Flat** | A fixed monetary amount (e.g. 5.00) | | **Percentage** | A decimal rate applied to the base amount (e.g. 0.015 = 1.5%) | | **Tiered** | Marginal tranches by base amount — each tier charges its rate on the slice up to its ceiling | | **Expression** | A formula over the gross amount plus transaction metadata keys, parsed and validated by the server | ## Creating a fee schedule *** Click the **New schedule** button at the top of the page. A slide-over sheet opens with the following sections. ### Schedule fields | Field | Description | | --------------------- | --------------------------------------------------------------------------- | | **Name** | A descriptive name for the schedule (e.g. "Bank Fees BRL") | | **Currency** | The three-letter currency code (e.g. BRL). Cannot be changed after creation | | **Application order** | Select Parallel or Cascading. Defaults to Parallel | | **Rounding mode** | Select one of: Half Up, Bankers, Floor, Ceil, Truncate. Defaults to Half Up | | **Rounding scale** | Number of decimal places for rounding (0–12). Defaults to 2 | ### Fee items After filling in the schedule fields, add at least one fee item. Click **Add item**: | Field | Description | | ------------- | ------------------------------------------------------------------------- | | **Name** | A descriptive name for the fee item (e.g. "Wire Fee") | | **Priority** | Determines the order in which items are applied (whole number, 0 or more) | | **Structure** | Select Flat, Percentage, Tiered, or Expression | Depending on the selected structure, additional fields appear: | Structure | Additional fields | Description | | -------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Flat** | **Amount** | The flat amount in the schedule currency (e.g. "5.00") | | **Percentage** | **Rate** | A decimal rate between 0 and 1 (e.g. "0.015" for 1.5%) | | **Tiered** | **Tiers** | A structured tier editor: each row has a **Rate** and an **Up to** ceiling. Add or remove rows with **Add tier** and the remove control; ceilings must strictly increase, and you can leave the last tier's ceiling blank for an open-ended tier | | **Expression** | **Expression** | A formula over the injected `gross` amount plus transaction metadata keys — operators `+ - * /` and parentheses, plus the functions `days_late(a, b)`, `days_between(a, b)`, `max(…)`, `min(…)`, `abs(x)`, and `clamp(x, lo, hi)`. Validated on save | You can add multiple items before submitting. Click **Create schedule** to save the fee schedule. Fee items are immutable after creation. To change item structures, create a new schedule. ## Editing a fee schedule *** Click **Edit** on a fee schedule row. The slide-over sheet opens with the current values pre-filled. You can modify the schedule name, application order, rounding mode, and rounding scale. The currency field is locked after creation, and fee items are shown read-only. Click **Save changes** to apply. ## Simulating a fee schedule *** Click **Simulate** on a fee schedule row to estimate fees for a given amount without recording anything. The **Simulate fees** dialog asks for: | Field | Description | | ---------------- | ----------------------------------------------- | | **Gross amount** | The amount to simulate against (e.g. "1000.00") | | **Currency** | Pre-filled from the schedule's currency | Click **Run simulation**. The results show: * **Gross** — The input amount * **Total fee** — The total fees deducted * **Net** — The amount remaining after fees If the schedule has multiple fee items, a breakdown table lists each **Item** with the **Base used** for its calculation and the **Fee** charged. ## Deleting a fee schedule *** Click **Delete** on a fee schedule row. The confirmation dialog warns that deleting permanently removes the schedule and can break fee rules that reference it. Click **Delete schedule** to proceed. If fee rules still reference the schedule, deletion is blocked and a **Fee schedule still in use** dialog lists the contexts whose fee rules reference it. Remove those fee rules first. ## Fee rules *** Below the schedule catalog, the page hosts a **Fee rules** section — context-scoped rules binding schedules to matching predicates. Select a context, then: * Click **New fee rule** to bind a fee schedule to a matching side and a set of field predicates. Predicates compare transaction fields with operators such as equals, does not equal, is one of, is present, greater than, at least, less than, at most, and in the inclusive range. * Use the inline **Edit** and **Delete** buttons on a rule row to change or remove it. # Managing matches Source: https://docs.lerian.studio/en/matcher/ui/managing-matches Review confirmed match groups inside a run's detail page in the Matcher UI — check confidence, unmatch incorrect groups, and pair unmatched transactions manually. There is no standalone Matches page in the Matcher UI. Match groups — the engine's confirmed pairings — live inside each reconciliation run's detail page, and manual matching happens in the **Unmatched** workbench. ## Viewing match groups for a run *** Navigate to **Matcher → Operate → Reconciliations** in the left sidebar. Select a run. The run detail page opens under the **Match groups** heading, showing the run's summary stats, a per-rule breakdown, and the reconciliation groups with confidence scores and item-level allocation for that run. Matcher Matches workbench listing matched transaction groups with their confidence scores Use the **Status** filter to narrow the groups; it is applied server-side, so it reflects the whole run, not just the loaded page. Each match group card shows the **Rule** that produced the group, the **Match rationale**, the group's transactions laid out as allocated-vs-expected items, and a **Confidence** meter labeled **High confidence**, **Medium confidence**, or **Low confidence**. While a run is still in progress, the page refreshes automatically as it advances and offers **Stop watching** and **Recheck status** controls. ## Unmatching a group *** If a group was matched incorrectly, click **Unmatch** on its card. The **Unmatch this group?** dialog requires a **Reason** explaining why the group is being unmatched. Confirm with **Unmatch**. The group's transactions return to the unmatched pool and become available for re-matching. ## Manual matching *** Transactions that were not matched automatically live in the **Unmatched** workbench at **Matcher → Resolve → Unmatched**. Select at least two transactions and click **Pair selected** to create a manual match; you can add an optional note explaining why the transactions belong together. For the full step-by-step walkthrough, see [Manual Matching](/en/matcher/ui/manual-matching). # Managing reconciliations Source: https://docs.lerian.studio/en/matcher/ui/managing-reconciliations Start and track reconciliation runs from the Matcher UI — trigger the engine from the Run match panel and follow every run in the run history. Use the **Reconciliations** screen in the Matcher UI to start reconciliation runs for your contexts and follow their progress. ## Accessing the Reconciliations screen *** Navigate to **Matcher → Operate → Reconciliations** in the left sidebar and select a reconciliation context. ## What you can do here *** Trigger the matching engine for the selected context — as a dry run to preview, or a commit run to persist matches. Review past and active runs, with status, matches, unmatched counts, and a per-run link to the exceptions it raised. The screen combines the **Run match** panel with the context's run history table. Selecting a run opens its detail page with the match groups it produced. ## Importing data first *** A run can only match transactions that have been imported. To upload transaction files, use **Operate → Import file** — the import flow walks you through picking the destination source and uploading the file. See [Uploading reconciliation data](/en/matcher/ui/uploading-reconciliation-data). If the selected context has no imported transactions yet, the **Run match** panel shows a warning with an **Import data** shortcut instead of starting an empty run. # Manual matching Source: https://docs.lerian.studio/en/matcher/ui/manual-matching Manually pair transactions the engine could not reconcile — select unmatched transactions in the Unmatched workbench and link them into a confirmed match group. Transactions that the automatic matching engine could not reconcile live in the **Unmatched** workbench. Use it to review the unmatched queue, select counterpart transactions, and pair them into match groups manually. ## Accessing the Unmatched workbench *** Navigate to **Matcher → Resolve → Unmatched** in the left sidebar and select a reconciliation context. ## The unmatched queue *** The workbench lists the context's unmatched transactions with the following columns: **Transaction**, **Source** (labeled with its side — left or right), **Amount**, **Status**, **Exception**, **Date**, and per-row **Actions**. By default the list is scoped by a date **Window**; set **Date from** and **Date to** and click **Apply filters**, or switch to **Show all (unbounded)** to browse all history. ## Selecting transactions *** Select transactions with the row checkboxes to build a pairing basket. A counter above the table shows how many transactions are on the page and how many are **In basket**. The basket keeps selections while you filter or page, and a **Clear** action empties it. When you select a transaction, the workbench can show **Suggested counterparts** — opposite-side unmatched transactions the engine considers plausible matches, each with a confidence score and the rationale behind it. ## Creating the match *** With at least two transactions in the basket, click **Pair selected**. A confirmation dialog (**Pair these transactions?**) summarizes the selection. Enter a reason in the **Note (optional)** field — why these transactions belong together. Confirm with **Pair selected**. The system creates a confirmed match group linking the selected transactions, and the rows leave the unmatched queue. A manual match requires at least two transactions, and they must come from at least two different sources. Manual matches are created with confidence 100 and confirmed immediately — to undo one, use the **Unmatch** action on the group in the run's match groups view (see [Managing matches](/en/matcher/ui/managing-matches)). ## Ignoring a transaction *** If an unmatched transaction should be excluded from matching altogether (for example, a duplicate or an already-reversed entry), use the row's **Ignore** action. The confirmation dialog explains that the transaction is excluded from matching and leaves the queue. # Matcher dashboard Source: https://docs.lerian.studio/en/matcher/ui/matcher-dashboard Watch reconciliation health for the active Matcher context — a balance bar, a double-entry worksheet, match-rate trend, cash impact, source breakdown, and SLA blotters. The **Reconciliation Dashboard** is the home page of the Matcher UI. It provides an at-a-glance overview of reconciliation health for the selected context: how much of the pool is matched, what remains open, how the match rate is trending, and where the unmatched cash exposure sits. ## Accessing the Dashboard *** Navigate to **Matcher → Home** in the left sidebar — the Dashboard is the Matcher UI's default landing page. All data on the Dashboard is filtered by the active context and by the **period selector** in the page header, which offers 7-, 30-, and 90-day windows (90 days by default). A compact context selector sits next to it. The Dashboard refreshes automatically every 30 seconds. A **Live** indicator and an **Updated** timestamp on the status rail show the last successful refresh; if a background refresh fails, the page flags the figures as possibly stale and offers a retry. ## Status rail *** A rail across the top of the board carries the context name, the selected window (for example, **90d**), the **Updated** timestamp with the **Live** pulse, and two exception figures: **Total pending** and **Pending Overdue**. ## Balance line and worksheet *** The hero of the page is a **balance bar** that scales matched against unmatched transactions — the conservation truth that matched + unmatched = total, with the match percentage alongside. Below it, the **Reconciliation Worksheet** posts the same truth as a double-entry worksheet: a **Cleared** column (matched transaction counts and amounts) against an **Open** column (unmatched counts and amounts, shown in credit-red when non-zero), closed with a **Total cleared** / **Total open** foot. ## Match rate over time *** A hairline plot shows the daily match rate over the selected period, with a delta against the previous point. Monitor it for downward trends that may indicate rule degradation or data-quality issues. The plot needs at least two data points to draw a trend. ## Exposure panels *** The bottom row is a connected sheet of up to three panels: | Panel | Description | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Cash impact** | The total **unmatched cash exposure** for the period, broken down by currency (with transaction counts) and **By Aging** buckets. Each aging bucket is toned by age: Recent, Watch, Aging, Stale. | | **Source Breakdown** | Matched vs. unmatched figures per source, so you can see which sources contribute the most unmatched items. The panel hides itself when no per-source breakdown exists for the period. | | **Exception SLA** | **SLA Compliance** percentage, **Resolved On Time** (with the late count), **Pending (within SLA)**, **Pending Overdue**, and **Avg Resolution** time, plus an **Exception Aging** breakdown by age bucket. | These panels help treasury and operations teams quantify the financial exposure from unresolved items and spot resolution bottlenecks. ## First-run experience *** If the Matcher installation has no contexts yet, the Dashboard redirects to the guided **setup** flow. If the selected context has no reconciliation data for the period, the page shows a **Run your first match** prompt whose action takes you to the next incomplete setup step (import data, create rules, or run a match). A self-retiring onboarding checklist appears above the board until the first run completes. ## Where the runs live *** The Dashboard summarizes health; it does not list runs. To start or watch reconciliation runs, use **Operate → Reconciliations** (see [Managing reconciliations](/en/matcher/ui/managing-reconciliations)); for the full run and import history, see [Operational history](/en/matcher/ui/operational-history). # Matcher UI pages Source: https://docs.lerian.studio/en/matcher/ui/matcher-module Configure contexts and match rules, import transaction data, run reconciliations, and resolve exceptions — all from a visual interface. The **Matcher UI** is the standalone web application for operating Matcher, Lerian's reconciliation engine. It lets you configure contexts and match rules, import transaction data, run reconciliation jobs, review results, and resolve exceptions — all through a visual interface. This page is a directory of every page in the UI; if you're new to it, start with the [Matcher UI overview](/en/matcher/ui/matcher-ui-overview). Matcher is only available if you've acquired the Matcher product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more. ## Accessing the Matcher UI *** The Matcher UI ships as its own web application, deployed alongside the Matcher API — it is not part of Lerian Console. Open the URL your platform team provides for your environment and sign in with your identity provider credentials. ## Active context *** The Matcher sidebar shows the **active context** — the reconciliation context you're working with. The active context applies across Matcher's pages, so switching it updates the data displayed in the Dashboard, Reconciliations, Unmatched, Exceptions, and other views. ## Home *** The **Home** page is the Reconciliation Dashboard: an at-a-glance overview of reconciliation health for the active context. See a detailed walkthrough of the Dashboard page, including the balance bar, reconciliation worksheet, match-rate trend, and exposure panels. ## Available pages *** The sidebar groups pages by workflow phase. ### Configure
Create and manage reconciliation contexts. Each context groups sources, rules, and schedules for a specific reconciliation workflow.
### Operate
Start reconciliation runs for your contexts and follow their progress; each run's detail page holds its match groups. Upload transaction files into a context's sources and track the resulting ingestion jobs.
### Resolve
Work the queue of transactions the engine could not match — pair them manually or ignore them. Review confirmed match groups and confidence scores inside each run's detail page.
### Exception Handling
View and resolve transactions that could not be matched automatically. Manage disputed transactions that require manual review and resolution.
### Reporting
Track reconciliation runs and data-import jobs for your reconciliation contexts. View a chronological record of all actions taken within the Matcher UI. Access archived reconciliation data for past periods.
### Settings
Define expected fee structures — flat, percentage, tiered, or expression-based — for automatic fee verification during reconciliation. Map external actors to internal entities for reconciliation matching.
## Advanced configuration *** For deployment-level configuration that affects how the Matcher service operates:
Enable tenant isolation for SaaS deployments — each client gets isolated infrastructure — database schema, message broker, and cache namespace. View and adjust Matcher settings at runtime without restarting the service.
# Operational history Source: https://docs.lerian.studio/en/matcher/ui/operational-history Review reconciliation runs and import jobs for any Matcher context on one operational history page, with live-refreshing tables and status badges. The **Operational History** page in the Matcher UI provides a centralized view of reconciliation runs and data-import jobs for a context. The page stacks two tables: **Run history** and **Import jobs**. Export jobs are not part of this page — they live on the **Reporting → Exports** page, next to the rest of reporting. See the Exports surface for report downloads. ## Accessing the Operational History page *** Navigate to **Matcher → Reporting → Operational History** in the left sidebar. ## Filtering by context *** A context selector in the page header scopes the page. Select a context to view its runs and import jobs; the status rail below the header confirms the context you are looking at. ## Run history *** The **Run history** table lists reconciliation runs for the selected context, newest first. While any run is still in progress, the table refreshes automatically every few seconds and shows a **Live** indicator. | Column | Description | | -------------- | -------------------------------------------------------------------------------- | | **Run** | Short run identifier, linking to the run's detail page | | **Mode** | Either **DRY\_RUN** (preview only) or **COMMIT** (results persisted) | | **Status** | Current run status displayed as a colored badge | | **Matches** | Number of match groups created | | **Unmatched** | Total unmatched transactions from both sides, highlighted when greater than zero | | **Split** | A mini balance bar visualizing the matched share of the pool | | **Started** | When the run started processing | | **Exceptions** | A **View exceptions** link that opens the exceptions queue filtered to this run | You can filter the table by run **Status** and by a **From**/**To** date window. Pressing Enter on a focused row opens the run's detail page — the same destination as the Run link. ### Run statuses | Status | Description | | -------------- | ------------------------------------------------------- | | **QUEUED** | The run is queued and waiting to be processed | | **PROCESSING** | The matching engine is actively processing transactions | | **FINALIZING** | The engine is finishing the run and writing results | | **COMPLETED** | The run finished successfully | | **FAILED** | The run encountered an error | ### Run modes | Mode | Description | | ------------ | ---------------------------------------------------------------------------------- | | **DRY\_RUN** | Simulates the reconciliation without persisting results — useful for testing rules | | **COMMIT** | Executes the reconciliation and persists all match results | ## Import jobs *** The **Import jobs** table lists the data-ingestion jobs for the selected context. Like the run history, it shows a **Live** indicator and refreshes while jobs are in flight. | Column | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------- | | **File** | Name of the uploaded file | | **Status** | Current job status as a colored badge; a job that finished with row failures is flagged **Completed with errors** | | **Rows** | Rows persisted out of the total parsed, with a note when duplicates were dropped | | **Failed** | Number of rows that failed ingestion | | **Created** | When the ingestion job was created | | **Completed** | When the ingestion job finished | Jobs with failed rows include a **View errors** link to the per-row error report. ### Ingestion job statuses | Status | Description | | -------------- | ---------------------------------------------------------- | | **QUEUED** | The job has been submitted and is waiting to be processed | | **PROCESSING** | The file is currently being ingested | | **COMPLETED** | The file was successfully ingested | | **FAILED** | The ingestion failed — check the file format and try again | # Uploading reconciliation data Source: https://docs.lerian.studio/en/matcher/ui/uploading-reconciliation-data Upload transaction files from your sources through the Matcher import flow so the engine can pair them in the next reconciliation run. Before a reconciliation run can match transactions, you need to import transaction data from your sources. The **Imports** screen handles the upload and tracks the resulting ingestion job on the same page. ## Uploading data *** Navigate to **Matcher → Operate → Import file** in the left sidebar. In the **Destination** panel, pick the reconciliation context and the source leg to ingest into. The panel confirms the destination is **Ready to ingest** — the context must be active before you can upload. In the **Upload file** panel, drop a transaction file or browse for one (one file at a time). Pick the file **Format** if it is not inferred from the extension. Supported formats are detected automatically; the panel lists the supported format families. For the full catalog — CSV, JSON, XML, ISO 20022 camt.053, CNAB, and acquirer layouts — see [Import formats](/en/matcher/imports/matcher-import-formats). Optionally click **Preview** to inspect how the file parses before committing to the upload. Click **Upload**. The upload progress is shown, and once accepted the page confirms the file is being processed. ## Tracking ingestion status *** After uploading, the active import's status appears on the same page — total rows, imported, and failed counts — until you **Dismiss** it or choose **Stop watching**. The **Import jobs** table below keeps the full ingestion history; it is also available on the [Operational History](/en/matcher/ui/operational-history) page. # Consuming events Source: https://docs.lerian.studio/en/streaming-hub/consuming-events Consume events from Streaming Hub — verify webhook HMAC signatures, handle rotation, read X-Lerian headers, deduplicate deliveries, and pull with cursor ack. Streaming Hub delivers events two ways. **Push** sinks (`webhook`, `sqs`, `rabbitmq`, `eventbridge`) send each matched event to a destination you own. A **pull** sink holds events on a server-side cursor that your consumer reads on its own schedule. This page covers what your consumer must do in each case. ## Verifying a webhook signature *** Every webhook delivery is signed with an HMAC over the request timestamp and the exact request body, so you can prove the request came from Streaming Hub and was not tampered with or replayed. Two headers carry the signature: | Header | Value | | --------------------- | ----------------------------------------------------- | | `X-Webhook-Signature` | `v1,sha256=` — the signature. | | `X-Webhook-Timestamp` | The Unix-seconds timestamp folded into the signature. | The signature is computed as: ``` signature_input = "v1:" + X-Webhook-Timestamp + "." + X-Webhook-Signature = "v1,sha256=" + hex( HMAC-SHA256(key = signing_secret, message = signature_input) ) ``` To verify a delivery: 1. **Read the raw body and the timestamp** — verify **before** parsing the body, over the exact bytes received. Any re-serialization changes the bytes and breaks the signature. 2. **Check freshness** — reject the request if `X-Webhook-Timestamp` is more than **5 minutes** from now. This is the replay-protection window. 3. **Recompute the signature** — build `signature_input` as above with your signing secret and hex-encode the HMAC-SHA256. 4. **Compare in constant time** — compare your `v1,sha256=` against the received `X-Webhook-Signature` with a constant-time (timing-safe) comparison. Reject on mismatch. 5. **Respond** — return `2xx` only after the signature and freshness both pass. The signing secret is the one you received when you created the subscription (or last rotated it). It is never sent in a header — only the derived signature is. See [creating a webhook subscription](/en/streaming-hub/managing-subscriptions) for where the secret comes from. ## Handling two signatures during rotation *** When you [rotate a signing secret](/en/streaming-hub/managing-subscriptions), the hub runs a **24-hour dual-sign overlap**. During the overlap each delivery carries **two `X-Webhook-Signature` headers** — one signed with the new secret and one with the previous secret — sent as **repeated headers**, so you can migrate to the new secret without dropping any delivery. Verify against both: recompute the expected signature with each secret you currently hold, and **accept the request if either received signature matches**. Once you have deployed and confirmed the new secret, retire the old one. Read `X-Webhook-Signature` as a **list of repeated header values** using your framework's multi-value header accessor (or its raw-headers view). Do **not** split a single joined string on commas: the signature value itself contains a comma (`v1,sha256=…`), so a naive comma-split corrupts it. Some HTTP stacks join repeated headers with `", "` by default — use the list accessor to get each value intact. ## Correlation headers *** Every webhook delivery also carries a set of `X-Lerian-*` context headers: | Header | Carries | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `X-Lerian-Event-Id` | The stable event id (the CloudEvents `ce-id`). **The deduplication key** — constant across redeliveries of the same event. | | `X-Lerian-Event-Type` | The event type, as the bare `.` tail (for example, `transaction.created`). | | `X-Lerian-Tenant-Id` | The owning tenant id (attribution only). | | `X-Lerian-Delivery-Id` | The per-attempt id — **changes on every retry** of the same event. | | `X-Lerian-Schema-Version` | The payload schema version. | ## Deduplicating deliveries *** Delivery is **at-least-once**: the hub may deliver the same event more than once, through retries or a redelivery after a worker restart. Deduplicate on **`X-Lerian-Event-Id`** — it is stable across every redelivery of the same event, while `X-Lerian-Delivery-Id` differs per attempt. Treat a `X-Lerian-Event-Id` you have already processed as a duplicate: acknowledge it with a `2xx` and do not reprocess. Keep your handler idempotent. ## Responding quickly *** Return a `2xx` as soon as you have verified and durably accepted the event — then do the real work asynchronously. A slow or failing response is treated as a failed delivery, which triggers the [retry curve](/en/streaming-hub/how-streaming-hub-works) and, if the destination stays broken long enough, eventually [auto-disables](/en/streaming-hub/how-streaming-hub-works) the subscription. Acknowledge fast, process out of band. ## Pulling events *** A `pull` subscription receives no push. Instead, your consumer reads a page of its events with: ``` GET /v1/events?subscription_id= ``` The events come back in ascending arrival order, each with its `seq`, its `ceId` (for dedup), its type, and its payload. The response includes a `next_cursor`. **The read is the acknowledgment (cursor-as-ack).** Fetching a page advances the subscription's durable cursor to the highest `seq` returned. There is no separate ack call — reading a page acknowledges it. This is monotonic, so it never moves backward. To page through normally, resume from the server's `next_cursor` and stop when a short page (fewer than your `limit`) returns `null`. Two behaviors to keep in mind: * **A forward `?after=` seek forfeits the gap.** Passing `?after=N` greater than your current cursor advances the cursor past everything up to `N` — the skipped events are **never redelivered**. Asking for events after `N` declares everything up to `N` consumed. This can only happen with a hand-crafted forward seek, never through normal `next_cursor` pagination. * **A backward `?after=` seek never rewinds.** A stale or lower `?after=` that reads an older page does not move the cursor back, because the cursor only ever advances. The pull read is rate-limited per tenant. A denied read returns **`429 rate_limited`** — back off and retry. Deduplicate pulled events on `ceId`, just as a webhook consumer deduplicates on `X-Lerian-Event-Id`. ## Next steps *** Create subscriptions, rotate secrets, and recover disabled destinations. Deploy, configure, and observe the hub. # How Streaming Hub works Source: https://docs.lerian.studio/en/streaming-hub/how-streaming-hub-works The path an event takes through Streaming Hub — ingest, classification, consume-once deduplication, subscription matching, dispatch, retries, and dead-letters. An event moves through Streaming Hub in three stages: the hub **ingests** it from the internal stream, **matches** it against your subscriptions, and **dispatches** it to each matched destination. This page walks through each stage and the delivery guarantees that fall out of it. ## Ingesting events from the stream *** Streaming Hub consumes the platform stream as **CloudEvents 1.0** messages in **binary content mode**: the CloudEvents context attributes travel in Kafka record headers (each prefixed `ce-`) and the event body is the record value. The hub reads routing and identity from the headers without deserializing the payload. The hub subscribes to the platform topics on the brokers you point it at. Its **follow set** is a topic grammar: `lerian.streaming.` plus two dot-separated segments, with an optional `.vN` schema-major tail. Each segment holds lowercase letters, digits, and underscores. The first segment carries the producer's folded service and resource, so every Midaz ledger topic belongs to the follow set — `lerian.streaming.ledger_organization.created` matches. A producer that publishes outside this grammar stays outside the follow set. Consume that producer's topics from the broker directly. A second consumer reads the `.dlq` topics that producers write. That consumer serves **observability only**. The follow-set grammar excludes a trailing `.dlq`, so a dead-letter record never becomes a delivery to one of your subscriptions. Operators read those records through `GET /admin/dlq` — see [DLQ forensics](/en/streaming-hub/operating-streaming-hub). Every record the hub accepts must carry this context set: | Header | Requirement | | ------------------ | -------------------------------------------------------------------------------------------------------- | | `ce-specversion` | The CloudEvents spec version. | | `ce-id` | A unique event id. **Must be a UUIDv7** — it is the deduplication and correlation key. | | `ce-source` | The producing service. | | `ce-type` | The event type, exactly `studio.lerian..`. | | `ce-time` | The emission timestamp. | | `ce-schemaversion` | The payload schema version. | | `ce-resourcetype` | The resource segment (for example, `account`). | | `ce-eventtype` | The event segment (for example, `created`). | | `ce-tenantid` | The owning tenant, and the only source of tenant identity at ingest. A record missing it is quarantined. | For each record the hub returns one of three **verdicts**: | Verdict | When | Outcome | | -------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | **Accept** | A well-formed event for a known, active tenant. | Persisted to the event inbox and matched against subscriptions. | | **Drop** | A system event, or an event for an unknown or inactive tenant. | Counted and skipped; the stream position still advances. No event is stored. | | **Quarantine** | A malformed event, or one missing its tenant. | Recorded as a poison event for forensics. Not retried — a malformed record never becomes valid on replay. | The tenant admitted at ingest comes only from the validated `ce-tenantid` attribute the parser extracted. An unknown or inactive tenant is dropped at the roster gate — the one point where tenant isolation is enforced on the ingest path. Three related strings look alike but are distinct — never conflate them: * **`ce-type`** — the CloudEvents type: `studio.lerian..` (for example, `studio.lerian.transaction.posted`). * **Kafka topic** — where the event rides on the internal bus. There is no single platform-wide prefix: each producer owns its own topic namespace. Midaz routes to explicit topics with a fixed service segment — ledger events ride `lerian.streaming.ledger_.`, the service folded into the first segment (so `transaction.posted` rides `lerian.streaming.ledger_transaction.posted`) and any hyphen in the resource or event turned into an underscore on the topic (`balance.config-changed` rides `lerian.streaming.ledger_balance.config_changed`). Other producers — Consignado and cross-product commands — derive their namespace from their `ce-source` instead. * **`X-Lerian-Event-Type`** — the type stamped on a delivered webhook: the bare `.` tail of the `ce-type` (`transaction.posted`). It mirrors the `ce-type` identity, not the Kafka topic — it carries no producer service segment (no `ledger_` fold). See the [event streaming overview](/en/reference/events/overview) for the full wire contract. ## Consume-once deduplication *** The internal stream is at-least-once, so the same record can arrive more than once. On the way into the event inbox the hub deduplicates on `ce-id`: a duplicate id writes nothing and is counted as a dedup drop. Records are persisted per Kafka partition in one transaction, and the stream position for a partition is committed only after that partition's write commits — so a failure on one partition never loses or double-commits events on another. ## Subscription matching *** Once an event is stored, the hub resolves which of your subscriptions should receive it. Subscription matching is internal and **catalog-free**: it evaluates each event against your subscriptions on `(tenant, event type, schema major)` directly, without consulting the event catalog. An event type no subscription wants produces zero deliveries — a correct outcome, not an error. A subscription is admitted by matching only when it is both **`enabled`** and in **`verification_state = active`**. These are two independent conditions — see [the subscription model](/en/streaming-hub/managing-subscriptions) — and matching requires both. ## Dispatching to your destinations *** For every match, the hub creates a delivery job and hands it to a worker pool (eight workers by default). Workers claim due jobs atomically with a short lease and interleave across tenants, so no single tenant's backlog starves the others. Each worker loads the stored payload, decrypts the destination's signing secret or credential in memory, and makes exactly one delivery attempt to the sink. Delivery is **at-least-once**: the hub may deliver the same event more than once (through retries or redeliveries after a worker restart). Every delivery carries a stable `X-Lerian-Event-Id` (the `ce-id`) for you to deduplicate on, and a per-attempt `X-Lerian-Delivery-Id` that changes on every retry. Treat a repeated `X-Lerian-Event-Id` as a duplicate and acknowledge it without reprocessing. See [Consuming events](/en/streaming-hub/consuming-events) for the consumer side of this contract. ## Retries and back-off *** When an attempt fails, the hub schedules the next one on a fixed back-off curve with full jitter: ``` 0 · 5s · 5m · 30m · 2h · 5h · 10h · 10h ``` The curve spans roughly 28 hours across its eight steps. Each subscription can override it with its own schedule (up to 12 steps, each capped at 10 hours); a malformed override falls back to the default curve rather than failing delivery. ## Dead-lettering *** An event whose retries are exhausted is **dead-lettered** — the hub stops attempting it and records the terminal outcome. A job that instead keeps crashing a worker mid-attempt — never recording an outcome — is reclaimed a bounded number of times (five by default) and then dead-lettered as poison, so a single toxic job can never occupy a worker forever. ## Circuit breaker *** Each `(tenant, destination)` pair has a circuit breaker. When a destination fails repeatedly, its breaker opens and further jobs for that destination are **shed** — rescheduled without an attempt — so a single broken endpoint does not burn worker capacity or hammer a struggling target. The breaker recovers on its own once the destination starts succeeding again. ## Auto-disabling a broken destination *** A destination that stays broken for a long time is automatically disabled. Auto-disable trips only when the open failure span is **both**: * **sustained** — it has been failing for at least the failure window (120 hours by default); and * **spread** — at least 12 hours separate the first and last failure in the span. Both gates must hold, and a **single successful delivery clears the span**, so a brief blip never accumulates toward the verdict. When auto-disable trips it flips the subscription's `enabled` flag to `false` — it never touches `verification_state`. Delivery stops (matching requires `enabled`), and the hub records why. You recover an auto-disabled subscription with `POST /v1/subscriptions/:id/verify`, which re-probes the destination and, on success, re-enables it in place. Auto-disable is governed by a kill switch (`STREAMING_HUB_AUTODISABLE_ENABLED`), so an operator can ship it dark. See [recovering an auto-disabled subscription](/en/streaming-hub/managing-subscriptions). ## Ordering guarantees *** The internal stream is partitioned by tenant, so events for one tenant normally ride a single partition and the hub preserves their **first-in, first-out** order end to end. The hub attributes and deduplicates on the CloudEvents headers, not the Kafka record key, so a producer that spreads (salts) a hot tenant across several partitions changes only physical placement — the hub still attributes and dedups correctly. The one trade-off is ordering: a salted tenant's events span multiple partitions with no cross-partition order guarantee, so that tenant forfeits strict FIFO. The arrival sequence the hub assigns reflects the order events *arrived* at the hub, not the order they were *produced*. A tenant that is not salted keeps single-partition FIFO throughout. ## Next steps *** The subscription model, onboarding flows, and secret rotation. Verify signatures, deduplicate, and pull events. # Managing subscriptions Source: https://docs.lerian.studio/en/streaming-hub/managing-subscriptions Create webhook and queue subscriptions in Streaming Hub, wire an AWS delegated grant, rotate signing secrets, recover auto-disabled destinations, and re-pin. A **subscription** tells Streaming Hub which events go to which destination for your tenant. You manage subscriptions through the `/v1` control-plane API, authenticated with a plugin-auth JWT. This page covers the model and the onboarding flows; the exact request and response shapes are in the API reference (start from the [reference introduction](/en/reference/introduction)). ## The subscription model *** A subscription records: * **`name`** — a human label (required, non-blank). * **`sink_kind`** — one of `webhook`, `pull`, `sqs`, `rabbitmq`, `eventbridge`. * **`endpoint`** — where deliveries go, in a shape that depends on the sink kind (see below). * **`event_types`** — the event types to deliver. The hub accepts any well-formed key, so a catalog gap never stalls onboarding. A key that no producer emits matches nothing and delivers nothing. Take each key from the per-product pages under [Event streaming](/en/reference/events/overview). * **`schema_major`** — the payload major version the subscription follows. * **`plan_tier`** — the delivery tier the subscription runs under. The endpoint format follows the sink kind: | Sink kind | Endpoint | | ------------- | ----------------------------------------------------------------------------------------------------------------------- | | `webhook` | An `https://` URL, no embedded userinfo. | | `pull` | Omitted — the server synthesizes `pull://`. | | `sqs` | The `https://` SQS queue URL. | | `rabbitmq` | `/` (exchange required, routing key optional). The broker host lives in the encrypted credential. | | `eventbridge` | The event-bus name / detail-type addressing string. The region lives in the encrypted credential. | ### Two orthogonal status fields *** Every subscription carries two independent status fields. Confusing them is the most common source of "why did delivery stop" questions, so keep them separate: * **`verification_state`** is **proof** that the destination can actually receive events. It moves through `pending_verification` → `active` → `degraded`, driven by probes. Only an `active` subscription is deliverable. * **`enabled`** is the **deliverability switch**. It is what auto-disable flips off, and what a re-enable turns back on. Subscription matching requires **both**: a subscription delivers only when `enabled` **and** `verification_state = active`. Auto-disable lives entirely in `enabled` and never changes `verification_state`, so an auto-disabled subscription reads as `enabled = false, verification_state = active`. See [auto-disabling a broken destination](/en/streaming-hub/how-streaming-hub-works) for how the two interact. ## Creating a webhook subscription *** A webhook subscription is the simplest to onboard — it needs no credential, only one probe: 1. **Create** — `POST /v1/subscriptions` with `sink_kind: "webhook"` and your `https://` endpoint. Send a unique `X-Idempotency` header: the hub rejects a create that omits it, before any write. The destination URL is validated against private, loopback, and cloud-metadata address ranges before any row is written, so a private or metadata target can never be stored. The response returns the new subscription in **`pending_verification`**, and the **signing secret exactly once**. 2. **Save the signing secret** — it is shown only in this response, stored only as ciphertext, and never returned by any read. Save it on receipt; if you lose it, you can only rotate to a new one. 3. **Activate the destination** — `POST /v1/subscriptions/:id/ping` sends a synthetic, signed probe through the real delivery path and reports the classified outcome. A successful probe moves the subscription to `active`, and that is what makes it deliverable. Deploy your endpoint before you ping: it must answer `2xx`. The signing secret is returned only in the create response (and again on rotation). It is never logged, never stored in plaintext, and never returned by a `GET`. Capture it when you create the subscription. Once the probe succeeds, the webhook subscription is matched and begins receiving events. For the full walkthrough from create to a first confirmed delivery, see the [quick start](/en/streaming-hub/streaming-hub-quick-start). See [Consuming events](/en/streaming-hub/consuming-events) for how to verify the signature on each delivery. ## Onboarding a queue subscription *** Queue subscriptions (`sqs`, `rabbitmq`, `eventbridge`) are born **`pending_verification`** and **deliver nothing** until their destination is verified. How you verify depends on the kind: * **RabbitMQ** — supply a broker credential, verified on write (below). * **SQS and EventBridge** — [wire an AWS delegated grant](#wiring-an-aws-delegated-grant) (next section) instead of storing a credential. For RabbitMQ, the credential flow is three steps: 1. **Create** — `POST /v1/subscriptions` with the queue `sink_kind` and its endpoint, and **no** inline credential (an inline `sink_config` or `credential` is rejected). Send a unique `X-Idempotency` header, as for any create. The subscription is stored `pending_verification`; matching excludes it, so it produces no delivery jobs yet. 2. **Supply the credential** — `PUT /v1/subscriptions/:id/credential` with the write-only broker credential. The hub holds it in memory, **probes it immediately** (connect and authenticate against the broker), and persists it as ciphertext **only if the probe succeeds** — a failed probe stores nothing. The broker host is validated against blocked address ranges before it is stored. A successful probe flips the subscription `pending_verification → active` in the same transaction. 3. **Active** — once `active`, matching admits the subscription and it begins receiving events. The credential is **write-only**: you supply it here and it is never returned, not even masked, on any read path. To change it, `PUT` a new one — the same probe-on-write applies. ## Wiring an AWS delegated grant *** For AWS sinks (`sqs`, `eventbridge`), the hub delivers by assuming a role **in your AWS account** — it never stores an AWS credential. You wire that trust before supplying the credential: 1. **Fetch the setup artifacts** — `GET /v1/subscriptions/:id/setup-artifacts` returns a cross-account IAM **trust policy**, a CloudFormation **quick-create link**, and a non-secret **`ExternalId`** the hub mints for this subscription. 2. **Apply them in your AWS account** — create the delivery role from the trust policy (the quick-create link scaffolds it). The role trusts the hub's principal *only* under the minted `ExternalId` condition, which closes the confused-deputy gap: a trust policy missing that condition is treated as a verification failure, never as verified. 3. **Register the grant** — `PUT /v1/subscriptions/:id/delegated-grant` with the delivery role ARN, region, and destination. This records non-secret coordinates only; it runs no probe and does not change `verification_state`. 4. **Verify** — `POST /v1/subscriptions/:id/verify` runs the hardened role-assumption probe and, on success, flips the subscription to `active`. No AWS credential ever crosses these requests or is stored by the hub; the hub assumes your role per delivery, guarded by the `ExternalId`. ## Rotating a signing secret *** `POST /v1/subscriptions/:id/secret/rotate` mints a **new** webhook signing secret, returns it **once**, and starts a **24-hour dual-sign overlap**. During the overlap the hub signs each delivery with **both** the new and the previous secret and sends **two** signature headers, so your consumer can switch to the new secret at any point in the window without dropping deliveries. Rotate cleanly like this: 1. Call rotate and save the new secret from the response (alongside the `overlapUntil` timestamp). 2. Deploy the new secret to your consumer within the overlap window. While both are configured, your verifier accepts a delivery if **either** signature validates. 3. After the overlap, retire the old secret. Rotating a `pull` sink — which has no signing secret — is rejected. See [handling two signatures during rotation](/en/streaming-hub/consuming-events) for the consumer-side verification. ## Recovering an auto-disabled subscription *** When a destination stays broken long enough, the hub [auto-disables](/en/streaming-hub/how-streaming-hub-works) the subscription by flipping `enabled = false`. To recover it: 1. Fix the destination (the endpoint, the queue, or the grant). 2. Call `POST /v1/subscriptions/:id/verify`. It re-probes the destination and, on a successful probe, re-enables the subscription and clears the auto-disable mark — in place, keeping the same id and signing secret. A failed re-probe changes nothing, so re-enable is always bound to a real, current probe success. `GET /v1/subscriptions/:id/health` gives you the delivery-health rollup — recent outcomes, dead-letter counts, and the auto-disable verdict — to confirm the destination is healthy before and after. ## Re-pinning the schema major *** `PATCH /v1/subscriptions/:id` re-pins the subscription's `schema_major` — the **only** mutable field: * `{"schema_major": 2}` pins the subscription to that major version. * `{"schema_major": null}` clears the pin to follow the base version. Any other field, an empty body, or a value below `1` is **rejected** (not silently ignored), so you can never believe a forbidden change took effect. Changing the endpoint, sink kind, or secret is out of scope for `PATCH` by design; create a new subscription for a different destination. A re-pin is not a destination change — it does not reset `verification_state` and echoes no secret. ## Idempotency *** The mutating routes — create, delete, `PATCH`, secret rotate, and delegated-grant registration — require an idempotency key: ``` X-Idempotency: ``` A mutation sent without it is rejected **before any write** with `400 missing_idempotency_key`. The store is **fail-closed**: if the idempotency store is unreachable, the request fails rather than risking a double write — this guarantees a replayed create or rotate re-serves the original once-shown secret instead of minting a new one. * Replaying a committed key returns the original response byte-for-byte, with `X-Idempotency-Replayed: true`. * Reusing a key with a *different* request body returns `409 idempotency_conflict` — mint a new key for a corrected request. The verification routes (`ping`, `verify`, `PUT /credential`, `GET /setup-artifacts`) are naturally idempotent and require no key. See the platform-wide guidance on [retries and idempotency](/en/reference/retries-idempotency). ## Next steps *** Verify webhook signatures, deduplicate deliveries, and pull events. Matching, dispatch, retries, and auto-disable in detail. # Quick start Source: https://docs.lerian.studio/en/streaming-hub/streaming-hub-quick-start From nothing to one event received — create a subscription, activate the destination, trigger an event, and confirm the delivery. This page takes you from nothing to one delivered event. It uses a `webhook` sink, because that is the shortest path: four calls to the hub, and one HTTP request arriving at your endpoint. Every `/v1` call sends `Authorization: Bearer ` and `application/json`. The hub reads your tenant from the token. It never reads a tenant from a body, a path, or a query. ## Before you start *** You need four things. * **A running hub and a token.** The `/v1` control plane authenticates every route with a plugin-auth JWT. The catalog read below also needs `catalog` `get`. See [Operating Streaming Hub](/en/streaming-hub/operating-streaming-hub) for the deployment. * **`STREAMING_HUB_MANIFEST_SOURCES` set on the hub**, if you want the catalog read to list your producers. It is empty by default, and an empty list leaves the catalog with the hub's own `hub.*` entries only. See [Operating Streaming Hub](/en/streaming-hub/operating-streaming-hub). * **A public `https://` endpoint you control.** The hub validates the destination before it writes any row, and rejects private, loopback, and cloud-metadata addresses. A `localhost` endpoint cannot be stored. * **A producer on the same stream, with publication turned on.** Publication is off by default on the producer side. On Midaz, set `STREAMING_ENABLED=true`, point `STREAMING_BROKERS` at the same brokers as `STREAMING_HUB_KAFKA_BROKERS`, and set `STREAMING_CLOUDEVENTS_SOURCE`. See [Streaming and outbox](/en/reference/byoc-configuration#streaming-and-outbox). The `ce-tenantid` the producer emits must also equal the hub's `STREAMING_HUB_TENANT_ID`. A mismatch drops every event with no error. [Operating Streaming Hub](/en/streaming-hub/operating-streaming-hub) states that rule. ## The five calls *** ``` 1. GET /v1/catalog → what your producer manifests declare 2. POST /v1/subscriptions → id + signingSecret 3. POST /v1/subscriptions/{id}/ping → active 4. POST /v1/organizations (Midaz) → emits organization.created 5. GET /v1/subscriptions/{id}/health → one successful delivery ``` ## 1. Get the matching key *** `GET /v1/catalog` The catalog lists what the producer manifests in `STREAMING_HUB_MANIFEST_SOURCES` declare. Each entry gives an `eventType` and a `topic`. **The catalog does not carry the matching key.** `eventType` is the event segment alone. The `topic` folds the producer's service name into its resource segment. So `lerian.streaming.ledger_organization.created` has `eventType` `created`, and the key you need is `organization.created`. Take the key from the per-product pages under [Event streaming](/en/reference/events/overview). The producer's own `/streaming/manifest` also reports `resourceType` next to `eventType`. The hub accepts any well-formed key in `event_types`. A key that no producer emits matches nothing, and the subscription receives no events. ## 2. Create the subscription *** `POST /v1/subscriptions` Send the header `X-Idempotency` with a unique value. The hub rejects a create that omits it, before any write. ```json theme={null} { "name": "quick-start", "sink_kind": "webhook", "endpoint": "https://hooks.example.com/lerian", "event_types": ["organization.created"] } ``` `event_types` holds **matching keys**, not full CloudEvents types. A matching key is the `.` tail — `organization.created`, and never the full `studio.lerian.organization.created`. Send the key you assembled in step 1. Omit `event_types` to receive every event the hub sees. The per-product pages under [Event streaming](/en/reference/events/overview) describe each type in full — start from the [Midaz event catalog](/en/reference/events/midaz). `plan_tier` defaults to `standard`. `schema_major` is optional — leave it out to follow the base version. The `201` response carries the subscription `id` and the plaintext `signingSecret`. **Save the secret now.** No read path returns it, and if you lose it your only route is rotation. ## 3. Activate the destination *** `POST /v1/subscriptions/{id}/ping` A new webhook subscription is born `pending_verification`. The hub delivers to it only after a probe proves the destination answers, so **this call is required**. It sends one synthetic signed request through the production delivery path, then reports the result: ```json theme={null} { "outcome": "ok", "statusCode": 200, "errorClass": "" } ``` `outcome: "ok"` moves the subscription to `active`, which is the state that makes it deliverable. Your endpoint must answer with a `2xx` for that to happen, so deploy it before you ping. A probe that ran and failed is still a `200` — read `outcome`, not the HTTP status. `outcome: "failed"` leaves the subscription unverified and names the cause in `errorClass`. Fix the endpoint and ping again. The call is safe to repeat and needs no idempotency key. Delivery requires `enabled` **and** `verification_state = active`. [Managing subscriptions](/en/streaming-hub/managing-subscriptions) explains why the two fields stay separate. ## 4. Trigger an event *** Do something in a Lerian product that emits the type you subscribed to. In Midaz, creating one organization emits the event above. `POST /v1/organizations` ```json theme={null} { "legalName": "Quick Start Ltda", "legalDocument": "00000000000191" } ``` Midaz publishes the event to the stream right after it persists the organization. The delivery reaches your endpoint moments later, not inside this call. ## 5. Confirm the delivery *** Your endpoint receives a `POST` carrying the event payload, an HMAC signature, and the hub's context headers — `X-Lerian-Event-Id`, `X-Lerian-Event-Type`, and `X-Lerian-Delivery-Id` among them. Verify the signature before you trust the body: an unverified request proves nothing. [Consuming events](/en/streaming-hub/consuming-events) has the verification steps, the full header list, and the deduplication rule. Then ask the hub what it recorded. `GET /v1/subscriptions/{id}/health` | Field | What it tells you | | -------------------- | -------------------------------------------------------- | | `verification_state` | `active` once the ping succeeded. | | `status` | The rolled-up verdict: `Healthy`, `Degraded`, or `Down`. | | `delivery_outcomes` | Attempt counts for the window, keyed by outcome. | | `last_success_at` | When the last delivery succeeded. | | `dead_lettered` | Attempts that exhausted their retries. | One success in `delivery_outcomes` and a fresh `last_success_at` mean the path works end to end. If nothing arrived, `status` and `delivery_outcomes` tell you whether the hub tried and failed, or never matched the event at all. ## Prefer to pull? *** A `pull` subscription needs no endpoint and no probe. Create it with `sink_kind: "pull"` and no `endpoint` — the hub synthesizes one, and the subscription is `active` from birth. Read pages of events with `GET /v1/events?subscription_id=`. The read is the acknowledgment, so read the cursor rules in [Consuming events](/en/streaming-hub/consuming-events) before your first call. ## Next steps *** Queue sinks, AWS delegated grants, secret rotation, and recovery. Verify signatures, deduplicate deliveries, and pull with a cursor. Matching, dispatch, the retry curve, and auto-disable. Deploy the hub, configure it, and watch it run. # What is Streaming Hub? Source: https://docs.lerian.studio/en/streaming-hub/what-is-streaming-hub Streaming Hub is Lerian's managed delivery edge — it consumes the platform's internal event stream and fans events out to webhook, queue, and pull consumers. **Streaming Hub** is Lerian's event-delivery service. It sits at the edge between the platform's internal event stream and the systems you run outside it, and delivers each event you care about to a destination you own — a webhook, a cloud queue, or a cursor you pull. Lerian products publish domain events — past-tense business facts such as an account being created or a transaction being posted — onto a shared streaming backbone. Streaming Hub subscribes to that backbone on your behalf, matches the events against the subscriptions you register, and delivers them. You describe *which events go where*; the hub owns the consume, match, and deliver loop. Streaming Hub is closed source and available to licensed Lerian customers; its repository is maintained internally. ## Streaming Hub in the Lerian platform *** The platform's event backbone is a shared, [CloudEvents-based stream](/en/reference/events/overview) carried on Kafka/Redpanda and produced through Lerian's `lib-streaming` library. Delivery on that stream is **at-least-once**, events from every product and tenant ride the same topics, and consuming it directly means running your own Kafka consumer: tracking offsets, filtering event types, deduplicating, retrying failed deliveries, and signing outbound requests. Streaming Hub does that work once, as a managed edge. It **consumes** the internal stream — it never produces onto it — and turns a raw event feed into per-destination delivery you configure through a single API. You register a subscription, the hub tracks the stream, and matched events arrive at your endpoint already signed and correlated. Because the hub is the delivery edge, the guarantees you build against are the hub's delivery contract, not the raw stream: your webhook endpoint verifies a signature, deduplicates on a stable event id, and returns quickly. See [How Streaming Hub works](/en/streaming-hub/how-streaming-hub-works) for the full path an event takes. ## Sink kinds *** A subscription delivers to exactly one destination, called a **sink**. Streaming Hub supports five sink kinds: | Sink kind | Delivery | Destination | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- | | `webhook` | Push | An `https://` endpoint you own. Each request is HMAC-signed and carries CloudEvents correlation headers. | | `pull` | Pull | A server-side cursor. Your consumer reads a page of events with `GET /v1/events`; the read doubles as the acknowledgment. | | `sqs` | Push | An Amazon SQS queue, reached through an AWS delegated grant. | | `rabbitmq` | Push | A RabbitMQ exchange and routing key. | | `eventbridge` | Push | An Amazon EventBridge event bus, reached through an AWS delegated grant. | Push sinks (`webhook`, `sqs`, `rabbitmq`, `eventbridge`) deliver events to you as they match. The `pull` sink inverts that: the hub holds events on a cursor and your consumer fetches them on its own schedule. A webhook subscription becomes deliverable after one signed probe confirms your endpoint answers. Queue subscriptions (`sqs`, `rabbitmq`, `eventbridge`) deliver nothing until you supply and verify their outbound credential — see [Managing subscriptions](/en/streaming-hub/managing-subscriptions). ## Deployment model *** Streaming Hub runs **BYOC** by default: you deploy it into your own infrastructure as a single-tenant service, with a static tenant roster and no dependency on Lerian-hosted tenant management. This is the shape most clients run. A **SaaS multi-tenant** mode sits behind a configuration flag. When enabled, the hub serves many tenants over the shared bus, admits tenants from a cache-backed roster fed by Lerian's tenant manager, and isolates every tenant's subscriptions, events, and cursors by tenant id. The BYOC path carries none of that machinery. See the platform-wide [multi-tenancy model](/en/multi-tenancy) for how tenant isolation works across Lerian products, and [Operating Streaming Hub](/en/streaming-hub/operating-streaming-hub) for the deployment details. ## The control-plane API *** You manage subscriptions through the `/v1` control-plane API. Every route is authenticated with a plugin-auth JWT (`Authorization: Bearer `), and the tenant is always taken from the validated token claims — never from a request body, path, or query. Through it you: * create, list, read, and delete subscriptions; * verify a destination (`ping` / `verify`) and supply queue credentials; * rotate a webhook signing secret; * read a subscription's delivery health; * pull events (`GET /v1/events`) for `pull` sinks; * browse the event [catalog](/en/reference/events/overview) the hub sees. The full operation set, request shapes, and error tokens live in the API reference — start from the [reference introduction](/en/reference/introduction). ## Next steps *** The path an event takes: ingest, match, dispatch, retry, and auto-disable. Create webhook and queue subscriptions, wire AWS grants, and rotate secrets. Verify webhook signatures, deduplicate deliveries, and pull events. The shared CloudEvents contract every Lerian event follows. # Platform services Source: https://docs.lerian.studio/en/platform/platform-introduction Discover the cross-product services — access control, lifecycle, observability — that power every Lerian product on a unified foundation. The **Lerian Platform** is a set of cross-product services shared by all Lerian products. It provides a unified foundation of security, orchestration, and governance that enables consistent operations across the entire Lerian ecosystem. These shared services ensure that every Lerian product benefits from **centralized access control**, **automated resource lifecycle management**, and **consistent operational visibility** — without requiring each product to implement these capabilities independently. ## Available services ***
Centralized authentication and authorization — manages users, roles, permissions, and credentials across all Lerian products. Orchestration layer that automates resource creation, updates, and decommissioning across environments. Integrated monitoring and alerting — providing full visibility into the performance and health of all Lerian systems. Official Helm charts for deploying and managing all Lerian products on Kubernetes via an OCI-compatible registry.
# Boletos & Bill Payment Source: https://docs.lerian.studio/en/rails/boleto/boleto-and-bill-payment Issue boletos and pay bills like DARF taxes and utility invoices through the Payments module in Lerian Console, with full Midaz ledger tracking. **Boletos & Bill Payment** covers Brazil's boleto rail: issuing boletos bancários and paying bills such as DARF tax documents and utility/concessionaire invoices. On the Lerian platform, you operate this capability through the **Payments module in Lerian Console** — a managed plugin for Midaz, provisioned for your organization. ## What you can do *** * **Issue boletos** with due dates, fines, interest, and discounts, recorded against your Midaz ledger. * **Pay bills** — DARF tax payments and utility/concessionaire bills — directly from the Console. * **Track operations** from a dashboard with KPI cards, recent activity, and quick actions. ## Where to start *** Activate the Payments plugin and take a first tour of the module. The dashboard: KPI cards, recent operations, and shortcuts. Issue, consult, and manage boletos. Pay DARF and utility bills from the Console. # Environment variables Source: https://docs.lerian.studio/en/rails/boleto/boleto-environment-variables Configure the Boleto and bill payments rail via BTG — BTG OAuth, credential encryption with key rotation, Midaz ledger, reconciliation, and webhooks. The Boleto and bill payments rail issues boletos and settles bill and tax (DARF) payments through BTG. DevOps sets its behavior through environment variables at deploy time. A change to one variable needs a service restart. This page covers the variables **distinctive to this rail**. For the datastore, multi-tenancy, telemetry, and authentication knobs shared across every Lerian Go service, see [BYOC configuration essentials](/en/reference/byoc-configuration). In the tables below, the **Default / Required** column shows the default value. A bold qualifier (for example **Required**) marks variables you must set. `—` means no default. `🔒` marks a **secret** — inject it at deploy time from your secret store. Never commit it. This page lists variable names and behavior only. It prints no secret values. This rail does **not** mount the systemplane admin API. It uses the shared `POSTGRES_*` datastore shape — see [Datastores](/en/reference/byoc-configuration#datastores). ## Server and port The service listens on the address in `SERVER_ADDRESS` (default `:8080`). The liveness, readiness, and version probes bind this same port. `MULTI_TENANCY_ENABLED` toggles multi-tenancy (note the `MULTI_TENANCY_` spelling). The Tenant Manager connection uses the shared `MULTI_TENANT_*` variables. See [Multi-tenancy](/en/reference/byoc-configuration#multi-tenancy) and [Default network ports](/en/reference/default-network-ports). ## BTG integration These variables set the endpoints and credentials for the BTG connection. They also set the background refresh intervals for the BTG access token and the synced credentials. | Variable | Default / Required | Description | | ------------------------------ | ------------------ | ------------------------------------------------------- | | `BTG_API_BASE_URL` | **Required** | Base URL of the BTG API. | | `BTG_AUTH_URL` | **Required** | BTG OAuth token endpoint. | | `BTG_CLIENT_ID` | **Required** | OAuth client ID for the BTG API. | | `BTG_CLIENT_SECRET` | 🔒 **Required** | OAuth client secret for the BTG API. | | `BTG_WEBHOOK_SECRET` | 🔒 **Required** | Secret used to validate inbound BTG webhook signatures. | | `BTG_HTTP_TIMEOUT` | `30s` | Timeout for calls to the BTG API. | | `BTG_TOKEN_REFRESH_INTERVAL` | `1h` | How often the BTG access token is refreshed. | | `BTG_CREDENTIAL_SYNC_INTERVAL` | `20h` | How often stored BTG credentials are re-synced. | ## Credential encryption and internal API keys The rail encrypts stored credentials at rest. It authenticates internal calls between the worker and API pods with an API key. Both keys support a `_PREVIOUS` slot so you can rotate the active value without downtime. | Variable | Default / Required | Description | | ------------------------------------ | ------------------ | ---------------------------------------------------- | | `CREDENTIAL_ENCRYPTION_KEY` | 🔒 **Required** | Key used to encrypt stored credentials at rest. | | `CREDENTIAL_ENCRYPTION_KEY_PREVIOUS` | 🔒 — | Prior encryption key, kept readable during rotation. | | `INTERNAL_API_KEY` | 🔒 **Required** | API key authenticating internal worker-to-API calls. | | `INTERNAL_API_KEY_PREVIOUS` | 🔒 — | Prior internal API key, accepted during rotation. | | `INTERNAL_WORKER_URL` | **Required** | URL the API uses to reach the internal worker. | ## Midaz ledger binding | Variable | Default / Required | Description | | --------------------------- | ------------------ | ------------------------------------------------------------------- | | `MIDAZ_LEDGER_URL` | **Required** | Midaz ledger service URL. | | `MIDAZ_DEFAULT_ORG_ID` | **Required** | Default Midaz organization UUID for bookings. | | `MIDAZ_DEFAULT_LEDGER_ID` | **Required** | Default Midaz ledger UUID for bookings. | | `MIDAZ_ALLOW_INSECURE_HTTP` | `false` | Allow a cleartext `http://` Midaz URL. Leave `false` in production. | ## Reconciliation | Variable | Default / Required | Description | | -------------------------------------- | ------------------ | --------------------------------------------------- | | `RECONCILIATION_INTERVAL` | `5m` | How often the reconciliation cycle runs. | | `RECONCILIATION_LOOKBACK_HOURS` | `24` | How far back each reconciliation cycle scans. | | `RECONCILIATION_MAX_BOLETOS_PER_CYCLE` | `100` | Maximum boletos reconciled per cycle, per tenant. | | `RECONCILIATION_DELAY_MS` | `500` | Delay in milliseconds between reconciliation steps. | ## Webhook dispatch and idempotency | Variable | Default / Required | Description | | ------------------------------- | ------------------ | ----------------------------------------------------------- | | `WEBHOOK_DISPATCHER_BATCH_SIZE` | `50` | Number of webhook events dispatched per batch. | | `WEBHOOK_DISPATCHER_INTERVAL` | `10s` | Interval between webhook dispatch cycles. | | `WEBHOOK_SKIP_URL_VALIDATION` | `false` | Skip subscriber-URL validation. Keep `false` in production. | | `IDEMPOTENCY_RECORD_TTL_HOURS` | `48` | How long idempotency records are retained, in hours. | | `ACCOUNT_VALIDATION_DISABLED` | `false` | Disable account validation. Keep `false` in production. | ## Migrations | Variable | Default / Required | Description | | --------------------------- | ------------------ | ------------------------------------------------------ | | `MIGRATION_TIMEOUT_SEC` | `300` | Timeout in seconds for a database migration run. | | `MIGRATION_LOCK_TIMEOUT_MS` | `10000` | Timeout in milliseconds to acquire the migration lock. | ## Health and readiness The rail exposes `GET /health` (liveness) and `GET /readyz` (readiness) on the main port. If you enable multi-tenancy, it adds an auth-gated per-tenant probe at `GET /readyz/tenant/{id}`. See [Health and readiness](/en/reference/health-and-readiness) for the response shape and startup/drain behavior. # Bill Payments Source: https://docs.lerian.studio/en/rails/boleto/console/payments-bill-payments Pay DARF tax forms and bills (boletos, utility slips) from the Lerian Console Payments plugin — required fields, payment states, and error handling. The **Bill Payments** page lets you pay bills from the Console. It covers **DARF** tax forms and **bills** such as boletos and utility slips. This page shows how to submit a payment in the [Lerian Console](/en/platform/console/about-lerian-console). It lists the fields each form needs, the states a payment moves through, and how to handle errors. ## Accessing the Bill Payments page *** To open the **Bill Payments** page, click **Payments** → **Bill Payments** from the left-side menu. The page displays all bill payments for the organization in a data table. Use the **Filters** section to narrow the payments shown. The top-right of the page shows two buttons: * **Pay bill** — pay a bill with a digitable line or barcode. * **Pay DARF** — pay a DARF tax form. Enter the tax details manually. ## Paying a bill *** From the **Bill Payments** page, click **Pay bill**. The **New payment** panel opens on the right. Select the **Pay bill** tab. Fill in the fields: * **Digitable line or barcode** *(required)* — the full digitable line or barcode from the bill. * **Source account ID** *(required)* — the Midaz account to debit for this payment. * **Scheduled date** *(optional)* — schedule the payment for a future date. Leave blank to process immediately. * **Description** *(optional)* — an internal note for this payment. Click **Pay bill**. ## Paying a DARF *** From the **Bill Payments** page, click **Pay DARF**. The **New payment** panel opens with the **Pay DARF** tab active. Fill in the fields: **Taxpayer** * **Person type** *(required)* — **Individual (PF)** or **Company (PJ)**. This selection changes the document field below. * **CPF** *(required, when Individual)* — the taxpayer's CPF (`000.000.000-00`). * **CNPJ** *(required, when Company)* — the company's CNPJ (`00.000.000/0000-00`). * **Taxpayer full name** *(required)* — the taxpayer's full name. **DARF details** * **Treasury revenue code** *(required)* — the 4-digit revenue code (for example, `0190`). * **DARF type** *(required)* — the type of DARF document. * **Reference date** *(required)* — the competency or reference date for the tax. * **Due date** *(optional)* — the due date for the payment. **Amounts** * **Principal amount** *(required)* — the main tax value (R\$). * **Interest** *(required)* — the interest amount due (R\$). Enter `0,00` if none. * **Fine** *(required)* — the fine amount due (R\$). Enter `0,00` if none. The **Total amount** summary updates as you fill in the amount fields. **Payment** * **Source account ID** *(required)* — the Midaz account to debit. * **Reference number** *(optional)* — an internal reference for this DARF payment. Click **Pay DARF**. ## Payment states *** A bill payment can show these states: * **Created** — the plugin recorded the payment. It waits to process. * **Scheduled** — the payment runs on a future date you set. * **Pending approval** — the payment waits for an approver. * **Approved** — an approver accepted the payment. The plugin runs it next. * **Executed** — the plugin sent the payment to the provider. * **Settled** — the provider confirmed the payment. The payment is complete. * **Rejected** — the provider rejected the payment. * **Failed** — the payment did not complete. See [Error handling](#error-handling). * **Canceled** — someone canceled the payment before execution. ## Error handling *** If a payment does not complete, it shows a **Rejected** or **Failed** status with an error reason in the details panel. Common causes include: * **Invalid digitable line** — the line has a wrong format, or the bill expired. Check the bill and retry. * **Invalid DARF details** — a wrong revenue code, CPF/CNPJ, or amount. Correct the fields and retry. * **Insufficient funds** — the source account does not have enough balance. Top up the account and retry. * **Provider error** — the payment provider rejected the request. Retry or contact support. Bill payments require the appropriate Payments permissions. If you cannot see the **Pay bill** or **Pay DARF** buttons, ask your administrator to check your role permissions. ## Related pages *** # Boletos Source: https://docs.lerian.studio/en/rails/boleto/console/payments-boletos Issue single, installment, or batch boletos (traditional or hybrid with Pix QR) from Lerian Console, track lifecycle states, and resolve payment errors. The **Boletos** page lets you issue boletos and follow their lifecycle. This section shows how to issue a boleto in the [Lerian Console](/en/platform/console/about-lerian-console). It lists the required fields, the boleto states, and how to handle errors. ## Accessing the Boletos page *** To open the **Boletos** page, click **Payments** → **Boletos** from the left-side menu. The page displays all boletos for the organization in a data table, with the following columns: * **Boleto** — the boleto identifier, plus the barcode and digitable line after issuance. * **Payer** — the party that you charge. * **Amount** — the boleto amount. * **Due Date** — the date the boleto is due. * **Status** — the current lifecycle state (see [States](#boleto-states)). * **Created At** — the date when you issued the boleto. * **Actions** — action menu. You can filter boletos by status, payer, or date range. Use the filter fields above the table. ## Issuing a boleto *** To issue a boleto, click the **+ Issue new boleto** button on the top-right of the **Boletos** page. A side panel opens with three tabs — choose the issuance type that fits your use case: ### Boleto types | Type | Description | | ---------------- | -------------------------------------------------------------------------------------------------------------- | | **Single** | Issue one boleto for a single payer with a fixed amount and due date. | | **Installments** | Issue a series of boletos for the same payer, split over multiple due dates at a configured interval. | | **Batch** | Issue up to 10 boletos for different payers in one operation. Each entry is sent individually to the provider. | ### Registration type All three issuance types include a **Type** field: * **Traditional** — a standard boleto that the banking provider registers. * **Hybrid** — a boleto with an embedded Pix QR code. The payer can pay by barcode or by QR code. ### Single boleto From the **Boletos** page, click **+ Issue new boleto**. Select the **Single** tab. Fill in the required fields: **Boleto details** * **Amount** *(required)* — the boleto value (R\$). * **Due date** *(required)* — the date the payer must pay the boleto. * **Account ID** *(required)* — the Midaz account to associate with the boleto. * **Type** *(required)* — **Traditional** or **Hybrid**. **Payer details** * **CPF / CNPJ** *(required)* — the payer's tax document. * **Full name** *(required)* — the full name of the payer. * **Street** *(required)* — payer's street address. * **Number** *(required)* — address number. * **Complement** *(optional)* — apartment, suite, or additional address info. * **Neighborhood** *(required)* — payer's neighborhood. * **City** *(required)* — payer's city. **Additional** * **Instructions** *(optional)* — the text to print on the boleto (for example, interest or fines after the due date). Click **Issue boleto**. After the provider processes it, the boleto appears in the list. It shows the generated **barcode** and **digitable line**, ready to share with the payer. ### Installment series Same fields as Single, with the following differences: * **Total amount** *(required)* — the full amount to split across all installments. * **First due date** *(required)* — the due date for the first installment. Lerian generates the later boletos automatically. * **Installments** *(required)* — number of boletos to generate (minimum 2). * **Interval (days)** *(required)* — number of days between each due date (default 30). Click **Issue installment series** to confirm. ### Batch The **Batch** tab lets you issue up to 10 boletos at once. Lerian sends each entry to the provider individually. For each entry, fill in: **Amount**, **Due date**, **Account ID**, **CPF / CNPJ**, and **Full name**. Use **+ Add entry** to add more entries. The counter shows how many entries you queued (for example, `1 / 10 entries`). Click **Issue batch** to submit all entries. After you issue a boleto, copy the **digitable line** or download the boleto document. Share it with the payer. ## Boleto states *** A boleto moves through the following states: * **Registering** — the provider registers the boleto after you submit the request. * **Registered** — the provider registered the boleto and the payer can pay it. * **Paid** — the payer paid the boleto. * **Expired** — the boleto passed its due date without payment. * **Cancelled** — a user cancelled the boleto before payment. * **Failed** — the boleto issuance did not complete (see [Error handling](#error-handling)). ## Available actions menu *** The action menu () shows different actions for each boleto. The available actions depend on the boleto state: * **View details** — opens the boleto details: barcode, digitable line, and status history. * **Download** — downloads the boleto document. * **Cancel** — cancels a boleto before the payer pays it. ## Error handling *** If a boleto issuance fails, it appears with a **Failed** status and an error reason in the details panel. Common causes include: * **Invalid payer document** — the CPF/CNPJ is malformed or fails validation. Correct the document and issue a new boleto. * **Invalid amount or due date** — the amount is zero or negative, or the due date is in the past. Adjust the values and try again. * **Provider/registration error** — the banking provider could not complete the registration. Issue the boleto again. If the error persists, contact support. To issue boletos, you need the appropriate Payments permissions. If you cannot see the **+ Issue new boleto** button, ask your administrator to check your role permissions. ## Related pages *** # Payments in Lerian Console Source: https://docs.lerian.studio/en/rails/boleto/console/payments-introduction Issue boletos, pay bills (DARF and utilities), and manage payment operations through the Payments plugin for Midaz in Lerian Console. **Payments** is a managed plugin for **Midaz** in Lerian Console. It lets you issue boletos and pay bills. Bill types include DARF tax payments and utility and concessionaire bills. Lerian provisions it for your organization, like the Fees Engine. You open it from the product launcher. This section explains how to use the Console to issue boletos, pay bills, and manage payment operations. Payments is available only if your organization acquired the Payments product. To get access, [contact our team](https://lerian.studio/contact). ## Activating the Payments plugin *** Lerian delivers Payments as a managed plugin. After Lerian provisions it for your organization, you enable it from the [Plugins Settings](/en/platform/console/plugin-settings) page. To enable Payments: Make sure the Payments plugin is available for your organization. If it is not, [contact our team](https://lerian.studio/contact). Open the **product launcher** in Lerian Console and select **Midaz**. In the **PLUGINS** section, select **Settings** > **Plugins**. Enable **Payments**. ## Accessing Payments *** To access Payments, select it from the **PLUGINS** section in the Midaz module. ## Permissions and RBAC *** Role-based access control (RBAC) controls access to the Payments plugin. You manage permissions the same way as other Midaz plugins, through [Access Manager](/en/platform/console/am-creating-application). Typical permissions include: * **View** — list and inspect boletos and bill payments. * **Create** — issue boletos and pay bills. * **Manage** — cancel boletos and payments when their state allows it. Assign these permissions to the relevant roles. Then only authorized users can issue payments. A user without Payments permissions does not see the plugin in the product launcher. ## Available pages ***
Issue boletos, track their lifecycle states, and handle issuance errors. Pay DARF tax forms and utility/concessionaire bills, and track payment states.
# Payments Overview Source: https://docs.lerian.studio/en/rails/boleto/console/payments-overview Understand the Payments dashboard in Lerian Console — KPI cards, recent activity, and quick actions for boletos and bill payments. The **Overview** is the home page of the Payments plugin for Midaz in [Lerian Console](/en/platform/console/about-lerian-console). It shows a summary of your organization's **boletos** and **bill payments**. The page has summary metrics, recent operations, and shortcuts to the most common actions. ## Accessing the Overview *** The Overview opens automatically when you select **Payments** from the **PLUGINS** section in the Midaz module. To return to it, select **Payments** in the left-side menu. The Overview requires the **View** permission. If your role does not include it, Lerian Console hides the page. See [Permissions and RBAC](/en/rails/boleto/console/payments-introduction#permissions-and-rbac). The dashboard shows all amounts in Brazilian Real (BRL). If the dashboard fails to load, an error banner appears with a **Retry** button. Select **Retry** to reload the data on the same page. ## Operations *** At the top of the page, the **Operations** section provides quick-action cards to start the most common tasks. Each card appears only if your role has the matching permission.
Create a new boleto for collection. Opens the **New Boleto** form on the [Boletos](/en/rails/boleto/console/payments-boletos) page. Requires boleto create permission. Pay a bill via its digitable line or barcode, or a DARF tax form. Opens the [Bill Payments](/en/rails/boleto/console/payments-bill-payments) page (**Pay bill** / **Pay DARF**). Requires payment create permission.
## Boletos summary *** The **Boletos** section summarizes the lifecycle of every boleto issued by your organization through a set of KPI cards. | Card | What it shows | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Issued** | Total number of boletos issued. | | **Pending** | Boletos still awaiting settlement. | | **Settled** | Boletos that have been paid. | | **Overdue** | Boletos past their due date and not settled. | | **Total amount** | Sum of all issued boleto amounts. | | **Settled amount** | Sum of the amounts already settled. | | **Overdue amount** | Sum of the amounts that are overdue. | | **Settlement rate** | Share of issued boletos that have been settled, as a percentage. The card color reflects performance: green at 80% or above, blue from 50% to 79%, and amber below 50%. | ### Recent boletos Below the KPI cards, the **Recent boletos** table lists the five most recently created boletos with the following columns: * **ID** — the boleto identifier (shortened). * **Type** — the boleto type. * **Amount** — the boleto value. * **Status** — the current lifecycle state. * **Created at** — the boleto issue date. Select any row to open the full boleto details, or select **View all →** to go to the [Boletos](/en/rails/boleto/console/payments-boletos) page. If there are no boletos yet, the table shows an empty-state message. ## Payments summary *** The **Payments** section summarizes your bill payment activity with its own set of KPI cards. | Card | What it shows | | ------------------ | ------------------------------------- | | **Created** | Total number of payments created. | | **Scheduled** | Payments scheduled for a future date. | | **Settled** | Payments that have been completed. | | **Failed** | Payments that failed to process. | | **Total amount** | Sum of all payment amounts. | | **Settled amount** | Sum of the amounts already settled. | | **Rejected** | Payments that were rejected. | | **Canceled** | Payments that were canceled. | ### Recent payments The **Recent payments** table lists the five most recent payments, with the same columns as the boletos table: **ID**, **Type**, **Amount**, **Status**, and **Created at**. Select any row to open the full payment details, or select **View all →** to go to the [Bill Payments](/en/rails/boleto/console/payments-bill-payments) page. If there are no payments yet, the table shows an empty-state message. The KPI cards reflect the organization currently selected in Lerian Console. Switch organizations from the breadcrumb at the top of the page to see metrics for a different organization. # Environment variables Source: https://docs.lerian.studio/en/rails/native/consignado/consignado-environment-variables Deploy-time environment variables distinctive to Lerian Consignado — Dataprev — the Dataprev integration adapters and the per-tenant credential custody store. Lerian Consignado — Dataprev is the Lerian-owned gateway that integrates payroll-lending flows with Dataprev. You set these variables at deploy time, and the service must restart before they take effect. [BYOC configuration essentials](/en/reference/byoc-configuration) documents the universal backbone that every Lerian Go service shares: server, datastores, multi-tenancy, telemetry, plugin authentication, and licensing. This page covers only the variables distinctive to Lerian Consignado. In the tables below, the **Default / Required** column shows the default value. A bold qualifier marks a variable that you must set, such as **Required** or **Required in managed deployments**. `—` means no default. A variable flagged **Sensitive** carries credential or key material. Inject it from your secret manager at deploy time, and never commit a value. ## Service and runtime | Variable | Default / Required | Description | | --------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SERVER_ADDRESS` | `:8080` | Main HTTP listen address. The liveness, readiness, metrics, and version probes bind this same port. | | `ENV_NAME` | `development` | Runtime environment name read by the gateway's own security gate. Set it to `production` to arm production hardening (disables Swagger, forces rate limiting, runs production config validation). If you do not set it, it defaults to `development` and runs as non-production. | | `ENVIRONMENT_NAME` | — | Environment for the multi-tenant tenant-event listener (`staging` or `production`; falls back to `ENV_NAME`). Required in multi-tenant mode. It does **not** arm the gateway's own security gate — set `ENV_NAME` for that. | | `SYSTEMPLANE_ENABLED` | `false` | Enable the [Systemplane](/en/reference/systemplane/overview) runtime-configuration admin API under the `/system` prefix on the main port. Off by default (environment-variable-only mode). | | `DEFAULT_TENANT_ID` | `default` | Tenant identifier used in single-tenant mode. | Lerian Consignado exposes `/health` (liveness) and `/readyz` (readiness) on the main port, plus `/version` and `/metrics`. With multi-tenancy enabled, it also exposes `GET /readyz/tenant/{id}`. See [Health and readiness](/en/reference/health-and-readiness) for the probe contract. ## Dataprev integration The master `DATAPREV_ENABLED` switch turns on the Dataprev integration. The averbação, reconciliation, and leilão-discovery flows each add their own enable switch. They also require `STREAMING_ENABLED` and run against the core Dataprev client. The employment-report, exclusion, and redirection flows are development-only. They ship only fake adapters today, so a managed deployment refuses to boot with any of them active. `DATAPREV_REDIRECTION_ADAPTER` alone selects the redirection adapter, because redirection has no separate enable switch. Leave that adapter `disabled` in managed deployments. The custody store below provisions per-tenant credentials for the core Dataprev client (margin, averbação, reconciliation). | Variable | Default / Required | Description | | ------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DATAPREV_ENABLED` | `false` | Master switch for the Dataprev integration. The averbação, reconciliation, and leilão-discovery flows require this together with `STREAMING_ENABLED`. | | `DATAPREV_AVERBACAO_ENABLED` | `false` | Independent opt-in for the averbação (margin-consumption) money path. Averbação stays unsubscribed unless this, `DATAPREV_ENABLED`, and streaming are all on. | | `DATAPREV_CODIGO_SOLICITANTE` | — | Static single-tenant Dataprev integrator code: the institution's three-digit BACEN COMPE code. Public, not a secret. It is the fallback for the single-tenant path only — where per-tenant custody is required, the code resolves per tenant from the custody store, and a missing one fails closed rather than borrowing another institution's code. | | `DATAPREV_CONSENT_CACHE_HMAC_KEY` | **Required when Redis is configured and the rail is enabled** | Key for the HMAC digest that keys the cross-pod worker-consent cache in Redis, so the cache key never carries a CPF. Boot fails closed without it rather than silently degrading to a per-pod cache. Sensitive. | | `DATAPREV_RECONCILIATION_ENABLED` | `false` | Enable the per-competência reconciliation poller. Requires `DATAPREV_ENABLED` and streaming. | | `DATAPREV_RECONCILIATION_INTERVAL_SEC` | `3600` | Reconciliation poll cadence, in seconds. | | `DATAPREV_RECONCILIATION_LOOKBACK_MONTHS` | `1` | Competências before the current one to include on each tick. | | `DATAPREV_RECONCILIATION_CNPJS` | — | Comma-separated consignante employer CNPJs to reconcile. | | `DATAPREV_RECONCILIATION_TENANT_ID` | — | Tenant that owns the reconciliation run. Required in single-tenant mode. | | `LEILAO_ENABLED` | `false` | Enable loan-auction (leilão) discovery. Requires `DATAPREV_ENABLED` and streaming. | | `LEILAO_INTERVAL_SEC` | `3600` | Auction-discovery poll cadence, in seconds. | | `LEILAO_TENANT_ID` | — | Tenant used by the auction-discovery path in single-tenant mode. Discovery relays offers only: no external bid manager is registered yet, so the automatic path submits no gateway-originated proposal. | | `LEILAO_BID_MANAGER_RPS` | `25` | Outbound requests-per-second budget for the bid-manager fan-out, independent of the rail's shared limiter. Active and validated at boot: a value above the code-locked 25 req/s per-institution ceiling refuses to start. The fan-out stays idle until external bid managers are registered. | | `LEILAO_BID_MANAGER_BURST` | `25` | Burst allowance for the bid-manager fan-out, validated at boot against the same 25 req/s ceiling. | | `LEILAO_DECIDE_DEADLINE_SEC` | `5` | Per-bid-manager decision deadline; a manager that exceeds it is skipped for that auction. It takes effect when external bid managers are registered. | | `DATAPREV_EMPLOYMENT_REPORT_ENABLED` | `false` | Enable the employment-status report flow. | | `DATAPREV_EMPLOYMENT_REPORT_ADAPTER` | `disabled` | Employment-report adapter. `disabled` is the no-op default. | | `DATAPREV_EMPLOYMENT_REPORT_INTERVAL_SEC` | `3600` | Employment-report poll cadence, in seconds. | | `DATAPREV_EMPLOYMENT_REPORT_PAGE_SIZE` | `100` | Records read per employment-report page. | | `DATAPREV_EMPLOYMENT_REPORT_CONCURRENCY` | `4` | Concurrent employment-report workers. | | `DATAPREV_EXCLUSION_ENABLED` | `false` | Enable the exclusion flow. | | `DATAPREV_EXCLUSION_ADAPTER` | `disabled` | Exclusion adapter. `disabled` is the no-op default. | | `DATAPREV_EXCLUSION_PROVIDER_TIMEOUT_SEC` | `30` | Per-request timeout, in seconds, for exclusion provider calls. | | `DATAPREV_EXCLUSION_CLAIM_DWELL_SEC` | `60` | Dwell before a claimed exclusion item is retried. | | `DATAPREV_EXCLUSION_STATUS_CHECK_LEASE_SEC` | `60` | Lease for the exclusion status-check worker. | | `DATAPREV_EXCLUSION_RECOVERY_INTERVAL_SEC` | `30` | Exclusion recovery-sweep cadence, in seconds. | | `DATAPREV_EXCLUSION_RECOVERY_BATCH` | `50` | Exclusion items processed per recovery pass. | | `DATAPREV_EXCLUSION_RECOVERY_CONCURRENCY` | `4` | Concurrent exclusion recovery workers. | | `DATAPREV_REDIRECTION_ADAPTER` | `disabled` | Redirection adapter. `disabled` is the no-op default. | In a managed deployment (production, multi-tenant, SaaS, or BYOC), the boot guard fails closed. The service refuses to start when any of these is true: * A Dataprev flow selects a fake or otherwise non-`disabled` adapter. * A Dataprev endpoint URL (base or OAuth token) uses plain HTTP instead of HTTPS. * The Dataprev rail runs with the credential custody store off. ## Credential custody A managed secret store custodies per-tenant Dataprev certificate and OAuth credentials. A managed deployment that runs the Dataprev rail must enable the store. The boot guard refuses to start while the store is off. | Variable | Default / Required | Description | | --------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CREDENTIALS_STORE_ENABLED` | `false` · **Required in managed deployments** | Enable the per-tenant credential custody store. With the Dataprev rail enabled, a production, multi-tenant, SaaS, or BYOC deployment refuses to boot while it is `false`. | | `CREDENTIALS_CLEANUP_INTERVAL_SEC` | `30` | Credential-cleanup sweep cadence, in seconds. | | `CREDENTIALS_CLEANUP_BATCH_SIZE` | `50` | Credentials processed per cleanup pass. | | `CREDENTIALS_CLEANUP_CONCURRENCY` | `4` | Concurrent credential-cleanup workers. | | `CREDENTIALS_CLEANUP_LEASE_SEC` | `60` | Lease for the credential-cleanup worker. | | `CREDENTIALS_CLEANUP_PERSISTENCE_TIMEOUT_SEC` | `5` | Per-operation persistence timeout, in seconds. | | `AWS_REGION` | `us-east-1` | Region for the secrets-manager custody backend. Credentials resolve through the default AWS SDK chain. | | `AWS_ENDPOINT_URL` | — | AWS-compatible endpoint override for local development. Leave unset in real AWS environments; production must not set it. | # Integrating with Lerian Consignado — Dataprev Source: https://docs.lerian.studio/en/rails/native/consignado/consignado-integration How Lerian Consignado integrates with your credit engine: the commands it consumes, business facts it emits, sink-agnostic outbox, and idempotency. Lerian Consignado — Dataprev is **event-first** at its platform edge. It consumes commands from the credit engine and emits business facts. It makes no direct ledger calls and hosts no webhook consumers. The event stream is its authoritative contract, not its REST surface. ## Commands it consumes *** The credit engine sends the gateway **four commands** as events over the platform's streaming backbone: * a **margin request** asks for a worker's available payroll margin * an **averbação request** asks to register a signed contract with Dataprev * an **exclusão request** asks to cancel a registered averbação * a **redirecionamento request** asks to redirect a contract to another creditor The exclusão and redirecionamento flows stay development-gated. Both adapters ship as fakes and stay disabled by default until homologation. A command that decodes but **lacks its identifying key** — for example the worker's CPF — is terminal. The gateway dead-letters it and does not relay it to Dataprev. ## Facts it emits *** The gateway emits **thirteen business facts**: * a discovered **loan-request offer** (solicitação) — relayed so the lender can decide whether to bid * the fetched **margin** for a worker * a **worker-absence alert** (afastamento) — one per alert element on a margin read * a **proposal-accepted** witness — the post-averbação read of what the rail registered * an **averbação-confirmed** result * an **averbação-rejected** result * a **contract-registered** booking handoff — the only fact that books a contract downstream * an **exclusão-confirmed** result * an **exclusão-rejected** result * a **reconciliation** record — one per escrituração or settlement record * an **employment-status report** for a worker * a **redirecionamento-confirmed** result * a **redirecionamento-rejected** result Facts share one package-wide schema version, with one exception: the proposal-accepted witness carries its own major version and publishes on a version-suffixed topic. A fact's catalog key is not its topic name. The topic is derived from the gateway's own application segment plus the fact's resource and event. See the [consignado event reference](/en/reference/events/consignado) for the full mapping. ## Sink-agnostic delivery *** The gateway writes every fact to a **transactional outbox** and relays from there — never straight to the broker, on any path. It publishes each fact to its own topic, but it owns **no consumer routing**: a downstream streaming hub decides which system receives each fact. The same fact can fan out to a ledger, a reconciliation store, or a lending system. The gateway does not know or care who consumes it. ## No direct ledger or webhook coupling *** The gateway makes **no direct Midaz calls** and hosts **no webhook consumers**. Off-platform borrower contact is out of scope. Downstream lending systems own origination and any borrower communication. ## On the wire *** * **Money and rates cross as decimal strings**, never floating-point, which keeps full precision in transit. * **Timestamps are UTC, RFC 3339.** * **Worker identity travels inside reconciliation records, never in the log-visible reference.** The **contract number is the match key**, and each record may also carry the worker's CPF and matrícula: a record whose contract number matches nothing in the consumer's book is exactly the exception the feed exists to raise, so the record still names the worker it moved money for. The event subject stays a rail reference and never carries the worker. * The CEF **repasse** settles **per daily movement, not per contract**, so bank-side reconciliation keys on the transfer handle rather than the contract number. ## Delivery semantics *** * **Idempotent by immutable subject.** Each record and settlement carries an immutable identifier. Reprocessing it is a no-op downstream. * **At-least-once.** The gateway delivers every event at least once. Redelivery and restarts dedupe to a no-op. * **Independent legs.** In reconciliation, the gateway processes the escrituração and repasse legs independently. A failure in one never blocks the other. # Operating Lerian Consignado — Dataprev Source: https://docs.lerian.studio/en/rails/native/consignado/consignado-operations Operating Lerian Consignado: the 24-hour Dataprev auction-discovery window, escrituração and CEF repasse reconciliation, credential rotation, and rate limits. Lerian Consignado — Dataprev handles the operational realities of payroll lending. Auction windows close on a clock. Settlement arrives late and without a terminal signal. Credentials rotate per institution. The rail tolerates only so many requests per second. The gateway manages each concern under full per-tenant isolation. ## Auction window *** Each worker loan request opens a **24-hour auction**. The discovery poller lists new requests. The automatic path does not submit or track proposals: it has no authorized producer for the mandatory proposal terms, so no bid can be selected or reach the rail. ## Reconciliation grain *** Reconciliation runs at **two grains**: * **Escrituração — per contract.** The gateway captures each contract's installment record once per (competência, employer) snapshot. * **Repasse — per record.** Each settlement record has an append-only, immutable identifier. The gateway **re-reads it on every tick** across a sliding window of competências. The rail exposes **no deterministic final competência** and **no per-competência terminal status**. The gateway therefore treats the **presence of a settlement record as the settled predicate**. It re-polls the window to catch late D+2 settlements: terminations, arrears, and corrections that land after the first pass. The gateway measures D+2 from the employer's payroll payment. ## Credentials and rotation *** Credentials rotate **per tenant** through the control plane. When you register a certificate or OAuth secret, the gateway upserts or rotates the stored value. The same surface holds the institution's Dataprev *código solicitante* — the three-digit requester code, `001` through `999`. The gateway caches the per-tenant Dataprev client and refreshes it on rotation, so a rotation takes effect without a redeploy. No read returns a secret. ## Idempotency and redelivery *** The gateway keys every record and settlement by its **immutable subject**. At-least-once redelivery and restarts therefore dedupe to a no-op downstream. A reprocessed record never double-counts a settlement. ## Rate limiting *** Each tenant's Dataprev client carries one outbound limiter, capped at **25 requests per second**. The auction-discovery and reconciliation pollers share this limiter, so neither one starves the other or exceeds the rail's limit. # How Lerian Consignado — Dataprev works Source: https://docs.lerian.studio/en/rails/native/consignado/how-consignado-works How Lerian Consignado works end to end: ICP-Brasil setup, Dataprev margin fetch, averbação, 24-hour loan-auction discovery, and CEF D+2 repasse reconciliation. Lerian Consignado — Dataprev runs on event-driven flows of two kinds. A **command-driven** flow starts when the credit engine asks for a margin or an averbação. A **poller-driven** flow sweeps Dataprev on a cadence for auction discovery and settlement. Before any flow runs, a tenant registers its credentials. ## Credential setup *** A tenant transacts only after it registers two credentials through the gateway's control plane: * an **ICP-Brasil A1 client certificate** — a PEM certificate and key. * an **OAuth2 client-credentials secret**. The gateway validates the certificate before it stores the credential. The gateway holds both credentials in a managed secret store. No read ever returns the secret material. A status read reports three facts about the certificate: whether it is present, its fingerprint, and its expiry. The status read also reports whether the OAuth secret is present. When the store holds nothing, the read returns every flag as false, not an error. ## Margin fetch *** When the credit engine needs a worker's payroll margin, it issues a **margin-request** command. The gateway queries Dataprev for the worker's available margin and disposable remuneration. It then emits a fact that carries the margin, keyed to the originating request. ## Averbação *** To register a signed contract, the credit engine issues an **averbação-request** command. The gateway registers the contract with Dataprev and emits one of two facts: * an **averbação-confirmed** fact that carries the rail's own success code. The gateway relays this code and never hardcodes it. * an **averbação-rejected** fact that carries the rail's error code and message. ## Loan auction (leilão) *** Each worker loan request (*solicitação*) opens a **24-hour auction**. A discovery poller lists these requests page by page. The discovery flow does not submit proposals. It has no authorized producer for the mandatory proposal terms, so no bid can be selected or reach the rail. ## Reconciliation *** On a configurable cadence, a reconciliation poller sweeps a sliding window of payroll *competências*. For each competência, it reads two always-on sources. The first source holds the per-contract **escrituração** records. The second source holds the **CEF D+2 repasse** records that settle paid installments. The poller normalizes both sources and emits one reconciliation fact per record. The two legs are independent. A failure on one leg never blocks the other. # What is Lerian Consignado — Dataprev? Source: https://docs.lerian.studio/en/rails/native/consignado/what-is-lerian-consignado Lerian Consignado is the gateway linking your credit engine to Dataprev's Crédito do Trabalhador (CLT payroll loans) — margin, averbação, auction discovery, repasse. **Lerian Consignado — Dataprev** is the Lerian rail that connects the platform's credit engine to **Dataprev's Crédito do Trabalhador**. The Crédito do Trabalhador is the CLT private-sector payroll-loan program under Portaria MTE 435/2025. It is not the legacy INSS consignado. It covers formally employed private-sector workers. The rail is a thin, sink-agnostic gateway. It fetches a worker's available payroll margin from Dataprev. It registers (*averba*) signed contracts. It discovers 24-hour auction requests. It reconciles the Caixa Econômica Federal (CEF) D+2 *repasses* that settle paid installments. The rail serves financial institutions that originate these payroll loans. It is multi-tenant with full per-institution isolation. ## What it does — and does not — do *** * It is the **integration boundary** between the platform's credit engine and Dataprev's Crédito do Trabalhador. It fetches margin, registers signed contracts, discovers loan-auction requests, and reconciles settlement. It reports each outcome back as a business fact. * It **holds no credit logic**. The credit engine decides whether to lend, at what rate, and to whom. This rail runs the Dataprev-facing steps and relays the rail's own result codes. It does not decide them. * It **owns no money movement**. It makes no direct ledger postings. The facts it emits let downstream systems originate loans and reconcile settlement in their own records. * It is **sink-agnostic**. It emits its results to a transactional outbox and owns no per-destination routing. A downstream streaming hub decides which system receives each fact. * It is **multi-tenant** from the ground up. Each financial institution is a tenant with its own credentials and isolated data. ## Who it serves *** Lerian Consignado — Dataprev serves banks and financial institutions that originate payroll loans under the Crédito do Trabalhador (CLT) program. Each institution is a tenant with its own ICP-Brasil certificate, OAuth2 credentials, and Dataprev *código solicitante*. Its data is isolated from every other institution on the deployment. ## Glossary *** | Term | Meaning | | ----------------------------- | -------------------------------------------------------------------------------------------------------------- | | **Crédito do Trabalhador** | The CLT private-sector payroll-loan program (Portaria MTE 435/2025), distinct from the legacy INSS consignado. | | **Averbação** | Registration of a signed payroll-loan contract to authorize the payroll deduction. | | **Margem consignável** | The worker's available payroll margin that a loan may consume. | | **Escrituração** | The per-contract record of the averbado installment. | | **Repasse** | The transfer that settles a paid payroll installment to the lending institution. | | **Leilão** | The 24-hour auction in which lenders bid to fund a worker's loan request. | | **CET (Custo Efetivo Total)** | The borrower's all-in effective cost. | | **Competência** | The `yyyyMM` payroll reference month. | For how Lerian Consignado — Dataprev sits alongside the other native rails and the partner interfaces, see [Native messaging and partner interfaces](/en/rails/native/native-messaging). # Native messaging and partner interfaces Source: https://docs.lerian.studio/en/rails/native/native-messaging The two ways the Lerian platform reaches Brazil's payment system — Lerian-owned messaging straight to BACEN, and adapters for connectivity partners such as JD and BTG. The Lerian platform reaches Brazil's payment system in two ways, and both run side by side. **Native messaging** is Lerian's own connectivity layer. It gives you direct, non-intermediated links to the Central Bank of Brazil (BACEN) networks and the market infrastructures under its supervision. Lerian owns and operates these connections, rather than adapting them from a third-party provider. A **partner interface** reaches the same rail through a connectivity provider's systems. Each rail section in this documentation lists its native messaging first, then the partner interfaces available for that rail. ## What "native" means *** A provider interface reaches a rail through another institution's infrastructure — your traffic rides on a connectivity provider's systems. A native connection removes that intermediary. Your institution's messages travel on Lerian-owned software straight to BACEN's networks (over the RSFN) and the market infrastructures that clear and settle Brazilian payments. One less dependency in the money path, and full alignment with the platform principle that clients own their stack. ## The native family *** Native messaging spans seven rails. Each one speaks the messaging contract of a specific Brazilian settlement or clearing system: | Rail | System | What it moves | | -------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Lerian SPB** | Sistema de Pagamentos Brasileiro | TED and STR messaging — large-value interbank transfers settled in real time | | **Lerian SPI** | Sistema de Pagamentos Instantâneos | Pix — instant payments settled directly in BACEN's instant-payment system | | **Lerian SILOC** | Sistema de Liquidação Diferida (Nuclea) | Deferred net settlement of interbank credit orders — boleto and card flows | | **Lerian SLC** | Serviço de Liquidação Centralizada (Nuclea) | Centralized settlement of the Brazilian card arrangement between acquirers, sub-acquirers, and settlement institutions | | **Lerian STA** | Sistema de Transferência de Arquivos | File exchange with BACEN over the RSFN — regulatory and operational file transfer | | **Lerian SISBAJUD** | Sistema de Busca de Ativos do Poder Judiciário | Court-ordered asset searches, balance queries, and blocking and unblocking orders | | **Lerian Consignado — Dataprev** | Dataprev | Payroll-deductible (consignado) credit operations exchanged with Dataprev | ## How the two models coexist *** * A **native rail** connects your institution directly, on Lerian-owned software, with no connectivity partner between you and BACEN. Lerian SPI carries Pix, and it covers the full SPI messaging contract. Lerian SPB carries TED and the currently supported SPB messaging flows. * A **partner interface** connects your institution through a connectivity partner — Pix through **JD** and **BTG**, TED through **JD**, boletos and bill payment through **BTG**. The partner absorbs the mechanics of the BACEN link. Your integration with the Lerian platform stays the same. Both models preserve your ledger, accounts, and business logic regardless of how a message reaches the rail. They do not require a particular ledger; the rail emits settlement facts for your ledger consumer. In the navigation, Pix and TED show the native rail and the partner interfaces together. The rails that only have Lerian native messaging — SILOC, SLC, STA, SISBAJUD, and Consignado — have their own sections. For the paths that run in production today, see [What are Brazil Rails?](/en/rails/what-are-brazil-rails). # How Lerian SILOC works Source: https://docs.lerian.studio/en/rails/native/siloc/how-siloc-works Gateway connection lifecycle, opt-in SFN ingestion, supported-message dispatch, at-least-once delivery with durable deduplication, and certificate handling. Lerian SILOC exposes a small set of typed operations over the `/api/v1/siloc` surface. The live surface covers connectivity and administration. SFN ingestion is opt-in; when enabled, the service dispatches its supported message codes. It also keeps the participant, certificate, and coverage records that support this work. ## Gateway connection lifecycle *** The service opens and maintains **one** messaging-gateway connection to Nuclea's SILOC over the national financial-system network. Four guards protect the connection: * a **circuit breaker** that trips on repeated failure, * **automatic reconnect** with a capped backoff, * a **credential-disabled stop** that halts the gateway after a certificate revocation, and * a **readiness probe** that reports the gateway's connection and health. `SFN_INGEST_ENABLED` defaults to `false`. Set it to `true` to enable the consumer; when enabled, the service decrypts and decodes inbound messages from the regulated envelope before it dispatches a supported message. ## Inbound SFN dispatch *** After decoding, the ingress dispatches a supported message by its `CodMsg`. It accepts the following codes: | Code | Dispatch | | ----------------------- | ------------------- | | **PAG0102** | Period opening | | **LDL0021** | Deposit instruction | | **LDL0020 / LDL0020R2** | Credit settled | | **LDL0006 / LDL0006R2** | Credit returned | | **PAG0103** | Period close | The ingress does **not** route `PAG0101`. An unsupported code, an undecodable envelope, or a message with a blank `BCMSG.NUOp` is non-retryable: it bypasses dispatch, follows the dead-letter path, and is acknowledged. These messages have no processed-message deduplication key. ## At-least-once dispatch with durable deduplication *** The source can redeliver a message, so dispatch is **at-least-once**. For a successfully decoded, supported message with a nonblank `BCMSG.NUOp`, the service checks durable deduplication by the composite key **(`BCMSG.NUOp`, `CodMsg`)** before dispatch. It does not deduplicate by a message ID alone. The service writes the processed-message record only after a successful dispatch and before it acknowledges the source. It creates no such record for an undecoded or unsupported message. A failure before that record is written can leave the message eligible for another delivery. Do not treat this behavior as an unconditional end-to-end exactly-once guarantee. ## Failure outcomes *** * A **retryable** dispatch failure remains unacknowledged. With offset-safe source commits, the message is read again after a restart from the last committed offset; a persistent transient failure can stall its partition until restart. * A **non-retryable** failure, such as a malformed envelope or an unsupported code, follows the dead-letter path and is acknowledged so the partition can progress. * The processed-message record protects only the successfully decoded and supported messages described above. It does not promise retention of every frame or exactly-once effects across every downstream system. ## Ingestion observability *** * **List processed messages** — the audit feed of supported SFN messages that the service dispatched. * **Read ingestion status** — the current health state of the enabled ingestion path, plus the last-dispatched timestamp. ## Participant directory *** Lerian SILOC keeps a directory of the SILOC participants it settles for. You **register**, **list**, **get**, and **update** a participant, and read a participant's **status**. Each participant carries its ISPB, its role, and its operational status. Every registration seeds a status-history entry and emits a participant fact. Registration writes are idempotent through an idempotency key, so a retried registration does not create a duplicate. | Role | Meaning | | ------------------------ | ---------------------------------------------------------------- | | **Direct** | A direct SILOC participant. | | **Indirect** | A participant that settles through another institution. | | **Settling institution** | The institution that settles on behalf of indirect participants. | The operational status uses SILOC's seven-value status domain: | Value | Status | | ----- | ---------------------------------- | | 1 | Participating | | 2 | Excluded from the cycle | | 3 | Excluded from SILOC | | 6 | In cycle, excluded from SILOC | | 7 | Suspended | | 8 | Suspended, excluded from the cycle | | 9 | Inoperative (special regime) | ## Regulated certificates *** The service holds the connection's regulated certificates as references, not as secrets. You **register** a public certificate together with an **external custody reference**. The service parses the certificate for its subject, serial, and validity window, and stores **no** private key. You then **list**, **get**, and **revoke** certificates over the same surface. ## Capability coverage *** A read-only **capability listing** describes the settlement-coverage capabilities as a transparency surface. For each message type, it reports the direction, the implementation state, and the disposition. # Environment variables Source: https://docs.lerian.studio/en/rails/native/siloc/siloc-environment-variables Deploy-time environment variables for Lerian SILOC — runtime, SILOC settlement-queue connectivity, persistence, authentication, and certificates. Lerian SILOC is Lerian's native messaging integration for SILOC, the deferred-net settlement system that Núclea operates for boleto and card flows. You set these variables at deploy time. A change takes effect only after you restart the service. For the knobs that behave the same across every Lerian Go service — deployment posture, telemetry, and datastore conventions — see [BYOC configuration essentials](/en/reference/byoc-configuration). Lerian SILOC is an early-stage service. This page documents the current verified configuration surface. Expect it to grow as the rail matures. For example, Lerian plans a systemplane runtime-configuration plane but does not yet expose it. Treat the service's own environment reference as the source of truth for a given release. In the tables below, **Required** marks a variable you must set, globally or under the stated condition. `—` means no default. ## Runtime and server | Variable | Description | Default | Required | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------- | | `SERVICE_NAME` | Service identifier used in logs and telemetry. | Service default | No | | `ENV_NAME` | Runtime environment label. `production` arms the stricter authentication and TLS gates below. | `development` | No | | `DEPLOYMENT_MODE` | TLS-enforcement posture (`local`, `byoc`, `saas`). `saas` makes TLS mandatory and refuses to start without it. See [Deployment mode and TLS](/en/reference/byoc-configuration#deployment-mode-and-tls). | — | No | | `LOG_LEVEL` | Log verbosity (`debug`, `info`, `warn`, `error`). | `info` | No | | `SERVER_PORT` | HTTP listen port. The liveness and readiness probes bind this same port. | `9820` | No | ## Authentication Lerian SILOC authorizes protected routes through Access Manager. It runs default-closed: authentication stays on unless you disable it. | Variable | Description | Default | Required | | -------------- | ------------------------------------------------------------------------------------------------- | ------- | ------------- | | `AUTH_ENABLED` | Require Access Manager authentication on protected routes. Must be `true` in production and SaaS. | `true` | In production | | `AUTH_ADDRESS` | Access Manager service address. Must be a valid `https://` URL in production and SaaS. | — | In production | ## PostgreSQL The replica falls back to the primary host when you leave `DB_REPLICA_HOST` empty (single-node default). | Variable | Description | Default | Required | | ----------------- | ------------------------------------------------------------------- | --------- | ---------- | | `DB_HOST` | PostgreSQL host. | — | Yes (live) | | `DB_PORT` | PostgreSQL port. | `5432` | No | | `DB_USER` | Database user. | — | Yes (live) | | `DB_PASSWORD` | Database password. Sensitive — inject at deploy time; never commit. | — | Yes (live) | | `DB_NAME` | Database name. | — | Yes (live) | | `DB_SSLMODE` | libpq TLS mode. Use `require` or stronger in production. | `disable` | No | | `DB_REPLICA_HOST` | Optional read-replica host. Falls back to `DB_HOST` when unset. | — | No | ## Redis | Variable | Description | Default | Required | | ---------------- | -------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `REDIS_ADDRESS` | Redis/Valkey host and port. | — | No | | `REDIS_PASSWORD` | Redis password. Sensitive. | — | No | | `REDIS_DB` | Redis database number. | `0` | No | | `REDIS_TLS` | Sets the TLS posture reported by the Redis readiness check. It does not configure TLS for the Redis transport. | `false` | No | ## SILOC settlement connectivity Lerian SILOC maintains a single IBM MQ connection to the Núclea SILOC settlement queue manager. Setting `MQ_HOST`, `MQ_CHANNEL`, `MQ_QUEUE_MANAGER`, `MQ_SEND_QUEUE`, or `MQ_RECEIVE_QUEUE` makes the service validate the whole connection descriptor fail-closed at startup. `MQ_PORT`, `MQ_TLS_ENABLED`, and `MQ_SSL_KEY_REPOSITORY` alone do not activate that validation. | Variable | Description | Default | Required | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------- | | `MQ_HOST` | Queue-manager host. | — | Yes (live SILOC) | | `MQ_PORT` | Queue-manager listener port. | — | Yes (live SILOC) | | `MQ_QUEUE_MANAGER` | Queue-manager name. Must be the Núclea SILOC settlement queue manager. | — | Yes (live SILOC) | | `MQ_CHANNEL` | Server-connection channel. Must match the Núclea SILOC channel pattern `C02992335..1`, where `` is the participant's 8-digit ISPB. | — | Yes (live SILOC) | | `MQ_SEND_QUEUE` | Outbound send queue. | — | Yes (live SILOC) | | `MQ_RECEIVE_QUEUE` | Inbound receive queue. | — | Yes (live SILOC) | | `MQ_TLS_ENABLED` | Enable mutual TLS on the MQ channel. Must be `true` in production. | `false` | In production | | `MQ_SSL_KEY_REPOSITORY` | Path to the GSKit key repository (`MQSSLKEYR`) backing MQ TLS. The private key lives here, never in an environment variable. Required when `MQ_TLS_ENABLED=true`. | — | If TLS enabled | | `CAMARA_PAG_ISPB` | The SILOC câmara's 8-digit ISPB. Every inbound settlement message is authorized against this sender before it has any semantic effect. It matches the settlement channel prefix, so override it only for a test câmara. A value that is not 8 digits refuses to start. | `02992335` | No | ## Certificates | Variable | Description | Default | Required | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | -------- | | `CERT_READINESS_MIN_DAYS` | Days-to-expiry threshold that logs a warning for a currently valid certificate. It does not degrade readiness solely because the certificate is near expiry. Must be greater than 0. | `30` | No | ## Trust and custody | Variable | Description | Default | Required | | --------------------------- | --------------------------------------------------------------------------------- | ------- | -------- | | `SFN_TRUST_MANIFEST_PATH` | Path to the SFN trust manifest used to validate regulated-message trust material. | — | No | | `SFN_CUSTODY_CONFIG_PATH` | Path to the custody-provider configuration. | — | No | | `SFN_CUSTODY_CACHE_ENTRIES` | Maximum number of custody entries kept in the in-memory cache. | `16` | No | ## Observability | Variable | Description | Default | Required | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------- | | `ENABLE_TELEMETRY` | Enable OpenTelemetry instrumentation. | `false` | No | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint. | — | If telemetry enabled | | `STREAMING_ENABLED` | Enable publishing platform streaming events. When it is off, or on with incomplete streaming settings, the facts persist durably as `PENDING` until a healthy dispatcher drains them. With `true`, set the shared `STREAMING_*` settings (see [Streaming and outbox](/en/reference/byoc-configuration#streaming-and-outbox)). | `false` | No | | `LDL_FACT_CONSUMER_ENABLED` | Enable the consumer of the authoritative SPB LDL settlement facts. Each fact completes the SILOC instruction it authorizes. With `true`, `STREAMING_BROKERS` and the PostgreSQL datastore are required, and the service refuses to start without them. Disabled, its readiness check reports `skipped`; running, the check does not gate readiness. | `false` | No | ## Health and readiness Lerian SILOC exposes `GET /health` (liveness), `GET /readyz` (readiness), and `GET /version` on the main HTTP port. The service boots and serves these probes even before its datastores and settlement connection are reachable. `/readyz` reports the health of each dependency. See [Health and readiness](/en/reference/health-and-readiness) for the probe contract. # Integrating with Lerian SILOC Source: https://docs.lerian.studio/en/rails/native/siloc/siloc-integration Integrating with Lerian SILOC: opt-in SFN ingestion, supported-message dispatch, participant CloudEvents, and API conventions. Lerian SILOC is event-driven at its edges. When enabled, it consumes supported SFN messages from Nuclea and emits participant facts onto the platform. It has **no client-facing webhook consumers**. Downstream systems subscribe to the streaming topics rather than receive callbacks. ## What it consumes *** SFN ingestion is opt-in: `SFN_INGEST_ENABLED` defaults to `false`, so the consumer starts only when you explicitly enable it. When enabled, Lerian SILOC decrypts and decodes inbound messages from the regulated envelope and accepts `PAG0102`, `LDL0021`, `LDL0020`/`LDL0020R2`, `LDL0006`/`LDL0006R2`, and `PAG0103`. It does **not** route `PAG0101`. ## Ingestion and delivery semantics *** * Delivery and dispatch are **at-least-once**. For a successfully decoded, supported message with a nonblank `BCMSG.NUOp`, the durable deduplication key is **(`BCMSG.NUOp`, `CodMsg`)**, not a message ID alone. * The service writes that processed-message record only after a successful dispatch. It creates no processed-message record for an undecoded or unsupported message. A failure before the record is written can result in another delivery, so this behavior is not an unconditional end-to-end exactly-once guarantee. ## What it emits *** * **Participant facts** — `siloc.participant.registered` and `siloc.participant.updated` — published as CloudEvents on the platform's streaming backbone. Downstream systems track the participant directory without polling. ## No Midaz touchpoint *** Lerian SILOC has **no** direct ledger integration. It holds no accounting position and performs no monetary arithmetic. It routes bytes and emits facts. This rail does not book money. The settlement of the net position happens at SILOC and the STR. Any ledger posting lives with the service that owns the money movement. ## Money-path boundary *** The separate STR rail, [Lerian SPB](/en/rails/native/spb/what-is-lerian-spb), executes the **STR deposit leg** — the monetary settlement at BACEN. SILOC does not. Lerian SILOC carries the SILOC messages and administers the connection. It never moves money. ## API conventions *** * **Base path** is `/api/v1/siloc`, and errors follow the RFC 9457 problem-detail format. * **Auth** follows the platform's standard bearer-token scheme. * **Participant registration is idempotent** through an idempotency key, so a retried registration does not create a duplicate. * **Certificates are references, not secrets.** A registered certificate carries a public certificate and an external custody reference. It stores or returns no private key. # Operating Lerian SILOC Source: https://docs.lerian.studio/en/rails/native/siloc/siloc-operations Operate Lerian SILOC: BACEN settlement windows, ICP-Brasil certificates, opt-in SFN ingestion, durable deduplication, monitoring, alerts, and audit. SILOC settles on a **deferred-net, business-day** basis, and Lerian SILOC works within that one operational reality. The service keeps the gateway connection open and, when SFN ingestion is enabled, dispatches its supported messages. It holds no accounting position. ## Settlement windows *** SILOC settles on a deferred multilateral net basis, on business days. Nuclea defines the daily settlement windows for boleto and card products. Lerian SILOC records and applies the transfer-order messages that open, advance, reconcile, and close its cycle state; it does not calculate the monetary net position. ## Regulated certificates *** You register the gateway's certificates as a **public certificate plus an external custody reference**. The service holds no private key. When you register a certificate, the service parses its subject, serial, and validity window. You revoke the certificate over the API when you retire it. A **credential-disabled** state halts the gateway. A disabled or revoked certificate fail-closes the connection instead of running on invalid credentials. ## Contingency and recovery *** The SFN ingestion path has defined outcomes under failure; it does not promise that every frame is retained or that every operation has a single end-to-end effect: * SFN ingestion is opt-in. `SFN_INGEST_ENABLED` defaults to `false`; enable it explicitly before the consumer starts. * An undecodable envelope, a decoded message without a nonblank `BCMSG.NUOp`, or an unsupported `CodMsg` — including `PAG0101` — bypasses normal dispatch. A non-retryable failure follows the dead-letter path and is acknowledged so the partition can progress. * A retryable dispatch failure remains unacknowledged. With offset-safe source commits, it is read again after a restart from the last committed offset; a persistent transient failure can stall its partition until restart. * Delivery and dispatch are **at-least-once**. For a successfully decoded and supported message, the service checks durable deduplication by **(`BCMSG.NUOp`, `CodMsg`)** before dispatch and writes the processed-message record only after dispatch succeeds. This is not message-ID-only deduplication and is not an unconditional end-to-end exactly-once guarantee. ## Reconciliation *** Reconciliation runs at several grains so the connection's state never drifts: * **Processed-message dedup ledger.** For successfully decoded and supported messages with a nonblank NUOp, the ledger uses the composite key (`BCMSG.NUOp`, `CodMsg`) and records it only after a successful dispatch. Undecoded and unsupported messages do not receive a processed-message record. * **Message-processing audit feed.** The audit feed lists supported SFN messages that the service dispatched. * **Per-participant status and status history.** Each participant carries its operational status. The service keeps every status change as a status-event history entry. ## Monitoring, alerts, and audit *** Lerian SILOC exposes an operator surface for watching OT settlement cycles, connection and relay health, and participant status. The cycle and reconciliation surfaces report recorded state and reconciliation sets; they do not calculate aggregate position figures on read. * **OT settlement cycles.** [`GET /api/v1/siloc/cycles`](/en/reference/rails/siloc/list-cycles) and [`GET /api/v1/siloc/cycles/{cycleId}`](/en/reference/rails/siloc/get-cycle) list and inspect cycles. [`GET /api/v1/siloc/cycles/{cycleId}/reconciliation`](/en/reference/rails/siloc/get-cycle-reconciliation) returns the reconciliation result, and [`GET /api/v1/siloc/cycles/{cycleId}/recalculations`](/en/reference/rails/siloc/get-cycle-recalculations) returns the cycle's recálculo round chain — including each round's complement/deposit window closure. * **Settlement instructions.** [`GET /api/v1/siloc/settlement-instructions`](/en/reference/rails/siloc/list-settlement-instructions) and [`GET /api/v1/siloc/settlement-instructions/{instructionId}`](/en/reference/rails/siloc/get-settlement-instruction) return each cycle's obligation legs. [`POST /api/v1/siloc/rocs`](/en/reference/rails/siloc/ingest-roc) ingests a semantic ROC revision that supersedes prior values for the cycle. * **Operational alerts.** [`GET /api/v1/siloc/alerts`](/en/reference/rails/siloc/list-alerts) returns an active-only, keyset-paged feed. Alert types include `WINDOW_CLOSING` (a deposit/complement deadline is approaching), `RECALCULATION` (a cycle is in a recálculo round), `RELAY_DOWN`, `CONNECTION_DOWN`, `CERTIFICATE_EXPIRY`, and `SCHEDULE_CHANGE` (an operator recorded a contingency schedule announcement). Alerts clear atomically when the underlying condition resolves — for example, a cycle that settles clears its `RECALCULATION` alert on the settle path. Pass `activeOnly=false` to include deactivated alerts as history. * **Audit trail.** [`GET /api/v1/siloc/audit-records`](/en/reference/rails/siloc/list-audit-records) returns a paged, verbatim read of the audit trail — for example, to export the record of an operator action or a participant status change. Bounds `from` and `to` are RFC3339 instants (date-only values are rejected). ## Schedule and contingency *** The OT cycle grid and business-day calendar are compiled artifacts on the service — the API projects them verbatim; it never parses a Núclea schedule wire. * **Calendar and windows.** [`GET /api/v1/siloc/schedule/calendar`](/en/reference/rails/siloc/get-business-day-calendar) returns the business-day calendar, and [`GET /api/v1/siloc/schedule/windows`](/en/reference/rails/siloc/list-schedule-windows) returns the compiled canonical OT window grid — a static artifact, not a per-day read, served even when the datastore is down. * **Contingency schedule changes.** [`POST /api/v1/siloc/schedule/changes`](/en/reference/rails/siloc/record-schedule-change) records a contingency announcement the operator received out of band from Núclea. The body carries `reason` (≤500 chars), `origin` — the announcing channel or upstream reference (≤256 chars) — `effectiveAt`, the announced RFC 3339 instant the change takes effect, and an optional `windowSeq` naming the affected canonical window. The record is **append-only**: a later announcement never rewrites an earlier one. The newest announcement does become the single active `SCHEDULE_CHANGE` alert — recording one clears the prior alert and raises a new one whose deadline is `effectiveAt` verbatim. [`GET /api/v1/siloc/schedule/changes`](/en/reference/rails/siloc/list-schedule-changes) returns the recorded changes, newest first. Record a contingency schedule change: ```bash theme={null} curl -X POST https://siloc.example.com/api/v1/siloc/schedule/changes \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reason": "Núclea extended the OT window by 30 minutes", "origin": "Núclea e-mail ref 2026-07-22/01", "effectiveAt": "2026-07-22T18:30:00-03:00", "windowSeq": 2 }' ``` # What is Lerian SILOC? Source: https://docs.lerian.studio/en/rails/native/siloc/what-is-lerian-siloc Lerian SILOC is your participant connectivity to BACEN's SILOC deferred net settlement via Nuclea — gateway link, card-domain relay to SLC, and certificates. **Lerian SILOC** is a financial institution's participant-side connectivity to **SILOC**. SILOC is the deferred multilateral net settlement system of the Central Bank of Brazil (BACEN). The clearinghouse **Nuclea** operates it. SILOC nets many interbank credit-order transactions. It settles only the resulting **net position** on Nuclea's settlement account at the **STR**, BACEN's Reserve Transfer System. Two products settle through SILOC: **boleto** (Cobrança) and **cards**. Lerian SILOC gives an institution one direct path to SILOC. It owns the single messaging-gateway connection to Nuclea over the national financial-system network. It relays the card-domain settlement traffic to **Lerian SLC**, Lerian's card-settlement service. It also keeps the participant directory, the regulated certificate references, and a settlement-coverage view. ## What Lerian SILOC does — and does not — do *** Lerian SILOC carries messages and administers the participant's connection. It holds **no** position and executes **no** transfer. * SILOC and Nuclea perform the netting and the final settlement of the net position, not Lerian SILOC. * The separate STR rail, [Lerian SPB](/en/rails/native/spb/what-is-lerian-spb), executes the STR deposit leg — the monetary settlement at BACEN — not SILOC. * Lerian SILOC computes no net position and performs no monetary arithmetic. It carries messages and records settlement facts. The service is single-institution: it connects one financial institution to SILOC, with no multi-tenancy. ## Who it serves *** Lerian SILOC serves financial institutions that are **direct or indirect SILOC participants** under their own **ISPB**. The service opens and maintains their gateway connection to Nuclea. It relays the card-domain settlement family to Lerian SLC. It keeps a directory of the participants it settles for. It holds the regulated certificate references the connection requires. ## Glossary *** | Term | Meaning | | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **Lerian SILOC** | A financial institution's native, participant-side connectivity to SILOC — gateway connection, card-domain relay, and administration. | | **SILOC** — Sistema de Liquidação Diferida das Transferências Interbancárias de Ordens de Crédito | BACEN's deferred multilateral net settlement system, operated by Nuclea. | | **Nuclea** | The clearinghouse that operates SILOC and settles its net position at the STR/BACEN. | | **LDL** — Liquidação Diferida Líquida | Deferred multilateral net settlement, SILOC's settlement model. | | **Deferred net settlement** | Settling only the net position of many transactions at fixed daily cycle times, rather than each transaction individually. | | **STR** — Sistema de Transferência de Reservas | BACEN's Reserve Transfer System, where SILOC's net position is finally settled; the deposit leg is executed by the separate STR rail. | | **Participant** | An institution in SILOC, classified as direct, indirect, or settling institution. | | **ISPB** | BACEN's 8-digit participant identifier. | | **NUOp** — Número Único da Operação | BACEN's globally unique per-operation identifier, used as the deduplication key. | | **Boleto (Cobrança) and Cards** | The two products that settle through SILOC; card settlement is handled by the sibling Lerian SLC service. | For how Lerian SILOC sits alongside the other native rails and the partner interfaces, see [Native messaging and partner interfaces](/en/rails/native/native-messaging). # How Lerian SISBAJUD works Source: https://docs.lerian.studio/en/rails/native/sisbajud/how-sisbajud-works How Lerian SISBAJUD works: judicial-order file intake, block execution against the Midaz ledger, event-driven permanent block, unblock, and BACEN result files. BACEN's file-exchange channel delivers every judicial order to Lerian SISBAJUD as a **remittance file**. Orders enter the integration as remittance files, not as API order payloads. The integration receives the file, parses it into canonical orders, and fulfils each order against the **Midaz** ledger. It then returns result files to BACEN. This page covers the three order kinds most institutions meet first: **block**, **unblock**, and **permanent block**. The integration also fulfils judicial transfer, precautionary (cautelar) account block, annulment, cancellation, guarantee liquidation, information requests, and confidentiality-breach orders. ## Order intake by file *** BACEN delivers a **remittance file**. The service accepts `5301`, `5303`, and `5308` in production, and `5311`, `5313`, and `5318` in homologation. Each code identifies a distinct file flow and parser. After a remittance object has been delivered, `POST /remittance-files/notifications` synchronously receives and parses that already-delivered object. Lerian SISBAJUD hashes the plaintext, envelope-encrypts it, stores the ciphertext, and normalizes the file into canonical judicial orders. The block-remittance flow (`5301` in production and `5311` in homologation) uses a **fixed-width** format. Each record is 684 characters and uses ISO-8859-1 encoding. The leading positions set each record type. A header and a trailer mark the file boundaries. The body carries the order records — block and reiteration, unblock, and permanent-block interruption among them. A per-order **permanent-block indicator** marks each block record: | Indicator | Meaning | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **T** | Traditional block — an initial, non-permanent attempt. After a partial initial block, a credit later that day can trigger complementary attempts. | | **P** | Permanent block with no deadline. | | **D** | Permanent block with a determined deadline. | ## Block execution (bloqueio) *** For a block order, Lerian SISBAJUD decrypts the encrypted CPF/CNPJ only to query CRM and never logs it; it does not resolve accounts by token alone. It then resolves the defendant accounts and executes the requested block through Midaz. An execution can create per-account block holdings. Every ledger transaction that Midaz accepts must be balanced. That invariant applies per accepted ledger transaction; it does not make an entire judicial order or a particular block or unblock leg a single double-entry transaction. Each account produces an outcome: the blocked amount against the ordered amount. This outcome feeds both the **response file** to BACEN and the tamper-evident audit trail. ## Permanent block (reiteração) *** A permanent order (indicator **P** or **D**) does not stop after the first attempt. It stays in monitoring and re-attempts the block until its deadline. This captures funds that arrive after the first attempt. Reattempts are event-driven, not scheduled. A ledger balance-change event wakes each reattempt. When a monitored account balance rises, the integration blocks the still-outstanding amount. Monitoring has a bound. A permanent order runs until the CNJ **60-day reiteration ceiling** or its own deadline, whichever applies. The integration then sweeps the expired order closed. ## Unblock (desbloqueio) *** An unblock order releases previously held funds according to the order. It can release amounts across per-account block holdings. Each accepted ledger transaction must be balanced; an unblock order is not necessarily one ledger transaction. The integration reports the outcome in the return file. ## Response and return files *** Lerian SISBAJUD generates and transmits result files back to BACEN over the same file-exchange channel: * A **syntactic-validation result** (file type `5303` in production, `5313` in homologation) that reports whether the remittance passed structural validation. * **Fulfilment result** files that carry the per-order, per-account outcomes of the blocks and unblocks. The file-exchange transport itself is the client-owned integration that [Lerian STA](/en/rails/native/sta/what-is-lerian-sta) describes. Lerian SISBAJUD is one of its downstream source products. It consumes the inbound judicial files that STA delivers. # Environment variables Source: https://docs.lerian.studio/en/rails/native/sisbajud/sisbajud-environment-variables Deploy-time environment variables for Lerian SISBAJUD: KMS and secret backends, S3 object storage, envelope-encryption keys, workers, and Midaz ledger. Lerian SISBAJUD is the Lerian-owned rail that fulfils judicial asset-blocking orders and protects the personal data they carry. You set these variables at deploy time. They take effect only after a service restart. [BYOC configuration essentials](/en/reference/byoc-configuration) documents the universal backbone that every Lerian Go service shares: server, datastores, multi-tenancy, telemetry, plugin authentication, and licensing. This page covers only the variables distinctive to Lerian SISBAJUD. In the tables below, the **Default / Required** column shows the default value. A bold qualifier (for example **Required** or **Required if enabled**) marks the variables you must set. `—` means no default. Any variable flagged **Sensitive** carries credential or key material. Inject it from your secret manager at deploy time. Never commit a value. ## Service and runtime | Variable | Default / Required | Description | | --------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SERVER_ADDRESS` | — | Main HTTP listen address; no code default, so set it explicitly (the reference deployment uses `:4029`). The liveness, readiness, metrics, and version probes bind this same port. | | `ENVIRONMENT_NAME` | — | Runtime environment: `local`, `development`, `staging`, `e2e`, `test`, or `production`. `ENV_NAME` is accepted as an alternative name. Unset stays empty and is treated as production-like, so the strictest security gates arm fail-closed. | | `SYSTEMPLANE_ENABLED` | `false` | Enable the [Systemplane](/en/reference/systemplane/overview) runtime-configuration admin API under the `/system` prefix on the main port. Off by default (environment-variable-only mode). | | `DEFAULT_TENANT_ID` | **Required in single-tenant mode** | Tenant UUID used in single-tenant mode. There is no effective string default: explicitly set a valid UUID for usable single-tenant operation. The tenant is the database-isolation boundary and can contain several institutions; each institution routes by its own identifier inside the tenant. With auth disabled, the rail also falls back to this UUID as its single configured institution. | Lerian SISBAJUD exposes `/health` (liveness), `/readyz` (readiness), `/version`, and `/metrics` on the main port. When you enable multi-tenancy, it also exposes `GET /readyz/tenant/{id}`. See [Health and readiness](/en/reference/health-and-readiness) for the probe contract. ## Security backends The service validates both selectors at boot. They choose the backends that protect court-ordered seizure data. In production, an unset or unsupported value fails the boot closed. Outside production, the selectors default to `vault` and `local`. | Variable | Default / Required | Description | | ------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `KMS_PROVIDER` | `vault` (non-production) · **Required in production** | Envelope-encryption key manager: `vault` (HashiCorp Vault Transit) or `aws` (cloud KMS). Read once at boot; not hot-reloadable. There is no in-memory provider. | | `SECRET_STORE_PROVIDER` | `local` (non-production) · **Required in production** | Where service secrets are read from: `vault` or `local`. `local` is development-only — a production boot refuses it, so production means `vault`. `aws-secrets-manager` passes validation but its backend is not wired yet; selecting it fails the boot closed. | | `CONNECTOR_CREDS_SOURCE` | `env` | Where the connector resolves and registers per-institution Midaz credentials: `env` keeps the legacy environment-credential behavior; `kek-db` routes resolution and registration through the per-institution KEK-envelope store in Postgres. Unset resolves to `env`; any other value fails the boot closed. `kek-db` requires the envelope-encryption (KMS) backend, or the boot fails closed. | `KMS_PROVIDER` and `SECRET_STORE_PROVIDER` each pull in a companion block, and the service validates every block at boot. `CONNECTOR_CREDS_SOURCE` has no block of its own — `kek-db` rides on whichever KMS backend `KMS_PROVIDER` selects. The `vault` value, for either selector, requires the Vault variables below. `KMS_PROVIDER=aws` requires the shared `AWS_REGION`. `SECRET_STORE_PROVIDER=local` reads secrets from the environment and needs no external backend. The `aws-secrets-manager` backend is not wired yet; selecting it fails the boot closed. Per-institution connector credentials no longer flow through the secret store: `CONNECTOR_CREDS_SOURCE=kek-db` resolves and registers them in the KEK-envelope store in Postgres, under the KMS backend. `KMS_PROVIDER` defaults to `vault` when unset outside production. In production, you must set it explicitly or the boot fails closed. `SECRET_STORE_PROVIDER` defaults to `local` the same way. In production, you must set it explicitly too. ### Vault (when `KMS_PROVIDER=vault` or `SECRET_STORE_PROVIDER=vault`) | Variable | Default / Required | Description | | ------------------------------------ | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `VAULT_ADDR` | **Required for the Vault provider** | Address of the client's Vault. | | `VAULT_AUTH_METHOD` | `token` | Authentication method: `token` (static `VAULT_TOKEN`) or `approle` (AppRole role and secret ids). | | `VAULT_TOKEN` | **Required if `token`, in production** | Service token for Vault. Sensitive. Non-production falls back to a development token. | | `VAULT_APPROLE_ROLE_ID` | **Required if `approle`** | AppRole role id. Sensitive. | | `VAULT_APPROLE_SECRET_ID` | **Required if `approle`** | AppRole secret id. Sensitive. | | `VAULT_TRANSIT_MOUNT_PATH` | `transit` | Mount path of the Transit engine used for envelope encryption. | | `VAULT_KV_MOUNT` | `sisbajud-secrets` | Mount path of the KV v2 engine backing the generic secret store when `SECRET_STORE_PROVIDER=vault`. Per-institution connector credentials no longer read from it — they follow `CONNECTOR_CREDS_SOURCE`. | | `VAULT_TOKEN_RENEW_ENABLED` | `true` | Run a background renewer that refreshes the Vault token before its lease lapses. | | `VAULT_TOKEN_RENEW_MIN_INTERVAL_SEC` | `60` | Floor, in seconds, between renewal attempts. | | `VAULT_TIMEOUT_SEC` | `15` | Per-request timeout, in seconds, for each Vault round-trip. | ### AWS (when `KMS_PROVIDER=aws` or `SECRET_STORE_PROVIDER=aws-secrets-manager`) | Variable | Default / Required | Description | | ------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AWS_REGION` | **Required with `KMS_PROVIDER=aws`** | Shared region for the AWS adapters — AWS KMS today, and the AWS secret-store once it is wired. Boot fails closed when `KMS_PROVIDER=aws` and it is blank. Credentials resolve through the default AWS SDK chain. | | `AWS_ENDPOINT_URL` | — | AWS-compatible endpoint override. Leave unset in real AWS environments so the SDK uses its default endpoints. | ## Crypto lifecycle Envelope encryption uses a per-record data key sealed under the institution's master key, plus a blind index for exact-match lookup on fiscal identifiers. | Variable | Default / Required | Description | | ---------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `SISBAJUD_DEK_CACHE_TTL` | `5m` | Lifetime of an unwrapped data-encryption key in the in-memory cache before the KMS is asked to unwrap again. Go-duration string. | | `SISBAJUD_DEK_CACHE_MAX_ENTRIES` | `50000` | Ceiling on cached data-key primitives; bounds heap during a large batch decrypt. | | `SISBAJUD_HMAC_COEXISTENCE_WINDOW` | `720h` | Window during which blind-index hashes from the previous HMAC key version stay queryable across a key rotation. Go-duration string. | | `KEK_REWRAP_BACKFILL_ENABLED` | `false` | Enable the background sweep that advances behind data-key rows to the active master-key version after a rotation. | | `REHASH_BACKFILL_ENABLED` | `false` | Enable the background sweep that re-hashes trailing blind-index rows to the new primary HMAC key version. | ## Domain workers Judicial-order processing runs as a set of per-institution background crons. All are off by default except the processing-lock reaper, which runs by default. The workers use the `*_SCAN_INTERVAL` (seconds) and `*_BATCH_SIZE` cadence knobs where applicable. | Variable | Default / Required | Description | | -------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `EXECUTION_ENABLED` | `false` | Master switch for the order-execution engine. When off, the FIFO orchestrator and downstream dispatch stay dormant. | | `ORCHESTRATOR_LOCK_TTL` | `30` | Per-subject execution lock lease, in seconds. | | `ORCHESTRATOR_RENEW_INTERVAL` | `10` | Cadence, in seconds, at which the owning worker renews the lock. Must stay strictly below `ORCHESTRATOR_LOCK_TTL` or boot fails closed. | | `PROCESSING_LOCK_REAPER_ENABLED` | `true` | Enable the background reaper that deletes expired processing-lock rows for each tenant. | | `PROCESSING_LOCK_REAPER_INTERVAL_SEC` | `300` | Reaper sweep cadence in seconds. When this variable is unset or non-positive, the service uses 300 seconds. | | `UNBLOCK_EXECUTION_SCAN_INTERVAL` | `60` | Pending-unblock sweep cadence in seconds. Shares the `EXECUTION_ENABLED` gate. | | `UNBLOCK_EXECUTION_BATCH_SIZE` | `500` | Pending unblock orders processed per tenant pass. | | `PERMANENT_BLOCK_EXPIRY_ENABLED` | `false` | Enable the daily scan that expires permanent blocks past their deadline. | | `RECONCILIATION_ENABLED` | `false` | Enable the scan that reconciles monitoring orders against the ledger and persists detected gaps. | | `RETURN_FILE_GENERATION_ENABLED` | `false` | Enable generation of SISBAJUD return files for unreturned terminal orders. | | `INFORMATION_RETURN_FILE_GENERATION_ENABLED` | `false` | Enable generation of AJUD309 information-response files. | | `SLA_ALERT_ENABLED` | `false` | Enable the evaluator that classifies active orders by SLA-risk band and emits the bands as metrics. | | `RETURN_FILE_ENVIRONMENT` | `HOMOLOGATION` | Regulatory environment stamped into generated return files. | ## Object storage Lerian SISBAJUD writes court-ordered seizure artifacts to an S3-compatible object store, already encrypted. The blob layer never sees plaintext. | Variable | Default / Required | Description | | ----------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SEAWEEDFS_S3_ENDPOINT` | `http://localhost:8333` | Endpoint of the S3-compatible object store. Storage wiring failures are non-fatal: the service boots and the readiness probe reports the store as unavailable. | | `SEAWEEDFS_BUCKET` | `sisbajud` | Bucket for remittance and return artifacts (already encrypted). | | `SEAWEEDFS_REGION` | `us-east-1` | S3 region label required by the AWS SDK. | | `SEAWEEDFS_ACCESS_KEY` | — | Object-store access key. Sensitive. Leave blank when the store needs no auth. | | `SEAWEEDFS_SECRET_KEY` | — | Object-store secret key. Sensitive. Leave blank when the store needs no auth. | | `STA_INBOUND_BUCKET` | `sta-files` | Bucket holding the raw remittance objects a reception notification points at. | | `STA_FILE_LOCK_TTL` | `5` | Per-file processing-lock TTL, in minutes. | ## Midaz ledger connector Lerian SISBAJUD reads balances and blocks through the Midaz ledger. `MIDAZ_BASE_URL` is an optional service-wide fallback; per-institution connector metadata takes precedence. | Variable | Default / Required | Description | | --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MIDAZ_BASE_URL` | — (optional fallback) | Base URL of the Midaz ledger. Used only when per-institution metadata provides no URL. Connector resolution fails closed only if neither provides a URL. | | `MIDAZ_AUTH_ENABLED` | `false` | Enable machine-to-machine authentication to Midaz. | | `MIDAZ_AUTH_ADDRESS` | **Required if enabled** | Auth service address for minting Midaz tokens. | | `MIDAZ_CLIENT_ID` | **Required if enabled** | OAuth client id for Midaz. Ignored in multi-tenant mode (resolved per tenant). | | `MIDAZ_CLIENT_SECRET` | **Required if enabled** | OAuth client secret for Midaz. Sensitive. Ignored in multi-tenant mode (resolved per tenant). | # Integrating with Lerian SISBAJUD Source: https://docs.lerian.studio/en/rails/native/sisbajud/sisbajud-integration Integrating with Lerian SISBAJUD: Midaz balance events that drive permanent-block reattempts, business events emitted, ledger connector, and delivery semantics. Lerian SISBAJUD is event-first where it meets the ledger. It **consumes** balance-change events from Midaz to drive permanent blocks. It **emits** operational business events that other systems can observe. It sends no outbound webhooks to third-party consumers; its interfaces are the ledger, the remittance-delivery notification endpoint, and its administrative surface. ## Ledger events it consumes *** The integration subscribes to Midaz **balance-change events**. It treats each event as a **trigger, not a rule**. An event decides nothing by itself. It wakes the permanent-block logic to re-evaluate a monitored account and attempt the outstanding block. A *reiteração* uses this path to capture funds that arrive after the first attempt. After a partial initial traditional block, a credit later that day can also trigger complementary attempts. Each event carries the **tenant identity**. The integration correlates each balance change only to orders of the same institution. Per-institution isolation holds across the event boundary. ## Business events it emits *** Lerian SISBAJUD emits operational business events with **no personal data** in their payloads: * A **block-account-created** event when an execution creates a block holding — an operational signal, not a money or PII carrier. * A **key-rotated** event when an institution rotates its master encryption key. These events serve observability and coordination. The money movements live in the Midaz ledger, not in the event payloads. ## Midaz boundary *** **Midaz** is the ledger connector. Through it, Lerian SISBAJUD: * **submits** block and unblock operations for the relevant customer accounts and block holdings * **creates** and **archives** block holdings as an execution needs them * **decrypts** the CPF/CNPJ only for the CRM query and never logs it; it does not resolve an account by token alone * **reads** the available balance of a monitored account * **reconciles** monitoring orders against ledger balance snapshots The downstream block code makes block writes **idempotent**. A retried block does not double-post. ## Delivery semantics *** * **Idempotent block writes.** The downstream block code keys each block write. A replay does not double-post. * **At-least-once events.** Under normal cache operation, Lerian SISBAJUD suppresses duplicate event identifiers. If the cache is unavailable, deduplication fails open and the event proceeds; downstream block-code idempotency is the backstop against duplicate block application. * **Tenant on every event.** The institution identity travels with each event. This keeps per-institution isolation intact from end to end. * **No outbound third-party webhooks.** Lerian SISBAJUD does not push to third-party webhook consumers. It also exposes an operational remittance-delivery notification endpoint; that endpoint is not an order-entry API. ## File transport *** Judicial files enter and leave over BACEN's file-exchange channel. [Lerian STA](/en/rails/native/sta/what-is-lerian-sta) documents this client-owned integration. Lerian SISBAJUD is a **source-product consumer** of that transport. Lerian STA delivers the inbound remittance object and its integrity claim-check; `POST /remittance-files/notifications` then synchronously receives and parses the already-delivered object. Lerian SISBAJUD executes the order and returns the result files. # Operating Lerian SISBAJUD Source: https://docs.lerian.studio/en/rails/native/sisbajud/sisbajud-operations Operating Lerian SISBAJUD: per-institution isolation, reconciliation, permanent-block cadence, KEK and credential rotation, and envelope-encryption guarantees. Lerian SISBAJUD handles the operational realities of judicial asset blocking. It fulfils and reports each order per institution, re-attempts permanent orders for weeks, and keeps personal data out of plaintext. It also proves every change it makes, because the law demands that proof. Per-institution isolation frames all of this. ## Per-institution isolation *** Lerian SISBAJUD is **multi-tenant**. The tenant is the database-isolation boundary. Lerian SISBAJUD reads it from the `tenantId` claim, and single-tenant mode uses the explicitly supplied `DEFAULT_TENANT_ID`. It has no effective string default, so a usable single-tenant deployment must set a valid UUID. The institution is a separate unit. Lerian SISBAJUD isolates each institution's orders, files, credentials, and encryption keys. One tenant can hold many institutions, and one deployment serves them all with no cross-institution visibility. ## Reconciliation *** Reconciliation works at the grain of **one monitoring order against a ledger balance snapshot**. A **scheduled scan** compares outstanding orders to the ledger and records any gap it finds. An operator can also start a **manual reconciliation** on demand. A separate daily sweep closes monitoring orders that pass their ceiling or deadline, so nothing lingers past its legal life. ## Permanent-block cadence *** Permanent orders re-attempt the block **on ledger balance-change events**. The CNJ **60-day reiteration ceiling**, or the order's court deadline, bounds each order. There is no scheduled reattempt pass. A deposit wakes the reattempt directly, and a daily expiry sweep closes orders whose deadline has lapsed. ## Credentials and key rotation *** Two things rotate per institution, and both are administrative: * **Connector credentials.** Lerian SISBAJUD uses these credentials to reach the ledger and the file-exchange channel. An operator **registers and rotates** them. * **Key-encryption key (KEK).** An operator rotates the institution's KEK, and the rotation emits a `kek.rotated` event. Each record's **data key (DEK)** sits under the KEK. A rotation therefore bumps the active KEK version without re-encrypting any field. Data keys sealed under the previous version stay readable, and a background re-wrap then advances them to the new version. A rotation that another rotation is already running returns `SBJ-0007` / **409**. A rotation that commits but cannot be audited returns `SBJ-0002` / **500** — the KEK has already advanced, so this is **not retryable**: retrying rotates a second time and widens the audit gap. Escalate and reconcile the audit trail against the `kek.rotated` event instead. ## Data protection *** Lerian SISBAJUD protects personal data at rest by design: * **Envelope encryption.** Each record carries its own data key, sealed under the institution's KEK with **AES-256-GCM**. Per-field additional authenticated data binds each ciphertext to one field and record, so no one can swap a ciphertext between fields or records. * **Searchable tokenization.** A **blind index** supports exact-match indexing on fiscal identifiers (CPF/CNPJ) and the process number without plaintext storage. It does not replace account discovery: to query CRM, Lerian SISBAJUD decrypts the CPF/CNPJ only as needed and never logs it. Lerian SISBAJUD encrypts free text only and never tokenizes it. It does not tokenize monetary values, and it stores the order tables' monetary values as plaintext, not ciphertext. * **Tamper-evident audit trail.** Lerian SISBAJUD appends every state change to a gap-free, cryptographically protected log. A per-event authentication code binds each entry to its position. **Merkle leaf hashes** let an operator verify the trail without decrypting any payload. * **LGPD.** A subject-access request **exports** the data the institution holds on a subject. **Crypto-erasure** honours an erasure request. It destroys the record's key and tombstones its plaintext values, so the personal data becomes irrecoverable, while the audit record of the change survives. ## Contingency *** Lerian SISBAJUD drives failed orders to a **FAILED** terminal state, not stuck in processing, and an operator can **reprocess** them. The **SLA-status** and **processing-statistics** views show where orders stand. Connector resolution **fails closed**. If the integration cannot safely resolve an account or a credential, it refuses the operation rather than act on an ambiguous target. ## HTTP surface *** Orders arrive only by file, so the HTTP surface **submits no court orders**. Most endpoints are administrative and observational. `POST /remittance-files/notifications` is operational: it synchronously receives and parses a remittance object already delivered through the file-exchange channel; it does not accept an order payload. An operator can: * list orders and files, and read the detail of a single order or file * **reprocess** failed orders * run a **reconciliation** and read its status * view **SLA status** and **processing statistics** * **verify** the audit trail * handle **LGPD** requests — create them, resolve them, run an erasure, and export a subject's data * register **non-compliance justifications** * configure the institution, and **register or rotate** connector credentials # What is Lerian SISBAJUD? Source: https://docs.lerian.studio/en/rails/native/sisbajud/what-is-lerian-sisbajud Lerian SISBAJUD is your CNJ/BACEN judicial asset-blocking integration. It receives court orders by file and runs blocks and unblocks on the Midaz ledger. **Lerian SISBAJUD** connects a financial institution to **SISBAJUD** (Sistema de Busca de Ativos do Poder Judiciário), the Brazilian judicial asset-search and blocking system. The **CNJ** runs it with the **Banco Central (BACEN)**. It lets each institution comply with court orders that block, release, transfer, and report on a defendant's financial assets. Lerian SISBAJUD receives each judicial order as a fixed-layout file. Courts issue the orders, and BACEN delivers them through its **STA** file-exchange channel. The service runs each order against the client's **Midaz** ledger and sends result files back to BACEN. No order enters through the API. The service is multi-tenant and isolates each institution. It encrypts every fiscal identifier and sensitive field at rest. ## What Lerian SISBAJUD does — and does not — do *** * It is the **judicial-order fulfilment boundary** between the Judiciary and the institution's ledger. It receives court orders by file and finds the affected accounts. It runs the block or unblock on the **Midaz** ledger and reports each outcome to BACEN. * It **does not accept court orders through its API**. Court orders enter only as remittance files through BACEN's STA channel. `POST /remittance-files/notifications` is an operational endpoint: it synchronously receives and parses a remittance object already delivered through that channel; it is not an order-entry endpoint. The remaining HTTP surface is administrative and observational. * It **freezes funds without deleting them**. A block execution can create one or more per-account block holdings, and an unblock can release held amounts across those holdings. Every ledger transaction accepted by Midaz must be balanced; this does not mean an entire judicial order or an individual block or unblock is one double-entry transaction. * It is **multi-tenant** from the ground up. One deployment serves many institutions. It isolates each institution, so no order or file crosses an institution boundary. ## Who it serves *** Lerian SISBAJUD serves banks and financial institutions. Each one must honour judicial asset orders under the CNJ/BACEN SISBAJUD regime. Each institution keeps its own credentials, encrypted data, and ledger connection. One deployment serves many institutions at once, and none of them sees another's orders. ## Glossary *** | Term | Meaning | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Lerian SISBAJUD** | Lerian's participant-institution integration for judicial asset blocking under the CNJ/BACEN SISBAJUD system. | | **Judicial block (bloqueio)** | A court-ordered hold on a defendant's funds, recorded through ledger block holdings. | | **Unblock (desbloqueio)** | A court-ordered release of previously held funds. | | **Permanent block (bloqueio permanente / reiteração)** | A standing order that re-attempts blocking whenever a monitored account's balance rises, until its deadline, capturing funds that arrive later — colloquially the *teimosinha*. | | **Remittance file** | The fixed-width file the Judiciary and BACEN send, carrying the judicial orders. | | **Response / return file** | The fixed-width file the institution sends back with syntactic-validation and fulfilment results. | | **Block account / holding** | A ledger holding for blocked funds; an execution can have multiple per-account block holdings. | | **Envelope encryption (KEK/DEK)** | A per-record data key (DEK) sealed under the institution's key-encryption key (KEK), so personal data at rest is never plaintext. | | **Searchable tokenization (blind index)** | A one-way index that can support exact-match indexing without plaintext storage. It is not an account-discovery substitute: for a CRM query, SISBAJUD decrypts the CPF/CNPJ only as needed and never logs it. | | **Tamper-evident audit trail** | An append-only, cryptographically protected record of every state change, verifiable without decrypting payloads. | | **Crypto-erasure (LGPD)** | Honouring an erasure request by neutralizing the record's data key (DEK) so its ciphertext becomes irrecoverable. | For how Lerian SISBAJUD sits alongside the other native rails and the partner interfaces, see [Native messaging and partner interfaces](/en/rails/native/native-messaging). # How Lerian SLC works Source: https://docs.lerian.studio/en/rails/native/slc/how-slc-works How Lerian SLC works: canonical intake, ASLC file build, ICP-Brasil custody signing, Nuclea transmission, and NUliquid-keyed card settlement lifecycle tracking. Lerian SLC runs one settlement pipeline for every card operation. The pipeline takes in one operation and builds its ASLC file. The client's custody signs the file. Lerian SLC transmits the file to Nuclea and correlates the returns. The NUliquid tracks the operation through its lifecycle. ## Intake *** Operations enter Lerian SLC through two canonical modes: * **Canonical API** — a REST/JSONL intake for operations in Lerian's canonical shape. `external_id` is the caller's deduplication key: a second submission under an `external_id` already in use is refused with **409 Conflict** naming the existing operation, so a retried submission never settles twice. * **Direct ASLC XML** — an upload of ready ASLC XML for callers that already produce it. An **audited pass-through** mode also accepts artifacts that the client already signed and forwards them under audit. You submit three operation types today: **CREDIT**, **DEBIT**, and **ANTICIPATION**. A **CANCELLATION** enters through its own path, because it carries a regulatory reason code and the identifier of the operation it cancels. **SWEEP** (varredura) is a canonical type Lerian SLC emits itself; it is not accepted at intake. ## Build, sign, transmit, correlate *** Each operation flows through one pipeline: 1. **Validate** the operation against Nuclea's XSDs. 2. **Build** the ASLC file as UTF-16BE, no BOM, up to a configurable record ceiling that defaults to **50,000** records per file. The build splits the file automatically above that ceiling. Cancellation files are exempt: they are never split. 3. **Seal** the file. First compress it with **GZIP**. Then build the **SPB security envelope**, which the client's custody signs (see [Signing orchestration](#signing-orchestration) below). 4. **Transmit** the file to Nuclea over the channel configured for the tenant: **Connect:Direct** over the private RSFN network, or **REST** with **mTLS**. The REST channel adds a **per-request JWS** signature because it crosses the public internet; Connect:Direct does not need one, since the payload is already SPB-signed and the network is private. 5. **Correlate** Nuclea's returns back to the operations that produced them. Each return seam uses the key the counterparty echoes: a **RET** line by its 20-position control number — the zero-left-padded form of your `external_id` — an **ASLC023** or D+1 line by its **NUliquid**, and a **PRO** at file level by the transmitted slice's control number. A line whose key matches nothing is skipped as uncorrelated. The returns are the **PRO / ERR / RET** files and the **ASLC028** status message. ### When one return contradicts itself A single return file can state two different outcomes for the same reference, and the XSDs cannot reject that shape. Lerian SLC resolves it by policy, not by the order the lines appear in the file: * one outcome — applied as stated; * the same outcome repeated — applied exactly once; * an acceptance **and** a refusal for the same reference — **the refusal wins**, and the displaced acceptance is reported for reconciliation. The result is the same whichever order the two lines arrive in. The file is never aborted: every other reference in it keeps processing, and the return is still acknowledged. ## Message families *** | Flow | Message family | | ----------------------- | --------------------------------------------------------- | | Credit settlement | ASLC027 / ASLC028 | | Debit settlement | ASLC029 / ASLC030 | | Anticipation | ASLC031 / ASLC034 | | Returns and devolutions | ASLC041 / ASLC042 / ASLC043 | | Cancellation | ASLC060–ASLC067 | | Sweep (varredura) | ASLC050 / ASLC051 | | Domicile inbound | ASLC022 / ASLC023 / ASLC024 / ASLC025 / ASLC032 / ASLC033 | ## Settlement flows *** * **Credit (acquirer).** Operations enter through the canonical intake. Lerian SLC builds the credit file (**ASLC027**), then signs and transmits it. It correlates the **ASLC028** status and the PRO/ERR/RET returns. The NUliquid tracks each operation through its lifecycle. * **Debit and anticipation.** The same intake-build-sign-transmit pipeline runs for the debit (**ASLC029 / ASLC030**) and anticipation (**ASLC031 / ASLC034**) families. The status returns and NUliquid tracking mirror the credit flow. * **Cancellation.** The acquirer informs a cancellation (credit **ASLC060**, debit **ASLC064**). Lerian SLC relays it to the **IF Domicílio** (**ASLC061**). The domicile returns its processing outcome (**ASLC062**). Lerian SLC returns the outcome to the acquirer (**ASLC063 / ASLC067**). It then emits a cancellation-confirmed-by-domicile event with the NUliquid. ## IF Domicílio inbound *** As the domicile institution, Lerian SLC receives credit and debit settlement notices (**ASLC022 / ASLC024 / ASLC032**). It confirms them (**ASLC023 / ASLC025 / ASLC033**). It issues returns and devolutions (**ASLC041 / ASLC042 / ASLC043**). A webhook keyed by the NUliquid signals the credit to the merchant and carries the return evidence. ## Clearing and funding for the IF Liquidante *** For the settling institution, Lerian SLC consumes the inbound clearing messages over the RSFN. These are the file-receipt acknowledgement (**GEN0015**), the preview and final clearing position (**SLC0001**), the bilateral-movement divergence (**SLC0002**), and the participant's operational status (**PAG0101**). Lerian SLC builds the **clearing position per settlement cycle** and reconciles it against the expected instructions. It parses and surfaces the **SLC0002** bilateral divergence. It raises events for preview-available, final-available, deposit-required, and deposit-deadline-approaching. ## Operation lifecycle *** The NUliquid tracks every operation through an **11-state** lifecycle. | State | Meaning | | ----------------- | ------------------------------------------------------------- | | **CREATED** | The operation has been accepted into Lerian SLC. | | **QUEUED** | It is queued for the next file build. | | **SENT** | Its file has been transmitted to Nuclea. | | **ACKNOWLEDGED** | Nuclea has acknowledged receipt. | | **ACCEPTED** | Nuclea has accepted the operation. | | **REJECTED** | Nuclea has rejected the operation. | | **FORWARDED** | The settlement notice has been forwarded to the IF Domicílio. | | **CONFIRMED** | The domicile has confirmed. | | **SETTLED** | The operation has settled. | | **D1\_CONFIRMED** | Settlement is confirmed on D+1. | | **CANCELLED** | The operation has been cancelled. | Files carry their own **10-state** lifecycle, from build through transmission and return correlation. ## Signing orchestration *** Lerian SLC materializes the unsigned ASLC XML and then **delegates signing to a custody backend chosen per tenant**. The private key never leaves the client's custody, and Lerian never signs on the client's behalf. | Custody backend | Where the key lives | | ---------------- | ------------------------------------------------ | | **Software key** | A software-held key in the client's environment. | | **PKCS#11 HSM** | A hardware security module. | | **Cloud KMS** | A cloud key-management service. | SaaS deployments lock custody to cloud KMS through **client-side-wrapped key import**. The client wraps and imports its own key. The service stores only the public certificate and a key reference — never the private material. ## Transport *** Lerian SLC submits the online operations to Nuclea over **REST** — **mTLS** and a **per-request JWS** signature secure that channel (the SLC0908 / SLC0912 / SLC0915 online series). Inbound returns are polled and acknowledged over the same REST channel. File transmission is the one surface with a per-tenant choice: **REST** or **Connect:Direct** over the private RSFN. REST adds the per-request JWS because it crosses the public internet; Connect:Direct does not need it, since the payload already carries its SPB signature and the network is private. The Connect:Direct wire client is not provisioned yet. A tenant configured for it fails closed with a typed transport error rather than silently falling back to REST, so REST is the only channel that carries files today. # Environment variables Source: https://docs.lerian.studio/en/rails/native/slc/slc-environment-variables Deploy-time environment variables distinctive to Lerian SLC — Núclea/RSFN transport, the SFN inbound bus, the signing sidecar, and the settlement schedule. Lerian SLC is the Lerian-owned settlement rail that clears and settles over Núclea's SLC across the RSFN. You set these variables at deploy time. A service restart applies them. Every Lerian Go service shares a universal backbone — server, datastores, multi-tenancy, telemetry, plugin authentication, and licensing. [BYOC configuration essentials](/en/reference/byoc-configuration) documents that backbone once. This page covers only the variables distinctive to Lerian SLC. In the tables below, the **Default / Required** column shows the default value. A bold qualifier (e.g. **Required**, **Required in production**) marks a variable you must set. `—` means no default. A variable marked **Sensitive** carries credential or key material. Inject it from your secret manager at deploy time. Never commit a value. ## Service and runtime | Variable | Default / Required | Description | | ------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `SERVER_ADDRESS` | `:4111` | Main HTTP listen address. The liveness, readiness, metrics, and version probes bind this same port. | | `SYSTEMPLANE_ENABLED` | `false` | Enable the [Systemplane](/en/reference/systemplane/overview) runtime-configuration admin API under the `/system` prefix on the main port. Off by default (environment-variable-only mode). | | `TENANT_RUNTIME_CONFIG_ENABLED` | `false` | Enable the per-tenant runtime-config store and admin surface, resolving each tenant's connectivity transport from a per-tenant record rather than the shared configuration. Selection and policy only. | Lerian SLC exposes `/health` (liveness) and `/readyz` (readiness) on the main port, plus `/version` and `/metrics`. When you enable multi-tenancy, it also exposes `GET /readyz/tenant/{id}`. See [Health and readiness](/en/reference/health-and-readiness) for the probe contract. ## Núclea REST transport This channel submits to Núclea online over mutual TLS. Leave `NUCLEA_REST_BASE_URL` empty to keep the transport unwired. | Variable | Default / Required | Description | | ------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `NUCLEA_REST_BASE_URL` | — | Base URL of the Núclea node. Empty disables the online submitter. In production this points at the Núclea endpoint; locally it points at a simulator. | | `NUCLEA_REST_TIMEOUT_SEC` | `30` | Per-request timeout in seconds. | | `NUCLEA_REST_CB_CONSECUTIVE_FAILURES` | `5` | Consecutive failures that trip the circuit breaker open. | | `NUCLEA_REST_CB_OPEN_TIMEOUT_SEC` | `30` | How long the breaker stays open before half-open. | | `NUCLEA_REST_ALLOW_INSECURE_TLS` | `false` | Relax Núclea TLS verification for local development only. Boot is rejected when this is `true` under `ENV_NAME=production` or `DEPLOYMENT_MODE=saas`. | | `NUCLEA_REST_CLIENT_CERT_PATH` | **Required in production/SaaS** | Path to the PEM client certificate presented to Núclea for mutual TLS. | | `NUCLEA_REST_CLIENT_KEY_PATH` | **Required in production/SaaS** | Path to the PEM client private key for mutual TLS. Sensitive — the file holds private key material. | ## RSFN inbound consumer This channel receives inbound RSFN messages. It also observes XSD conformance. | Variable | Default / Required | Description | | --------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RSFN_CONSUMER_TRANSPORT` | `stub` | Inbound transport behind the RSFN consumer port: `stub` (a development REST channel) or `mq` (the RSFN network transport, wired at homologation). | | `MQ_BRIDGE_URL` | **Required if transport `mq`** | Base URL of the MQ bridge. You must declare it explicitly when `RSFN_CONSUMER_TRANSPORT=mq`; it cannot inherit `http://localhost:9121`, because the correct address depends on your deployment topology. | | `RSFN_CONSUMER_BASE_URL` | — | Base URL for the `stub` transport. Empty leaves the consumer unwired. | | `RSFN_CONSUMER_TIMEOUT_SEC` | `30` | Per-request timeout in seconds. | | `RSFN_CONSUMER_CB_CONSECUTIVE_FAILURES` | `5` | Consecutive failures that trip the circuit breaker open. | | `RSFN_CONSUMER_CB_OPEN_TIMEOUT_SEC` | `30` | How long the breaker stays open before half-open. | | `RSFN_CONSUMER_ALLOW_INSECURE_TLS` | `false` | Relax the stub-channel TLS verification for local development only. Forced `false` under `ENV_NAME=production` or `DEPLOYMENT_MODE=saas`. | | `RSFN_INBOUND_XSD_VALIDATION_ENABLED` | `true` | Observe-only XSD validation of inbound RSFN/SILOC messages against the official schemas. Logs and meters non-conformance without altering dispatch. | | `ASLC_INBOUND_XSD_VALIDATION_ENABLED` | `true` | Observe-only XSD validation of inbound ASLC return messages. Logs and meters non-conformance without altering the return pipeline. | | `XSD_VALIDATOR_URL` | `http://localhost:9091` | Base URL of the XSD-validation sidecar the observers call. | | `XSD_VALIDATOR_TIMEOUT_SEC` | `10` | Per-request timeout in seconds for XSD-validation calls. | ## SFN inbound bus The SFN bus relays SILOC messages. This path ingests them. The whole family stays inert unless you set `SFN_INGEST_ENABLED=true`. | Variable | Default / Required | Description | | ---------------------- | ---------------------------------------------- | ---------------------------------------------------------------- | | `SFN_INGEST_ENABLED` | `false` | Master switch for the SFN ingest path (bus consumer and driver). | | `SFN_BUS_PROVIDER` | `kafka` | Consumer adapter: `kafka` or `stub` (development/CI). | | `SFN_KAFKA_BROKERS` | **Required if ingest on and provider `kafka`** | Comma-separated Kafka/RedPanda bootstrap broker list. | | `SFN_KAFKA_TOPIC` | `siloc.inbound` | Topic the SILOC relay publishes to. | | `SFN_KAFKA_GROUP_ID` | service-scoped default | Kafka consumer group id; override per deployment when needed. | | `SFN_KAFKA_BATCH_SIZE` | `100` | Records read per batch. | | `SFN_STUB_TIMEOUT_SEC` | `30` | Per-request timeout in seconds for the `stub` adapter. | ## Signing sidecar Lerian SLC signs the SPB security envelope through a stateless signing sidecar. The private key never enters the service process. You select the custody backend per deployment. | Variable | Default / Required | Description | | ------------------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SIGNER_URL` | `http://localhost:9101` | Base URL of the signing sidecar. | | `SIGNER_TIMEOUT_SEC` | `10` | Per-request timeout in seconds for signing calls. | | `SIGNER_SOFTKEY_PFX_PASSPHRASE` | — | Passphrase for the software-key custody adapter. Sensitive. The software-key posture is refused under `ENV_NAME=production` unless explicitly allowed. | | `SIGNER_SOFTKEY_ALLOW_IN_PRODUCTION` | `false` | Permit the in-process software-key posture in production. Leave `false` — a hardware or cloud-KMS custody path is the production posture. | | `SIGNER_AWSKMS_DEFAULT_KEY_REF` | — | Optional fallback key reference for the cloud-KMS custody adapter, used only when a request supplies none. Key material never enters the process — only the reference is passed. The cloud-KMS adapter needs `SIGNER_AWSKMS_REGION`, the KMS key's AWS region, which the standard deployment derives from the shared `AWS_REGION`. | ## Settlement schedule Lerian SLC gates dispatch to Núclea's settlement windows and runs a per-tenant schedule worker. | Variable | Default / Required | Description | | ----------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SCHEDULE_CREDIT_WINDOW_OPEN` | **Required in every environment** | Credit-acceptance window open, as `HH:MM` São Paulo local time (half-open interval, opening boundary inclusive). Declare it explicitly for the grade in force. | | `SCHEDULE_CREDIT_WINDOW_CLOSE` | **Required in every environment** | Credit-acceptance window close, as `HH:MM` São Paulo local time (exclusive). Declare it explicitly for the grade in force. | | `SCHEDULE_WORKER_ENABLED` | `true` | Enable the recurring per-tenant schedule worker (cutoff alerting and, when enabled, the dispatch and return drivers). | | `SCHEDULE_WORKER_INTERVAL_SEC` | `30` | Schedule worker tick period in seconds. | | `SCHEDULE_GRID_GATE_ENABLED` | `false` | Gate dispatch to the calendar-backed STR grid instead of the single credit window. | | `SCHEDULE_CUTOFF_ALERT_LEAD_MIN` | `30` | Minutes before an STR cutoff at which the window enters pre-cutoff and one alert is emitted. | | `SCHEDULE_DISPATCH_MAX_TRANSMIT_ATTEMPTS` | `20` | Transmit attempts a dispatch may burn before the operation is parked for operator review instead of retrying forever. At the default 30-second tick, 20 attempts is about ten minutes before the first alert. | | `SCHEDULE_HOLIDAYS` | — | Comma-separated `YYYY-MM-DD` banking holidays in São Paulo local time; the grid never opens a window on a holiday. | You must declare both credit-window variables, even when you use the standing schedule. The service refuses to start if either value is omitted or blank; it never assumes a credit grade from a default. Lerian SLC ships a family of per-driver runtime toggles. Each toggle opts one flow into the schedule worker: credit and debit dispatch and forward, anticipation, cancellation, status relay, D+1 confirmation, and stuck-operation alerts. Each is a `SCHEDULE_*_ENABLED` switch that defaults to `false`. You opt each one in during homologation, once the per-tenant Núclea material is ready. The cadence and batch knobs follow the `*_INTERVAL_SEC` / `*_BATCH_SIZE` / `*_PAGE_SIZE` pattern. ## Webhooks Lerian SLC can deliver settlement events to a subscriber endpoint. In BYOC the client supplies the credential material through their own Kubernetes secret. | Variable | Default / Required | Description | | -------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `WEBHOOK_WORKER_ENABLED` | `false` | Enable the per-tenant webhook delivery worker. | | `WEBHOOK_DELIVERY_PROVIDER` | `direct` | Delivery adapter: `direct` (BYOC HMAC HTTP) or `notifications` (SaaS). | | `WEBHOOK_API_KEY` | **Required if delivering** | Client-defined API key sent in the webhook API-key header. Sensitive. | | `WEBHOOK_API_KEY_FILE` | — | Path to a file holding the API key (a mounted secret volume). When set, the inline value is ignored. | | `WEBHOOK_HMAC_SECRET` | **Required if delivering** | Client-defined shared secret used to sign each webhook. Sensitive. Never comes from the runtime-config plane or the database. | | `WEBHOOK_HMAC_SECRET_FILE` | — | Path to a file holding the HMAC secret (a mounted secret volume). When set, the inline value is ignored. | | `WEBHOOK_MAX_ATTEMPTS` | `6` | Per-subscription retry budget before a delivery is parked on the dead-letter queue. | | `WEBHOOK_ALLOW_PRIVATE_NETWORK` | `false` | Development-only relaxation of the delivery SSRF block. Must stay `false` in production/SaaS — boot fails closed otherwise. | | `WEBHOOK_NOTIFICATIONS_BASE_URL` | **Required if provider `notifications`** | Base URL of the notifications service used by the SaaS provider. | # Integrating with Lerian SLC Source: https://docs.lerian.studio/en/rails/native/slc/slc-integration Integrating with Lerian SLC: CloudEvents outbox families for card settlement lifecycle and clearing, HMAC-signed webhooks, and optional Midaz and Reporter. Lerian SLC is event-first at its edges. It consumes Nuclea's inbound messages. It emits its business facts through a **transactional outbox** onto the platform's streaming backbone and to **HMAC-signed webhooks**. Every event carries a **CloudEvents** envelope. Lerian SLC publishes an event only after its state change commits. ## What it consumes *** From Nuclea inbound, over the **RSFN**: * **GEN0015** * **SLC0001** — clearing preview and final * **SLC0002** * **PAG0101** — operational status Lerian SLC correlates each return seam by the key the counterparty echoes: a **RET** line by its 20-position control number, an **ASLC023** or D+1 line by its **NUliquid**, and a **PRO** at file level by the transmitted slice's control number. ## What it emits *** Every emission goes through the transactional outbox. Lerian SLC publishes an event if and only if its state change commits. Each event carries a CloudEvents envelope. The platform delivers it on the streaming backbone and to HMAC-signed webhooks. * **Operation lifecycle events** — one per operation state transition. * **`operation.status_relayed`** — the rail relayed an operation status to the IF Domicílio. * **`operation.forward_rejected`** — an outbound submission was parked and needs operator action. The payload, not the event name, says which leg and why. `stage` is `forward` — the leg to the IF Domicílio, paired with `parkReason` `permanent_rejection` — or `dispatch`, the leg to Núclea, paired with `attempt_budget_exhausted`. The payload's operation ids are what the un-park endpoint takes. * **`cancellation.confirmed_by_domicile`** — the IF Domicílio confirmed a cancellation and the event carries the NUliquid. * **`d1.value_mismatch`** — a D0-versus-D+1 value divergence. * **`certificate.expiring`** — a certificate approaching expiry. * The **`clearing.*`** family — `cycle_opened`, `cycle_closed`, `cycle_recalculated`, `preview_available`, `final_available`, `credit_settled`, `credit_returned`, `deposit_required`, `deposit_deadline_approaching`, and `divergence_reported`. The domicile-flow webhooks carry the **NUliquid**. Consumers use it to correlate each operation's merchant credit and its domicile-return evidence. ## Midaz touchpoint *** Lerian SLC is standalone. It does not post to a ledger itself. **Midaz** is an **optional** consumer. Midaz can subscribe to Lerian SLC's events or call its API to mirror settlement into the ledger. Lerian SLC does not depend on Midaz. ## Reporter touchpoint *** **Reporter** is an **optional** consumer of Lerian SLC's events. Reporter can also serve as an **optional pluggable provider of ASLC materialization** through templates. Lerian SLC keeps regulatory ownership of the ASLC layout. Reporter only renders it when you plug it in. ## Money-path boundary *** Lerian SLC owns the card-settlement messaging with Nuclea. It does not perform the monetary settlement itself. The netted position settles at the SLC and Nuclea. The reserve leg settles at BACEN. The card-domain deferred-net settlement family reaches Lerian SLC relayed from [Lerian SILOC](/en/rails/native/siloc/what-is-lerian-siloc), the participant-side SILOC connectivity. ## API conventions *** * **Idempotent intake.** Canonical API submissions are idempotent by `external_id`. Lerian SLC rejects a duplicate `external_id`, so a retried submission never double-settles. * **Online auth.** mTLS and a per-request JWS signature secure each individual online operation over REST. * **Certificates are references, not secrets.** A registered certificate carries its public certificate and a custody reference. Lerian SLC never stores or returns the private key. # Operating Lerian SLC Source: https://docs.lerian.studio/en/rails/native/slc/slc-operations Operating Lerian SLC: Nuclea's grade horária cycles, ICP-Brasil A1 custody and rotation, contingency and retransmit endpoints, and per-NUliquid reconciliation. Lerian SLC runs against two constraints of Nuclea's SLC: rigid daily settlement windows, and regulated ICP-Brasil credentials. The rail enforces the windows, holds the credentials in client custody, and reconciles at several grains. ## Settlement windows and grade horária *** Nuclea's SLC settles on rigid daily windows set by its grade horária. Lerian SLC evaluates each window as **OPEN**, **PRE\_CUTOFF**, or **CLOSED**, and gates dispatch on the result. The rail holds an operation submitted outside its window in the **QUEUED** state. It dispatches the operation when the window opens. This avoids Nuclea's out-of-window rejection (**ESLC0029**). Lerian SLC raises a proactive alert as a window approaches its cutoff. ## Certificates and custody *** Lerian SLC delegates signing to a client-controlled custody backend. The client's private key never enters the rail. The signing credential is an **ICP-Brasil A1**, **RSA-2048** certificate. You select the custody backend per deployment: a software key, a **PKCS#11 HSM**, or a **cloud KMS**. * **SaaS deployments** lock custody to a cloud KMS. The client wraps the private key locally and imports it. The rail then stores only the public certificate and a key reference. * **Rotation is atomic.** The client resolves the active certificate at connection time, so a rotation takes effect transparently. * **Expiry alerts** escalate at 30, 15, 7, and 1 day before expiry. The rail emits each one as a `certificate.expiring` event. ## Contingency and recovery *** * **Dead-letter queue.** The rail parks any inbound RSFN message it cannot process. It holds the message for review and never drops it. * **Retransmit and recovery endpoints.** An operator can retransmit a transmitted file. The recovery endpoints re-run dispatch, forwarding, and status polling. An outbound operation is **parked** with an alert instead of retrying forever, for one of two reasons that depend on the leg. A file forwarded to the IF Domicílio parks when one response is classified as final. A file dispatched to Núclea parks when consecutive refusals exhaust its transmit-attempt budget. The un-park endpoint is the only way out: it returns the operation to the eligible backlog so the next tick re-drives it. It does not reset the attempt counter — that zeroes only after a submit succeeds, so un-parking without fixing the cause parks the operation again on the next refusal. ## Reconciliation *** Lerian SLC reconciles at several grains so state never drifts: * **Per operation** — the rail tracks each operation by its NUliquid. * **Per file** — the rail tracks each file through its lifecycle and its returns. * **Per NUliquid** — the settlement identifier stays queryable for 30 days. * **Per clearing cycle** — the rail tracks the clearing net position per cycle and stage. It reports a bilateral divergence. It detects a D0-versus-D+1 value divergence on the D+1 confirmation and flags it. # What is Lerian SLC? Source: https://docs.lerian.studio/en/rails/native/slc/what-is-lerian-slc Lerian SLC is your client-owned software for operating Nuclea's Serviço de Liquidação Centralizada (SLC) — the ASLC engine, card roles, and multi-tenant model. **Lerian SLC** is the client-owned software layer that operates the **Serviço de Liquidação Centralizada (SLC)** for a participant. The SLC is Nuclea's centralized card-settlement messaging service. Lerian SLC runs inside the institution's own regulated environment. It signs with the institution's own **ICP-Brasil** certificate. It speaks the official **ASLC** message protocol to Nuclea (ISPB **29011780**) over the **RSFN**, Brazil's National Financial System Network. Lerian SLC builds, signs, and transmits the ASLC settlement files. It correlates Nuclea's returns. It tracks every operation through its settlement lifecycle by its **NUliquid**, Nuclea's settlement identifier. Lerian SLC is standalone, multi-tenant, and API- and event-first. ## What Lerian SLC does — and does not — do *** * It is **standalone**. Lerian SLC settles cards on its own and depends on no other Lerian product. Midaz and Reporter can consume Lerian SLC, but Lerian SLC does not depend on them. * The **private key never leaves the client's custody**. Lerian SLC materializes the unsigned file and delegates signing to a custody backend the client controls. Lerian never signs on the client's behalf. * It is **multi-tenant** from the ground up. One deployment serves many participants with full isolation. ## Who it serves *** Lerian SLC serves the roles of the Brazilian card arrangement. A single deployment can act for one or several of them. | Role | What it is | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Credenciadora** (acquirer) | Captures card transactions from merchants and submits their settlement operations. | | **Subcredenciadora** (sub-acquirer) | A participant that enables receiving merchants to accept payment instruments but does not act as creditor toward the issuer in settlement. | | **IF Domicílio** | The domicile financial institution that receives settlement notices and credits the merchant. | | **IF Liquidante** | The settling financial institution that funds the clearing position for a settlement cycle. | | **Bandeira** | The card scheme of the arrangement. | ## Glossary *** | Term | Meaning | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | **Lerian SLC** | Lerian's client-owned software for operating Nuclea's centralized card-settlement (SLC) messaging. | | **SLC** — Serviço de Liquidação Centralizada | Nuclea's centralized settlement service for the Brazilian card arrangement. | | **ASLC** | The official Nuclea XML message layout family used by the SLC — UTF-16BE, XSD-validated. | | **NUliquid** | Nuclea's 21-position settlement identifier (an `AAAAMMDD` date prefix plus a sequence) that tracks each operation's lifecycle. | | **ISPB** | The BACEN participant identifier; Nuclea's SLC destination is 29011780. | | **RSFN** | Brazil's National Financial System Network, which carries SLC traffic. | | **ICP-Brasil** | The Brazilian public-key infrastructure whose certificate signs SLC files. | | **SPB security envelope** | The BACEN payment-system envelope — GZIP plus an RSA/SHA-256 signature and AES-256, with a 588-byte security header — wrapping each file. | | **Clearing cycle** | The settlement window over which Lerian SLC computes and reconciles positions for the IF Liquidante. | For how Lerian SLC sits alongside the other native rails and the partner interfaces, see [Native messaging and partner interfaces](/en/rails/native/native-messaging). The card-domain deferred-net settlement family reaches Lerian SLC relayed from [Lerian SILOC](/en/rails/native/siloc/what-is-lerian-siloc), the participant-side SILOC connectivity. # How Lerian SPB works Source: https://docs.lerian.studio/en/rails/native/spb/how-spb-works The onboarding sequence, transfer and lifecycle flows, liquidity operations, and asynchronous queries that make up the STR message surface. Lerian SPB exposes the STR message surface as a set of typed operations. Each flow persists its work before dispatch, returns an accepted-but-not-settled state, and reconciles against BACEN's asynchronous reply. ## Onboarding *** Certificate rotation and GEN0006 activation are operational guidance, as is a GEN0001 connectivity echo; they are not code-mandated gates for submitting commands. Before side effects, a command checks rail readiness. `READY` and `DEGRADED` admit commands; `PENDING_CONFIGURATION` and `UNAVAILABLE` block them. Readiness evaluates channel configuration and certificate state. It does not require a GEN0006 acknowledgement or a GEN0001 echo. ## Bank transfers *** You declare the typed sender and recipient parties. The platform derives the STR transfer code — STR0004, STR0005, STR0006, STR0007, or STR0008. It persists the operation and the outbound work, and only then dispatches. The rail returns the status `ACCEPTED` — it accepted the request, but BACEN did not settle it yet. BACEN confirms the transfer asynchronously, and the operation moves to `CONFIRMED` or `REJECTED`. ## Returns and cancellations *** A return and a cancellation are both sub-resources of a parent operation. The parent's id in the request path ties each one to that parent. * A **return** requires a parent in a settled-outbound state: `SENT`, `CONFIRMED`, or `DELIVERED` (STR0010 pipeline). * A **cancellation** requires a parent that still admits one (STR0011 pipeline). The rail refuses a request against the wrong parent state. ## IBS repasse *** An **IBS repasse** (STR0053) transfers IBS settlement values from the reserve account to the managing committee. The rail translates the request to the STR0053 message, persists it, and dispatches it. BACEN confirms it asynchronously. ## Conta PI liquidity *** Two operations move liquidity between an institution's reserve/settlement account and its Conta PI: * **Aporte** (LPI0001) funds the Conta PI from the reserve account. * **Resgate** (LPI0003) recalls funds back to the reserve account. Both return `PENDING`. The inbound settlement reply (LPI0001R1 / LPI0003R1) moves the operation to `SETTLED` or `REJECTED`. Lerian SPB projects the reply value verbatim. GEN0021 operating-window grades are persisted and queryable; they do not impose a generic rejection of every submit outside an operating window. The explicit schedule-related admission rule is RLPI0007: during the additional-aporte window, an aporte to a foreign Conta PI is rejected. Aportes to the institution's own Conta PI and resgates remain allowed. ## Automatic-transfer policy *** You declare the STR automatic-transfer rule for an account class with LPI0005. BACEN acknowledges it (LPI0005R1) and later reports each automatic transfer it performs (LPI0006). Lerian SPB forwards the reported values verbatim and performs no arithmetic on them. ## Approval queue for high-value emissions *** The rail classifies each declared amount against half-open configured bands and can apply an override for a specific message type. The selected rule determines the state: `requiredSignatures` of `0` sends the emission to `SUBMITTED`; a nonzero value sends it to `PENDING_APPROVAL`. At a band ceiling, the next band applies; above all ceilings, the top-tier requirement applies. An operator can sign or deny a queued emission. Governance configuration hot-reloads through Systemplane. ## Curated message compose *** A catalog-gated compose surface builds an allowlisted STR message type (STR0004, STR0005, STR0006, STR0007, STR0008, or STR0053). The chosen type must resolve in the capability catalog and be publicly submittable. Otherwise the rail refuses the request. ## Asynchronous single-party queries *** Reserve-position and schedule reads return a correlation id with `PENDING`. BACEN's reply arrives asynchronously. You read it back by that correlation id. Lerian SPB projects what BACEN sends and computes no position of its own. | Query | STR code | Returns | | --------------------------- | -------- | -------------------------------------------------------------- | | Reserve-account balance | STR0013 | Correlation id + `PENDING`, then the balance BACEN reports | | Reserve statement (extrato) | STR0014 | Correlation id + `PENDING`, then the statement in message mode | | STR operating schedule | STR0001 | Correlation id + `PENDING`, then the operating-window grades | ## Inbound notices and roster *** * The rail records **notices** (GEN0001, GEN0004, GEN0005) to a queryable notice log. * **Schedule broadcasts** (GEN0021) update the operating-window grades — the last announcement wins. * A **responsável roster** submission (GEN0019) fully replaces the participant's roster. ## Reconciliation and recovery *** Settlement discrepancies open reconciliation cases. Operators record `ACKNOWLEDGE`, `RESOLVE`, `DISMISS`, or `INVESTIGATE` actions against a case. The rail locks a terminal case. A cross-rail stuck-operations view lists the items that need attention. It exposes only the actions the rail can perform. # Environment variables Source: https://docs.lerian.studio/en/rails/native/spb/spb-environment-variables Deploy-time environment variables for Lerian SPB — runtime, STR/RSFN connectivity, message signing and certificates, persistence, events, and security. Lerian SPB is Lerian's native messaging integration for the STR (Sistema de Transferência de Reservas), BACEN's real-time gross settlement system, reached over the RSFN. Set these variables at deploy time. A change takes effect only after you restart the service. Most variables are specific to this rail. For the knobs that behave the same way across every Lerian Go service — deployment posture, datastores, multi-tenancy, and telemetry — see [BYOC configuration essentials](/en/reference/byoc-configuration). In the tables below, **Required** marks a variable you must set — globally, or under the condition named. A qualifier such as *In production* or *If enabled* narrows when the requirement applies. `—` means no default. `ENV_NAME` arms the stricter security gates. Set it to `production` or `staging` and the service enforces TLS on Redis, RabbitMQ, and the STR connection. It also requires a hardware-backed signing custody backend and rejects development-only surfaces. `ENV_NAME` has no default. Set it explicitly. The service refuses to start when `ENV_NAME` is unset or holds an unrecognized value. ## Runtime and server | Variable | Description | Default | Required | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------- | | `ENV_NAME` | Runtime environment. Must be one of `production`, `staging`, or `develop`. Production and staging arm the security gates described above. | — | Yes | | `SERVICE_NAME` | Service identifier used in logs and telemetry. | Service default | No | | `LOG_LEVEL` | Log verbosity (`debug`, `info`, `warn`, `error`). | `info` | No | | `SERVER_PORT` | HTTP listen port. The liveness, readiness, and systemplane surfaces bind this same port. | `3000` | No | | `CORS_ALLOWED_ORIGINS` | Comma-separated allowed CORS origins. In production/staging, only `https://` origins are accepted and localhost origins are rejected. | `http://localhost:3000` | No | | `TRUSTED_PROXIES` | Comma-separated proxy IPs/CIDRs trusted to set the real client IP. | — | No | ## Authentication Lerian SPB delegates authorization — including the systemplane admin API — to Access Manager. See [BYOC configuration essentials](/en/reference/byoc-configuration#plugin-authentication). | Variable | Description | Default | Required | | --------------------- | ---------------------------------------------------------- | ----------------------- | ---------- | | `PLUGIN_AUTH_ENABLED` | Require Access Manager authentication on protected routes. | `true` | No | | `PLUGIN_AUTH_ADDRESS` | Access Manager service address. | `http://localhost:4000` | If enabled | ## PostgreSQL | Variable | Description | Default | Required | | ----------------------------- | ------------------------------------------------------------------- | ----------- | --------------------- | | `POSTGRES_HOST` | Primary PostgreSQL host. | `localhost` | Yes | | `POSTGRES_PORT` | Primary PostgreSQL port. | `5432` | No | | `POSTGRES_USER` | Database user. | — | Yes | | `POSTGRES_PASSWORD` | Database password. Sensitive — inject at deploy time; never commit. | — | In production/staging | | `POSTGRES_DB` | Database name. | — | Yes | | `POSTGRES_SSLMODE` | libpq TLS mode. Must be `verify-full` in production/staging. | `require` | No | | `POSTGRES_MAX_OPEN_CONNS` | Maximum open connections. | `25` | No | | `POSTGRES_MAX_IDLE_CONNS` | Maximum idle connections. | `10` | No | | `POSTGRES_CONN_MAX_LIFETIME` | Connection max lifetime (Go duration, e.g. `5m`). | `5m` | No | | `POSTGRES_CONN_MAX_IDLE_TIME` | Connection max idle time (Go duration). | `2m` | No | ### PostgreSQL replica Optional read replica. All fields fall back to the primary when unset. | Variable | Description | Default | Required | | --------------------------- | ------------------------------------------------------------------------------------ | ------- | -------- | | `POSTGRES_REPLICA_HOST` | Replica host. Unset = no replica. | — | No | | `POSTGRES_REPLICA_PORT` | Replica port. | — | No | | `POSTGRES_REPLICA_USER` | Replica user. | — | No | | `POSTGRES_REPLICA_PASSWORD` | Replica password. Sensitive. | — | No | | `POSTGRES_REPLICA_DB` | Replica database name. | — | No | | `POSTGRES_REPLICA_SSLMODE` | Replica TLS mode. Must be `verify-full` in production/staging when a replica is set. | — | No | ## Redis | Variable | Description | Default | Required | | -------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------- | --------------------- | | `REDIS_HOST` | Redis host. Must be explicit and non-local in production/staging. | `localhost` | In production/staging | | `REDIS_PORT` | Redis port. | `6379` | No | | `REDIS_PASSWORD` | Redis password. Sensitive. | — | In production/staging | | `REDIS_DB` | Redis database number. | `0` | No | | `REDIS_TLS_ENABLED` | Enable TLS to Redis. Must be `true` in production/staging. | `false` | In production/staging | | `REDIS_TLS_CA_CERT_BASE64` | Base64-encoded CA certificate for Redis TLS. Required when `REDIS_TLS_ENABLED=true` in production/staging. | — | If TLS enabled | | `REDIS_POOL_SIZE` | Connection pool size. | `20` | No | | `REDIS_MIN_IDLE_CONNS` | Minimum idle connections. | `5` | No | ## STR connectivity (RSFN) Lerian SPB exchanges STR messages with BACEN over the RSFN using an IBM MQ transport. These variables define the participant identity, the queue-manager connection, and the queues. | Variable | Description | Default | Required | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------- | | `STR_ISPB` | The participant's 8-digit ISPB. In production/staging it must be an explicit real ISPB, not `00000000`. | `00000000` | In production/staging | | `SILOC_ISPB` | The SILOC câmara's 8-digit ISPB, resolved server-side into SILOC-related settlement messages — never client-supplied. In production/staging it must be an explicit 8-digit câmara ISPB, not `00000000`. | `02992335` | No | | `STR_MQ_HOST` | Queue-manager host. | — | Yes (live STR) | | `STR_MQ_PORT` | Queue-manager listener port. | `1414` | No | | `STR_MQ_CHANNEL` | MQ server-connection channel. | `DEV.APP.SVRCONN` | No | | `STR_MQ_QUEUE_MGR` | Queue-manager name. | `QM1` | No | | `STR_MQ_USER` | MQ connection user. | `app` | No | | `STR_MQ_PASSWORD` | MQ connection password. Sensitive. | — | In production/staging | | `STR_MQ_TLS_ENABLED` | Enable mutual TLS on the MQ channel. Required for a real STR connection. | `false` | In production/staging | | `MQSSLKEYR` | Path to the GSKit key repository backing MQ TLS. The private key lives here, never in an environment variable. | — | If TLS enabled | | `STR_MQ_SEND_QUEUE` | Outbound request queue. Derived from `STR_ISPB` when unset. | Derived | No | | `STR_MQ_RESPONSE_QUEUE` | Response queue. Derived from `STR_ISPB` when unset. | Derived | No | | `STR_MQ_RECEIVE_QUEUE` | Inbound receive queue. Derived from `STR_ISPB` when unset. | Derived | No | | `MQ_HEARTBEAT_INTERVAL` | MQ heartbeat interval (Go duration). | `300s` | No | | `MQ_DISCONNECT_INTERVAL` | MQ disconnect interval (Go duration). | `6000s` | No | | `MQ_SEQ_WRAP` | Message-sequence wrap ceiling for the MQ channel. | `99999999` | No | | `MQ_ADOPTNEWMCA` | MQ `AdoptNewMCA` channel setting. | `ALL` | No | ### Circuit breaker Guards the STR transport against a failing queue manager. | Variable | Description | Default | Required | | ------------------------- | ------------------------------------------------------------- | ------- | -------- | | `CB_MAX_REQUESTS` | Requests allowed through while the breaker is half-open. | `3` | No | | `CB_INTERVAL` | Rolling window over which failures are counted (Go duration). | `30s` | No | | `CB_TIMEOUT` | Open-state recovery timeout (Go duration). | `10s` | No | | `CB_CONSECUTIVE_FAILURES` | Consecutive failures before the breaker trips. | `5` | No | | `CB_FAILURE_RATIO` | Failure ratio that trips the breaker (0–1). | `0.5` | No | | `CB_MIN_REQUESTS` | Minimum requests in a window before the breaker can trip. | `10` | No | ## Message signing and certificates Lerian SPB signs every STR message. Choose the key-custody backend with `SPB_SIGNER_KIND`. Production and staging reject the `file` backend and require a hardware-backed key (`pkcs11` or `kmip`). | Variable | Description | Default | Required | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------- | | `SPB_SIGNER_KIND` | Signing custody backend: `file`, `pkcs11`, or `kmip`. `file` is permitted only when `ENV_NAME=develop`; production and staging reject it. (`hsm` and `kms` are declared but not implemented.) | `file` | No | | `CERT_BASE_PATH` | Base directory for certificate material. | `/certs` | No | | `CERT_PATH` | Path to the signing certificate. | — | In production/staging | | `KEY_PATH` | Path to the signing private key. | — | In production/staging | | `PROCESS_CERT_PATH` | Path to the process/TLS certificate. | — | In production/staging | | `PROCESS_PRIVATE_KEY_PATH` | Path to the process/TLS private key. Sensitive. | — | No | | `BACEN_PUBLIC_CERT_PATH` | Path to BACEN's public certificate, used to verify inbound signatures and for SFN encryption. | — | In production/staging | | `CERT_READINESS_MIN_DAYS` | Minimum days-to-expiry before the certificate readiness check reports degraded. | `30` | No | ### PKCS#11 custody Required when `SPB_SIGNER_KIND=pkcs11`. | Variable | Description | Default | Required | | ------------------------ | ------------------------------------------------ | ------- | ----------- | | `SPB_PKCS11_MODULE_PATH` | Path to the PKCS#11 module library. | — | If `pkcs11` | | `SPB_PKCS11_TOKEN_LABEL` | PKCS#11 token label. | — | If `pkcs11` | | `SPB_PKCS11_PIN_FILE` | Path to a file holding the token PIN. Sensitive. | — | If `pkcs11` | | `SPB_PKCS11_KEY_LABEL` | Label of the signing key on the token. | — | If `pkcs11` | ### KMIP custody Required when `SPB_SIGNER_KIND=kmip` (KMIP-over-HTTP to a network HSM). | Variable | Description | Default | Required | | ------------------------------- | -------------------------------------------------------------- | ------- | --------- | | `SPB_KMIP_BASE_URL` | KMIP service base URL. | — | If `kmip` | | `SPB_KMIP_VHSM` | Virtual HSM identifier. | — | If `kmip` | | `SPB_KMIP_CRYPTO_USER` | KMIP crypto user. | — | If `kmip` | | `SPB_KMIP_CRYPTO_USER_TOKEN` | KMIP crypto-user token. Sensitive. | — | If `kmip` | | `SPB_KMIP_SIGN_PRIVATE_KEY_UID` | UID of the signing private key. | — | If `kmip` | | `SPB_KMIP_SIGN_PUBLIC_KEY_UID` | UID of the signing public key. | — | If `kmip` | | `SPB_KMIP_DECRYPT_KEY_UID` | UID of the decryption key used on the inbound decrypt path. | — | No | | `SPB_KMIP_DIGEST_INFO_PREFIX` | Prepend the ASN.1 DigestInfo prefix before the KMIP sign call. | `false` | No | ## Events, outbox, and RabbitMQ Lerian SPB records every message before dispatch and publishes lifecycle events through a transactional outbox to RabbitMQ. | Variable | Description | Default | Required | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------- | | `EMISSION_REQUIRED` | When `true`, bootstrap fails closed unless event emission is fully wired. | `false` | No | | `RABBITMQ_HOST` | RabbitMQ host. | — | Yes (events) | | `RABBITMQ_PORT` | RabbitMQ port. | `5672` | No | | `RABBITMQ_USER` | RabbitMQ user. Must not be the default `guest` in production/staging. | `guest` | In production/staging | | `RABBITMQ_PASSWORD` | RabbitMQ password. Sensitive. Must not be empty or default in production/staging. | `guest` | In production/staging | | `RABBITMQ_VHOST` | RabbitMQ virtual host. | `/` | No | | `RABBITMQ_STR_EXCHANGE` | Exchange name for STR lifecycle events. | `str.events` | No | | `RABBITMQ_TLS_ENABLED` | Enable TLS to RabbitMQ. Must be `true` in production/staging when a host is set. | `false` | In production/staging | | `OUTBOX_DISPATCH_INTERVAL` | Interval between outbox dispatch cycles (Go duration). | `2s` | No | | `OUTBOX_BATCH_SIZE` | Rows drained per dispatch cycle. | `50` | No | | `OUTBOX_MAX_PUBLISH_ATTEMPTS` | Maximum RabbitMQ publisher retry attempts for each publish call. Webhook delivery exhaustion is governed separately by `EVENT_DELIVERY_MAX_ATTEMPTS`. | `3` | No | | `OUTBOX_POSTGRES_MAX_OPEN_CONNS` | Max open connections for the outbox pool. | `10` | No | | `OUTBOX_POSTGRES_MAX_IDLE_CONNS` | Max idle connections for the outbox pool. | `5` | No | | `EVENT_DELIVERY_BATCH_SIZE` | Events delivered per cycle. | `25` | No | | `EVENT_DELIVERY_MAX_ATTEMPTS` | Delivery attempts before an event is parked. | `3` | No | | `EVENT_DELIVERY_RETRY_BACKOFF` | Base backoff between delivery retries (Go duration). | `30s` | No | | `DISPATCH_MAX_AUTO_ATTEMPTS` | Automatic dispatch attempts before manual intervention. | `8` | No | | `DISPATCH_RETRY_BACKOFF_CEILING` | Maximum backoff between dispatch retries (Go duration). | `30m` | No | | `LDL_ADVISORY_CONSUMER_ENABLED` | Enable the supervised consumer of SILOC OT-settlement advisory facts. It requires `STREAMING_BROKERS` and durable persistence when enabled, and refuses to start without them. The consumer topic and group are fixed in the service; neither is configurable through the environment. The broker connection comes from the shared `STREAMING_*` settings (see [Streaming and outbox](/en/reference/byoc-configuration#streaming-and-outbox)). Disabled, its readiness check reports `skipped`. | `false` | No | ## Approval (maker-checker) Optional value-band approval that gates STR emission. Off by default — an empty band table lets every emission proceed single-step. | Variable | Description | Default | Required | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `APPROVAL_ALCADA_BANDS` | Semicolon-separated `maxExclusive:requiredSignatures` bands (e.g. `10000.00:1;100000.00:2`). Empty keeps approval off. | — | No | | `APPROVAL_DEADLINE_WINDOW` | How long a parked emission may wait (Go duration, capped at 7 days). | `24h` | No | | `APPROVAL_EXPIRY_ENABLED` | Run the background sweep that expires overdue parked emissions. | `true` | No | | `APPROVAL_EXPIRY_SWEEP_INTERVAL` | Sweep interval (Go duration). | `1m` | No | ## Rate limiting and idempotency | Variable | Description | Default | Required | | ----------------------------- | -------------------------------------------------- | ------- | -------- | | `RATE_LIMIT_IP_MAX` | Max requests per IP per window. | `300` | No | | `RATE_LIMIT_IP_WINDOW` | Per-IP rate-limit window (Go duration). | `1m` | No | | `RATE_LIMIT_KEY_MAX` | Max requests per API key per window. | `100` | No | | `RATE_LIMIT_KEY_WINDOW` | Per-key rate-limit window (Go duration). | `1m` | No | | `IDEMPOTENCY_ENABLED` | Enable idempotency-key deduplication. | `true` | No | | `IDEMPOTENCY_DEFAULT_TTL_SEC` | How long (seconds) an idempotency key is retained. | `300` | No | ## Systemplane and runtime configuration Lerian SPB mounts the systemplane runtime-configuration admin API at `/v1/system` on the main port. PostgreSQL backs this plane, so operators can read and change hot-reloadable settings without a restart. See [Systemplane](/en/reference/systemplane/overview) for the API, namespaces, and permissions. | Variable | Description | Default | Required | | ---------------------------- | --------------------------------------------------------------------------------------------------- | --------------- | -------- | | `SYSTEMPLANE_LISTEN_CHANNEL` | PostgreSQL `LISTEN`/`NOTIFY` channel the runtime-config plane subscribes to for hot config changes. | Service default | No | ## Encryption and secrets | Variable | Description | Default | Required | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------- | --------------------- | | `STR_PAYLOAD_ENCRYPTION_KEY` | Base64-encoded AES-256 key for at-rest encryption of stored STR payloads. Sensitive. Required in production/staging. | — | In production/staging | | `WEBHOOK_SECRET_KEY` | Key used to encrypt stored webhook secrets. Sensitive. Required in production/staging. | — | In production/staging | | `INBOUND_ALLOW_CLEARTEXT_FALLBACK` | Allow the inbound path to accept unencrypted payloads. Leave `false` in production and staging. | `false` | No | Never commit or log the values of any variable marked *Sensitive* (passwords, tokens, PINs, and AES keys). Inject them at deploy time through your secret manager. ## Observability | Variable | Description | Default | Required | | ----------------------------- | ---------------------------------------------------------------------- | ----------------------- | --------------------- | | `ENABLE_TELEMETRY` | Enable OpenTelemetry tracing and metrics. | `false` | In production/staging | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint. Must use `https://` in production/staging. | `http://localhost:4318` | If telemetry enabled | | `OTEL_RESOURCE_SERVICE_NAME` | Service name attached to exported telemetry. | Service default | No | | `METRICS_PROMETHEUS_ENABLED` | Expose a dedicated Prometheus scrape endpoint. | `false` | No | | `METRICS_PROMETHEUS_ADDRESS` | Listen address for the Prometheus endpoint. Binds loopback by default. | `127.0.0.1:9090` | No | ## Development-only Keep these variables unset outside local development. They expand the attack surface, and production or staging rejects or forces them off. | Variable | Description | Default | Required | | --------------------------- | ----------------------------------------------------------------------------------- | ----------------------- | ---------------- | | `ENABLE_DEV_DEBUG_ROUTES` | Expose debug routes. Forbidden in production/staging. | `false` | No | | `DEV_DEBUG_TOKEN` | Token guarding the debug routes. Sensitive. Required when debug routes are enabled. | — | If debug enabled | | `SWAGGER_ENABLED` | Serve the OpenAPI spec and API docs UI. Forced off in production and staging. | `false` | No | | `STR_MQ_MOCK` | Route the STR transport to a mock instead of a live queue manager. | `false` | No | | `STR_MQ_MOCK_HOST` | Mock STR host. Required when `STR_MQ_MOCK=true`. | `http://localhost:8080` | If mock enabled | | `STR_MQ_MOCK_ALLOWED_HOSTS` | Allow-list of hosts the mock transport may reach. | — | No | | `ALLOW_MOCK_TRANSPORT` | Permit the mock transport to be selected at all. | `false` | No | ## Health and readiness Lerian SPB exposes `GET /health` (liveness) and `GET /readyz` (readiness) on the main HTTP port. See [Health and readiness](/en/reference/health-and-readiness) for the probe contract. # Integrating with Lerian SPB Source: https://docs.lerian.studio/en/rails/native/spb/spb-integration Integrating with Lerian SPB: STR operation event families, ledger integration, webhook delivery, and API conventions. Lerian SPB is event-driven. Event emission is not guaranteed for every operation or lifecycle change: some emitters are optional or best effort, and `EMISSION_REQUIRED` defaults to `false`. Set `EMISSION_REQUIRED=true` in a deployment whose downstream systems depend on these events; bootstrap then fails closed unless event emission is fully wired. Durable control-plane events are delivered to registered webhooks. `settlement.*` and `spb.ldl.*` are delivered on the streaming backbone only. Downstream systems read settlement state from `settlement.*` streaming events and do not poll. ## Event families *** | Family | Emitted on | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `str.operation.*` | Operation lifecycle — `accepted`, `received`, `returnRequested`, `cancelRequested` | | `str.readiness.changed` | The rail's readiness state changes | | `str.certificate.*` | Certificate lifecycle — `rotated`, `expiring`, `counterpartyChanged`, `activationRequested` | | `str.approval.*` | A queued approval emits `signed` or `denied`; when it reaches quorum and changes from `PENDING_APPROVAL` to `SUBMITTED`, it emits `quorumReached` exactly once | | `str.reconciliation.*` | A reconciliation case is `opened` or `resolved` | | `str.schedule.changed` | The operating-window grades change | | `str.message.*` | Message-level `received`, `sent`, `submitted`, `failed`, `rejected` | | `spb.ldl.*` | SILOC deposit-advisory and deposit-command facts, including `deposit-commanded`, `deposit-confirmed`, and `deposit-failed`. Delivered on the streaming backbone only, not on webhooks | | `settlement.*` | The final settlement position: `settled` once the inbound R-leg confirms an operation, `returned` when a confirmed return reverses a settled original, `failed` on a rejection or on the cancellation of a never-settled original. Delivered on the streaming backbone only, not on webhooks | ## Ledger integration *** Your ledger consumer must receive both the `str.operation.*` and `settlement.*` families on the streaming backbone. `str.operation.accepted` signals dispatch acceptance, not BACEN settlement, so use it to record a pending posting. Record the final position from the `settlement.*` facts on the streaming backbone: `settlement.settled` fires exactly once when the inbound R-leg moves the operation to `CONFIRMED`; `settlement.failed` fires when BACEN rejects the operation or a cancellation reverses a never-settled original; `settlement.returned` fires when a confirmed return reverses a settled original. A return follows the same pattern: `str.operation.returnRequested` signals the return's dispatch acceptance, and the parent posting reverses on `settlement.returned`. Lerian SPB holds **no** accounting position. The rail carries the message and its settlement state, and your ledger records the money. ## Inbound from BACEN *** Lerian SPB consumes the inbound STR settlement replies (R-legs) and the GEN-family notices. Both arrive from BACEN over the RSFN. A submitted operation stays open until its R-leg arrives. The R-leg then moves the operation to `CONFIRMED` or `REJECTED`. Lerian SPB projects BACEN's settlement fields verbatim. ## Webhooks *** Webhook consumers self-register on the canonical event constants. They negotiate payload shapes from a shared event catalog. Delivery is durable. You can retry a failed delivery manually. A dead-letter path handles the deliveries that exhaust their retries. ## API conventions *** * **Auth** is a bearer token. * **Writes are idempotent** through an idempotency key. A retried submit does not double-dispatch. * **Reads never leak raw protocol payloads.** You read a single message by its NUOp. The rail reconstructs an XML view on read. It does not separately retain the literal sent bytes. * **Unknown ids return a uniform not-found.** The response never reveals whether an operation exists that your institution does not own. # Operating Lerian SPB Source: https://docs.lerian.studio/en/rails/native/spb/spb-operations Operating Lerian SPB: BACEN STR operating windows (GEN0021), ICP-Brasil signing certificate rotation, contingency, and per-case settlement reconciliation. Lerian SPB works within two operational realities of the STR. The STR opens only inside BACEN's operating windows. Settlement is always asynchronous. The rail persists every message before it dispatches, so it never sends an unrecorded message. ## Operating windows *** BACEN announces the STR operating-window grades through GEN0021. The rail persists them, and you query the current grades with STR0001. A live schedule view lists the opening and closing times per reference date. The last announcement for a reference date wins. The rail does not impose a generic rejection of every submit outside an operating window. Its explicit schedule-related admission rule is RLPI0007: during the additional-aporte window, it rejects an aporte to a foreign Conta PI. Aportes to the institution's own Conta PI and resgates remain allowed. ## Certificate rotation *** The rail hot-reloads the ICP-Brasil signing certificate pair with no restart. A rotation swaps the in-memory signing pointer and writes audit evidence. An activation announces the new certificate to BACEN through GEN0006. Responses never expose the private key path or any key material. Certificate warnings surface in the readiness and summary views before the certificate expires. ## Contingency and durability *** * **Persist-before-send** guarantees the rail dispatches no unrecorded message. * A **dispatch sweep** retries stuck outbound work. * An operator retries a **failed event delivery** manually. A delivery that exhausts its retries moves to the `EXHAUSTED` state. ## Reconciliation *** Reconciliation opens one case per operation-level settlement discrepancy. An operator resolves a case by recording an action against it. The rail locks a terminal case. No one can reopen it or record a new action against it. ## Settlement invariants *** Two invariants hold throughout: * **A submit is dispatch, not confirmation.** `SUBMITTED` or `PENDING` means the rail accepted the dispatch. Only the inbound R-leg produces `CONFIRMED` or `REJECTED`. That R-leg carries BACEN's settlement fields — for example `numCtrlSTR` and `sitLancSTR` — projected verbatim. * **The rail is money-neutral.** It forwards declared amounts exactly and computes no position or balance. Your ledger consumer records any accounting position. # What is Lerian SPB? Source: https://docs.lerian.studio/en/rails/native/spb/what-is-lerian-spb Lerian SPB is your direct, non-intermediated BACEN STR connector for TED-family interbank transfers and reserve-account operations over the RSFN network. **Lerian SPB** is Lerian's own messaging connector to the **STR** (Sistema de Transferência de Reservas). The STR is the real-time gross-settlement core of the Brazilian payment system. Lerian SPB reaches the STR over the **RSFN**. The RSFN is the private national network that carries payment-system messages between participants and the Central Bank of Brazil (BACEN). Lerian SPB gives a participant institution a direct path to BACEN. The institution sends and settles interbank and large-value wire transfers — the TED family. It also runs reserve-account operations against BACEN's settlement system. No connectivity partner sits between the institution and BACEN. ## Who it serves *** Lerian SPB serves financial institutions that hold their own reserve and settlement account at BACEN. These institutions send TED-family transfers through the STR. They move liquidity between their reserve account and their liquidity account (Conta PI). They also query their reserve position directly through the STR. ## An operation-centric API *** You do not hand-pick a wire code. You declare the typed roles of the parties to an operation. You name who debits, who credits, and whether each side is an institution or a client. The platform then derives the correct STR message code for you. | Declared parties | STR code | Movement | | -------------------------- | -------- | ------------------------------------------------------------- | | Institution to institution | STR0004 | Interbank transfer between two account-holding participants | | Non-account-holders | STR0005 | Transfer involving parties that do not hold a reserve account | | Client to institution | STR0006 | A client sends to an institution | | Institution to client | STR0007 | An institution sends to a client | | Client to client | STR0008 | Transfer between two clients across institutions | ## Settlement is asynchronous *** The STR is a real-time gross-settlement system. Every operation settles individually against reserve balances. When you submit a transfer, the platform returns the status `ACCEPTED`. This status means the platform accepted your command for dispatch. It does not mean BACEN settled the transfer. BACEN confirms asynchronously through a settlement reply, called the R-leg. The reply moves the operation to `CONFIRMED` or `REJECTED`. It carries BACEN's settlement fields, for example `numCtrlSTR` and `sitLancSTR`, projected verbatim. ## Money-neutrality *** Lerian SPB is money-neutral by design. It forwards the amounts you declare exactly as declared and computes no balance or position of its own. The rail carries the message and settlement state; your ledger consumer records the money movement. ## Glossary *** | Term | Meaning | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | **STR** — Sistema de Transferência de Reservas | BACEN's real-time gross-settlement (RTGS) system, where each operation settles individually. | | **SPB** — Sistema de Pagamentos Brasileiro | The Brazilian payment system that the STR is the settlement core of. | | **RSFN** | The private national financial-system network that carries SPB messages between participants and BACEN. | | **TED** | An electronic wire transfer settled through the STR. | | **NUOp** | 23-digit BACEN operation identifier (ISPB + date + sequence) that correlates a submitted message with its reply. | | **ISPB** | 8-digit BACEN code identifying a payment-system participant institution. | | **Conta PI / reserve-settlement account** | The accounts an institution holds at BACEN for settlement and liquidity. | | **ICP-Brasil certificate** | The digital certificate used to sign outbound SPB messages. | | **Settlement confirmation (R-leg)** | The asynchronous BACEN reply that moves a submitted operation to `CONFIRMED` or `REJECTED`. | | **STR / LPI / GEN codes** | Official BACEN protocol identifiers for each STR and SPB message type (for example STR0004, LPI0001, GEN0006). | For how Lerian SPB sits alongside the other native rails and the partner interfaces, see [Native messaging and partner interfaces](/en/rails/native/native-messaging). # How Lerian SPI works Source: https://docs.lerian.studio/en/rails/native/spi/how-spi-works How Lerian SPI works: Pix onboarding, send and receive flows (pacs.008 and pacs.002), returns, DICT keys and claims, BR Code charges, Pix Automático, and MED. Lerian SPI exposes the Pix message surface as a set of typed operations. Most flows persist their work before dispatch; the return dispatcher only attempts to record an outbound `pacs.004` before submitting it, and a recording failure does not necessarily block dispatch. Operations return an accepted-but-not-settled state and reconcile against BACEN's asynchronous reply. ## Onboarding and readiness *** You register a participant by its ISPB. An indirect participant enters `PENDING`; the rail sends its registration request, and only BACEN's confirmation can make it active. The activation action only reactivates an already-suspended participant. The rail runs readiness in a required order. A passing connectivity test is the prerequisite for a submit. 1. **Upload** a Pix certificate — the public `.cer` only. The rail rejects an uploaded private key. 2. Confirm the rail reports **ready**. 3. Pass a **connectivity test**. 4. You can now submit payments. The same Core surface also suspends and offboards a participant over its lifecycle. ## Send a Pix *** You create a payment order. The platform builds the ISO 20022 credit-transfer message (`pacs.008`), persists the operation, and dispatches it. BACEN returns an asynchronous status callback (`pacs.002`). The rail validates and applies it, then moves the payment to `completed` or `rejected`. You read a payment back by its end-to-end ID, with its history and a per-operation timeline. ## Receive a Pix *** The ICOM consumer receives signed BACEN messages and passes them to the rail's authenticated internal ingress. For an inbound `pacs.008`, the rail validates the message and records the Pix as pending. The client then supplies the funding decision for that already-received Pix. An outbound payment cannot be funded as incoming money. Participants list the Pix they receive. ## Return (devolução) *** You initiate a return (`pacs.004`) only for a settled Pix that the rail received from BACEN, then read the return's status. The return debits the original recipient and credits the original payer. You cannot initiate a return for a Pix your client sent; a return of that Pix is issued by the counterparty and reaches the rail as an inbound message. A return is the MED money-movement path — the way funds flow back to a payer for a completed dispute or error. Two return surfaces exist, and the rail records which one created a return rather than inferring it later. A **full** return reverses the whole Pix and moves the parent payment out of `completed`. A **partial** return is keyed by the `devolucaoId` you choose and never moves the parent payment. Several partial returns can coexist for one Pix. Three gates apply to every return, on both surfaces: * **The parent must be an inbound Pix that settled.** Only a received payment that reached `completed` — or that already carries a return — can be returned. * **BACEN's return window.** A return must be requested within **90 days of the original Pix settlement**, and the rail measures the window from the settlement instant, never from creation or from the last update. A Pix with no recorded settlement instant is not blocked: the rail logs the gap and forwards the request. * **The sum ceiling.** The values of all returns of one Pix may not exceed that Pix's own value. The rail reads only that payment's amount and its own returns; it computes no position across payments. A return is born `EM_PROCESSAMENTO` and reaches `DEVOLVIDO` or `NAO_REALIZADO` only on BACEN's answer — a transport acceptance is not a conclusion. A return that fails releases the ceiling it was holding, and a failed full return returns the parent payment to `completed` without rearming its 90-day window. You request a return as `ORIGINAL` (the default when you send no nature) or `RETIRADA`, the Pix Saque and troco leg. The two MED natures — operational failure and founded fraud suspicion — are response-side only: they follow from the reason the rail puts on the `pacs.004`, and you never ask for them. ## DICT key lifecycle *** You manage Pix keys directly against the DICT directory: register, list, search, look up, update, and delete a key. You also batch-check whether a set of keys exists. The rail reads DICT key statistics and BACEN antifraud statistics, both per key and per person. ## DICT claims (reivindicação) *** A claim moves a Pix key between participants for portability or ownership. You initiate a claim against a participant, then move it through its lifecycle. The lifecycle covers acknowledge, confirm or reject, and complete or cancel, across the donor and claimer sides. When both `SCHEDULER_ENABLED=true` and `SCHEDULER_CLAIM_DEADLINE_ENABLED=true`, the rail registers periodic claim-deadline processing. It attempts to advance claims against their BACEN windows, but a claim can still require attention. ## BR Code and charges *** A dynamic QR resolves to a persisted charge, so you create the charge first, then generate the payload that resolves to it. A static QR is generated from static payment input; it neither requires nor points to a charge. * Create a charge: **Cob** (immediate), **CobV** (due-date, with interest and fine), or a **batch** of due-date charges. * Generate the **dynamic EMV QR** payload, which links to the charge by its txid or locator and resolves as a signed JWS. You also generate a static QR, decode a payload, validate it, and register a receiver profile (recebedor). ## Pix Automático (recurring) *** Pix Automático authorizes recurring and scheduled payments through the ISO 20022 recurring family: * **Create** a recurring authorization (recorrência) or a request for one. * **Request confirmation** of the mandate (`pain.009`), **cancel** it (`pain.011`), or **accept / reject** it (`pain.012`). * **Schedule** an instruction (`pain.013`) and **accept / reject** it (`pain.014`). * **Request cancellation** of a scheduled instruction (`camt.055`) and **resolve** a received cancellation (`camt.029`). * **Request a settlement retry** (retentativa) when a scheduled charge misses. ## MED disputes *** The MED (Mecanismo Especial de Devolução) surface handles fraud and error cases end to end: * **Open** a MED case, **analyze** it, then **resolve**, **close**, or **cancel** it with attached evidence. * File DICT **infraction reports**, **refund requests**, **fraud markers**, and **funds-recovery** requests, each tracked through its lifecycle graph. * Report internally-settled Pix through the MED 2.0 settlement report. ## Conta PI reporting *** You request an account report (`camt.060`), then read the balance (`camt.053`), statement (`camt.052`), or entry detail (`camt.054`) that BACEN returns. Synchronous count-only volumetria, rejected-payments, balance, and extract reports round out the reporting surface, each windowed to its reference period. # Environment variables Source: https://docs.lerian.studio/en/rails/native/spi/spi-environment-variables Deploy-time environment variables for Lerian SPI: BACEN SPI/DICT connectivity, ISO 20022 signing certificates, PII encryption, persistence, and streaming. Lerian SPI is Lerian's native messaging integration for Pix. It reaches BACEN's instant-payment system (SPI) and the DICT key directory over the RSFN. It ships four runtime binaries — `spi`, `dict`, `brcode`, and `core` — so its variable set is broad. The service reads every variable at startup. When systemplane is on — the default — it can override a subset at runtime without a restart. The rest of the variables need a restart to change. For the knobs that behave the same across every Lerian Go service — deployment posture, datastores, multi-tenancy, telemetry, streaming — see [BYOC configuration essentials](/en/reference/byoc-configuration). In the tables below, **Required** marks a variable you must set — globally, or under the condition named. `—` means no default. Most BACEN-facing variables carry a surface prefix: `BACEN_SPI_*` for the SPI settlement transport, `BACEN_DICT_*` for the DICT client, `BACEN_BRCODE_JOSE_*` for BR Code JOSE signing, and `BACEN_ICOM_*` for the inbound long-poll channel. ## Runtime and server | Variable | Description | Default | Required | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------- | | `ENV_NAME` | Runtime environment label. When unset, it resolves to `development`; an explicit `none` is invalid. Production arms stricter security gates. | `development` | No | | `LOG_LEVEL` | Log verbosity (`debug`, `info`, `warn`, `error`). | `info` | No | | `SERVER_ADDRESS` | Main HTTP listen address (`host:port` or `:port`). Liveness, readiness, and systemplane bind this port. | `:8080` | No | | `HTTP_BODY_LIMIT_BYTES` | Maximum request body size in bytes. | `1048576` | No | | `PUBLIC_BASE_URL` | Externally reachable base URL of the service, used to build absolute callback URLs and BR Code payload links. | — | In production or if BR Code JOSE is enabled | | `ACCESS_CONTROL_ALLOW_ORIGIN` | Allowed CORS origins. | `http://localhost:3000` | No | | `ACCESS_CONTROL_ALLOW_METHODS` | Allowed CORS methods. | `GET,POST,PUT,PATCH,DELETE,OPTIONS` | No | | `ACCESS_CONTROL_ALLOW_HEADERS` | Allowed CORS request headers. | (standard set) | No | | `TRUSTED_PROXIES` | Comma-separated proxy IPs/CIDRs trusted to set the real client IP. | — | No | | `SERVER_TLS_CERT_FILE` | Path to the server TLS certificate. Set together with the key file. | — | No | | `SERVER_TLS_KEY_FILE` | Path to the server TLS private key. Sensitive. | — | No | | `SERVER_TLS_CLIENT_CA_FILE` | CA file for verifying client certificates (mutual TLS). | — | No | | `TLS_TERMINATED_UPSTREAM` | Set `true` when TLS is terminated by a load balancer or reverse proxy. | `false` | No | ## Authentication Lerian SPI authorizes protected routes — including the systemplane admin API — through Access Manager. | Variable | Description | Default | Required | | ------------------------------ | -------------------------------------------------------------------------------------------------- | ------- | ------------- | | `AUTH_ENABLED` | Require Access Manager authentication on protected routes. | `false` | In production | | `PLUGIN_AUTH_ADDRESS` | Access Manager service address. | — | If enabled | | `AUTH_TRUST_UPSTREAM_METADATA` | Trust identity metadata forwarded by an upstream proxy. Leave `false` unless the proxy is trusted. | `false` | No | ## BACEN callback (inbound from SPI) | Variable | Description | Default | Required | | ------------------------------------ | --------------------------------------------------------------------------------- | ------- | --------------------------------------- | | `BACEN_CALLBACK_TRUSTED_PROXY_CIDRS` | CIDRs trusted as the source of BACEN callbacks. | — | No | | `BACEN_CALLBACK_MTLS_HEADER_SECRET` | Shared secret proving the upstream terminated the callback mutual-TLS. Sensitive. | — | If the primary ICOM consumer is enabled | ## SPI settlement transport (`BACEN_SPI_*`) Connection to BACEN's SPI settlement endpoint over the RSFN, with an optional secondary endpoint and the archive (ARQ) endpoint. | Variable | Description | Default | Required | | -------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------------------- | | `BACEN_SPI_ENDPOINT` | Primary SPI endpoint URL. | `http://localhost:9900` | Yes (live SPI) | | `BACEN_SPI_SECONDARY_ENDPOINT` | Secondary SPI endpoint URL for failover. | — | If the secondary ICOM consumer is enabled | | `BACEN_ARQ_ENDPOINT` | ARQ (archive/batch) endpoint URL. | — | No | | `BACEN_SPI_PARTICIPANT_ISPB` | The participant's ISPB for the SPI surface. | — | Yes (live SPI) | | `BACEN_SPI_ALLOWED_ENDPOINT_HOSTS` | Allow-list of hosts the SPI client may reach (SSRF guard). | — | No | | `BACEN_SPI_SECONDARY_ALLOWED_ENDPOINT_HOSTS` | Allow-list for the secondary endpoint. | — | No | | `BACEN_ARQ_ALLOWED_ENDPOINT_HOSTS` | Allow-list for the ARQ endpoint. | — | No | | `BACEN_SPI_TIMEOUT_SEC` | Request timeout (seconds). | `30` | No | | `BACEN_SPI_INITIATION_TIMEOUT_MS` | Payment-initiation timeout (milliseconds). | `150` | No | | `BACEN_SPI_INBOUND_CALLBACK_TIMEOUT_MS` | Inbound-callback processing timeout (milliseconds). | `250` | No | | `BACEN_SPI_RETRY_ATTEMPTS` | Retry attempts on transport failure. | `3` | No | | `BACEN_SPI_RETRY_INITIAL_BACKOFF_MS` | Initial retry backoff (milliseconds). | `500` | No | | `BACEN_SPI_RETRY_MAX_BACKOFF_MS` | Maximum retry backoff (milliseconds). | `5000` | No | | `BACEN_SPI_OUTBOUND_QUOTA_ENABLED` | Enable client-side outbound rate quota. | `false` | No | | `BACEN_SPI_OUTBOUND_QUOTA_LIMIT` | Sustained outbound quota (requests). | — | If quota enabled | | `BACEN_SPI_OUTBOUND_QUOTA_BURST` | Burst allowance above the quota. | — | If quota enabled | | `BACEN_SPI_CATALOGUE_VERSION` | BACEN SPI message-catalogue version. | `5.12.1` | No | | `BACEN_SPI_XSD_DIR` | Directory of the bundled BACEN XSD schemas for the catalogue. | Bundled path | No | | `BACEN_SPI_CATALOGUE_ROOT` | Override root for the message catalogue. | — | No | | `BACEN_SPI_INTERNAL_CALLBACK_SECRET` | Shared secret for the internal callback path. At least 32 characters when set. Sensitive. | — | In production or if the primary ICOM consumer is enabled | ### mutual TLS to BACEN The `BACEN_TLS_*` files back the mutual-TLS channel to BACEN. The SPI and DICT clients share them. | Variable | Description | Default | Required | | --------------------- | ---------------------------------------------------- | ------- | ---------- | | `BACEN_TLS_CERT_FILE` | Client certificate presented to BACEN. | — | Yes (live) | | `BACEN_TLS_KEY_FILE` | Client private key. Sensitive. | — | Yes (live) | | `BACEN_TLS_CA_FILE` | CA bundle used to verify BACEN's server certificate. | — | Yes (live) | ## Message signing and certificates Lerian SPI digitally signs its outbound messages. Pick a custody backend with `BACEN_SPI_SIGNER_KIND`. | Variable | Description | Default | Required | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------- | | `BACEN_SPI_SIGNER_KIND` | Signing custody backend: `file`, `pkcs11`, or `kmip`. Use a hardware backend in production. | `file` | No | | `BACEN_SPI_SIGNER_COMMON_NAME` | Expected common name of the signing certificate. | — | No | | `BACEN_SPI_SIGNING_CERT_FILE` | Path to the certificate bound to the signing key. Required for `pkcs11` or `kmip`; for `file`, it falls back to `BACEN_TLS_CERT_FILE` when unset. | `BACEN_TLS_CERT_FILE` (file) | If `pkcs11` or `kmip` | | `BACEN_SPI_INBOUND_SIGNER_COMMON_NAME` | Expected common name of the signer on inbound messages. | — | No | | `CERT_READINESS_MIN_DAYS` | Minimum days-to-expiry before the certificate readiness check reports degraded. | `14` | No | | `BACEN_SPI_PKCS11_MODULE_PATH` | PKCS#11 module library path. | — | If `pkcs11` | | `BACEN_SPI_PKCS11_TOKEN_LABEL` | PKCS#11 token label. | — | If `pkcs11` | | `BACEN_SPI_PKCS11_PIN_FILE` | Path to a file holding the token PIN. Sensitive. | — | If `pkcs11` | | `BACEN_SPI_PKCS11_KEY_LABEL` | Signing-key label on the token. | — | If `pkcs11` | | `BACEN_SPI_KMIP_BASE_URL` | KMIP service base URL. | — | If `kmip` | | `BACEN_SPI_KMIP_VHSM` | Virtual HSM identifier. | — | If `kmip` | | `BACEN_SPI_KMIP_CRYPTO_USER` | KMIP crypto user. | — | If `kmip` | | `BACEN_SPI_KMIP_CRYPTO_USER_TOKEN` | KMIP crypto-user token. Sensitive. | — | If `kmip` | | `BACEN_SPI_KMIP_SIGN_PRIVATE_KEY_UID` | UID of the signing private key. | — | If `kmip` | | `BACEN_SPI_KMIP_SIGN_PUBLIC_KEY_UID` | UID of the signing public key. | — | If `kmip` | | `BACEN_SPI_KMIP_DIGEST_INFO_PREFIX` | Prepend the ASN.1 DigestInfo prefix before the KMIP sign call. | `false` | No | ### Certificate validation (OCSP/CRL) | Variable | Description | Default | Required | | ---------------------------------- | ----------------------------------------------------- | ----------- | -------- | | `BACEN_SPI_OCSP_MODE` | Certificate revocation-check mode (e.g. `soft_fail`). | `soft_fail` | No | | `BACEN_SPI_OCSP_TIMEOUT_MS` | OCSP request timeout (milliseconds). | `3000` | No | | `BACEN_SPI_OCSP_CACHE_TTL_CAP_SEC` | Cap on cached OCSP response TTL (seconds). | `3600` | No | | `BACEN_SPI_OCSP_CRL_CACHE_TTL_SEC` | CRL cache TTL (seconds). | `3600` | No | ### Payload resolver These variables control how the service stores and references large SPI payloads. | Variable | Description | Default | Required | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ----------- | --------------------- | | `BACEN_SPI_PAYLOAD_RESOLVER_KIND` | Payload storage backend: `in_memory` or `postgres`. Production-like environments refuse `in_memory` — use a durable backend. | `in_memory` | No | | `BACEN_SPI_PAYLOAD_RESOLVER_IN_MEMORY_MAX_BYTES` | Max bytes held by the in-memory resolver. | `134217728` | No | | `BACEN_SPI_PAYLOAD_RESOLVER_TTL_SEC` | Retention TTL for resolved payloads (seconds). | `86400` | No | | `BACEN_SPI_PAYLOAD_RESOLVER_ENCRYPTION_KEY` | AES-256 key for encrypting stored payloads. Sensitive. | — | If kind is `postgres` | ## Inbound channel (`BACEN_ICOM_*`) Long-poll channel for messages BACEN pushes back to the participant. | Variable | Description | Default | Required | | --------------------------------------- | ----------------------------------------- | ------- | --------------------------------------- | | `BACEN_ICOM_BASE_URL` | Inbound channel base URL. | — | If the primary ICOM consumer is enabled | | `BACEN_ICOM_ISPB` | Participant ISPB for the inbound channel. | — | If the primary ICOM consumer is enabled | | `BACEN_ICOM_CONSUMER_ENABLED` | Enable the primary inbound consumer. | `false` | No | | `BACEN_ICOM_SECONDARY_CONSUMER_ENABLED` | Enable the secondary inbound consumer. | `false` | No | | `BACEN_ICOM_LONGPOLL_TIMEOUT_MS` | Long-poll wait timeout (milliseconds). | `90000` | No | With `BACEN_ICOM_CONSUMER_ENABLED=true`, startup also requires a configured persistence backend, `BACEN_SPI_INTERNAL_CALLBACK_SECRET`, and `BACEN_CALLBACK_MTLS_HEADER_SECRET`. With `BACEN_ICOM_SECONDARY_CONSUMER_ENABLED=true`, it additionally requires `BACEN_SPI_SECONDARY_ENDPOINT`. ## DICT client (`BACEN_DICT_*`) Client for BACEN's Pix key directory (DICT), including the anti-fraud (NP) endpoint. | Variable | Description | Default | Required | | -------------------------------------- | ---------------------------------------------------------------- | ----------------------- | ---------------------------------- | | `BACEN_DICT_ENDPOINT` | DICT endpoint URL. | `http://localhost:9900` | Yes (live DICT) | | `BACEN_DICT_PARTICIPANT_ISPB` | Participant ISPB for the DICT surface. | — | Yes (live DICT) | | `BACEN_DICT_ALLOWED_ENDPOINT_HOSTS` | Allow-list of hosts the DICT client may reach. | — | No | | `BACEN_DICT_TIMEOUT_SEC` | DICT request timeout (seconds). | `10` | No | | `BACEN_DICT_NP_ENDPOINT` | Anti-fraud (NP) endpoint URL. | — | No | | `BACEN_DICT_NP_ALLOWED_ENDPOINT_HOSTS` | Allow-list for the NP endpoint. | — | No | | `BACEN_DICT_SIGNER_KIND` | DICT signing custody backend: `file`, `pkcs11`, or `kmip`. | `file` | No | | `BACEN_DICT_SIGNING_CERT_FILE` | Path to the DICT signing certificate. | — | For the selected DICT custody path | | `BACEN_DICT_SIGNING_KEY_FILE` | Path to the DICT signing private key (file backend). Sensitive. | — | If `file` | | `BACEN_DICT_VERIFY_CERT_FILE` | Certificate used to verify DICT responses. | — | No | | `BACEN_DICT_PKCS11_MODULE_PATH` | PKCS#11 module path. | — | If `pkcs11` | | `BACEN_DICT_PKCS11_TOKEN_LABEL` | PKCS#11 token label. | — | If `pkcs11` | | `BACEN_DICT_PKCS11_PIN_FILE` | Path to the token PIN file. Sensitive. | — | If `pkcs11` | | `BACEN_DICT_PKCS11_KEY_LABEL` | Signing-key label on the token. | — | If `pkcs11` | | `BACEN_DICT_KMIP_BASE_URL` | KMIP service base URL. | — | If `kmip` | | `BACEN_DICT_KMIP_VHSM` | Virtual HSM identifier. | — | If `kmip` | | `BACEN_DICT_KMIP_CRYPTO_USER` | KMIP crypto user. | — | If `kmip` | | `BACEN_DICT_KMIP_CRYPTO_USER_TOKEN` | KMIP crypto-user token. Sensitive. | — | If `kmip` | | `BACEN_DICT_KMIP_SIGN_PRIVATE_KEY_UID` | UID of the signing private key. | — | If `kmip` | | `BACEN_DICT_KMIP_SIGN_PUBLIC_KEY_UID` | UID of the signing public key. | — | If `kmip` | | `BACEN_DICT_KMIP_DIGEST_INFO_PREFIX` | Prepend the ASN.1 DigestInfo prefix before the KMIP sign call. | `false` | No | | `BACEN_DICT_INTENT_ENCRYPTION_KEY` | AES-256 key for encrypting stored DICT claim intents. Sensitive. | — | In production | ## BR Code JOSE signing (`BACEN_BRCODE_JOSE_*`) Signs dynamic BR Code payloads (JWS). | Variable | Description | Default | Required | | --------------------------------------------- | --------------------------------------------------------------- | ------- | ---------------------------------------- | | `BACEN_BRCODE_JOSE_SIGNER_KIND` | JOSE signing custody backend: `file`, `pkcs11`, or `kmip`. | — | No | | `BACEN_BRCODE_JOSE_SIGNING_CERT_FILE` | Path to the JOSE signing certificate. | — | If BR Code JOSE is enabled | | `BACEN_BRCODE_JOSE_SIGNING_KEY_FILE` | Path to the JOSE signing private key (file backend). Sensitive. | — | If BR Code JOSE is enabled with `file` | | `BACEN_BRCODE_JOSE_KID` | JWS key identifier (`kid`) header value. | — | If BR Code JOSE is enabled | | `BACEN_BRCODE_JOSE_PKCS11_MODULE_PATH` | PKCS#11 module path. | — | If BR Code JOSE is enabled with `pkcs11` | | `BACEN_BRCODE_JOSE_PKCS11_TOKEN_LABEL` | PKCS#11 token label. | — | If BR Code JOSE is enabled with `pkcs11` | | `BACEN_BRCODE_JOSE_PKCS11_PIN_FILE` | Path to the token PIN file. Sensitive. | — | If BR Code JOSE is enabled with `pkcs11` | | `BACEN_BRCODE_JOSE_PKCS11_KEY_LABEL` | Signing-key label on the token. | — | If BR Code JOSE is enabled with `pkcs11` | | `BACEN_BRCODE_JOSE_KMIP_BASE_URL` | KMIP service base URL. | — | If BR Code JOSE is enabled with `kmip` | | `BACEN_BRCODE_JOSE_KMIP_VHSM` | Virtual HSM identifier. | — | If BR Code JOSE is enabled with `kmip` | | `BACEN_BRCODE_JOSE_KMIP_CRYPTO_USER` | KMIP crypto user. | — | If BR Code JOSE is enabled with `kmip` | | `BACEN_BRCODE_JOSE_KMIP_CRYPTO_USER_TOKEN` | KMIP crypto-user token. Sensitive. | — | If BR Code JOSE is enabled with `kmip` | | `BACEN_BRCODE_JOSE_KMIP_SIGN_PRIVATE_KEY_UID` | UID of the signing private key. | — | If BR Code JOSE is enabled with `kmip` | | `BACEN_BRCODE_JOSE_KMIP_SIGN_PUBLIC_KEY_UID` | UID of the signing public key. | — | If BR Code JOSE is enabled with `kmip` | | `BACEN_BRCODE_JOSE_KMIP_DIGEST_INFO_PREFIX` | Prepend the ASN.1 DigestInfo prefix before the KMIP sign call. | `false` | No | ## PII encryption and hashing Every variable below holds sensitive key or pepper material. It protects personal data at rest through encryption and blind indexing. Never commit or log a value. Inject it at deploy time through your secret manager. A pepper or key rotation requires a planned re-index or re-encryption. | Variable | Description | Default | Required | | ----------------------------------- | --------------------------------------------------- | ------- | ------------------------- | | `DICT_KEY_PII_ENCRYPTION_KEY` | AES key for DICT key-holder PII. Sensitive. | — | Protected DICT runtime | | `DICT_KEY_BLIND_INDEX_PEPPER` | Pepper for blind-indexing DICT key PII. Sensitive. | — | Protected DICT runtime | | `DICT_AUDIT_HASH_PEPPER` | Pepper for DICT audit-record hashing. Sensitive. | — | Protected runtime | | `AUDIT_HASH_PEPPER` | Legacy audit-hash pepper. Sensitive. | — | Protected runtime | | `SPI_RESPONSIBLES_ENCRYPTION_KEY` | AES key for responsible-party PII. Sensitive. | — | Protected SPI runtime | | `SPI_OPERATIONS_PII_ENCRYPTION_KEY` | AES key for operation PII. Sensitive. | — | Protected SPI runtime | | `SPI_OPERATIONS_BLIND_INDEX_PEPPER` | Pepper for blind-indexing operation PII. Sensitive. | — | Protected SPI runtime | | `BRCODE_PII_ENCRYPTION_KEY` | AES key for BR Code PII. Sensitive. | — | Protected BR Code runtime | | `BRCODE_PII_BLIND_INDEX_PEPPER` | Pepper for blind-indexing BR Code PII. Sensitive. | — | Protected BR Code runtime | ## PostgreSQL | Variable | Description | Default | Required | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | -------- | | `POSTGRES_HOST` | Primary PostgreSQL host. | `localhost` | Yes | | `POSTGRES_PORT` | Primary PostgreSQL port. | `5432` | No | | `POSTGRES_USER` | Database user. | `brspi` | No | | `POSTGRES_PASSWORD` | Database password. Sensitive. Use an explicit secret in production. | In-code development password | No | | `POSTGRES_DB` | Database name. | `brspi` | No | | `POSTGRES_SSLMODE` | libpq TLS mode. For remote production connections, use `verify-full` with a trusted CA; `require` encrypts the channel but does not verify the server identity. | `disable` | No | | `POSTGRES_MAX_OPEN_CONNS` | Maximum open connections. | `25` | No | | `POSTGRES_MAX_IDLE_CONNS` | Maximum idle connections. | `5` | No | | `POSTGRES_CONN_MAX_LIFETIME_MINS` | Connection max lifetime (minutes). | `30` | No | | `POSTGRES_CONN_MAX_IDLE_TIME_MINS` | Connection max idle time (minutes). | `5` | No | | `POSTGRES_CONNECT_TIMEOUT_SEC` | Connection timeout (seconds). | `10` | No | | `POSTGRES_REPLICA_HOST` | Optional read-replica host. Other replica fields fall back to the primary. | — | No | | `POSTGRES_REPLICA_PORT` | Replica port. | — | No | | `POSTGRES_REPLICA_USER` | Replica user. | — | No | | `POSTGRES_REPLICA_PASSWORD` | Replica password. Sensitive. | — | No | | `POSTGRES_REPLICA_DB` | Replica database name. | — | No | | `POSTGRES_REPLICA_SSLMODE` | Replica TLS mode. | — | No | ## Redis | Variable | Description | Default | Required | | ------------------------ | ----------------------------------------- | ---------------- | -------- | | `REDIS_HOST` | Redis host and port. | `localhost:6379` | No | | `REDIS_MASTER_NAME` | Sentinel master name (if using Sentinel). | — | No | | `REDIS_PASSWORD` | Redis password. Sensitive. | — | No | | `REDIS_DB` | Redis database number. | `0` | No | | `REDIS_PROTOCOL` | Redis protocol version (2 or 3). | `3` | No | | `REDIS_TLS` | Enable TLS to Redis. | `false` | No | | `REDIS_CA_CERT` | CA certificate for Redis TLS. | — | No | | `REDIS_POOL_SIZE` | Connection pool size. | `10` | No | | `REDIS_MIN_IDLE_CONNS` | Minimum idle connections. | `2` | No | | `REDIS_READ_TIMEOUT_MS` | Read timeout (milliseconds). | `3000` | No | | `REDIS_WRITE_TIMEOUT_MS` | Write timeout (milliseconds). | `3000` | No | | `REDIS_DIAL_TIMEOUT_MS` | Dial timeout (milliseconds). | `5000` | No | ## Outbox and streaming Lerian SPI publishes events through a transactional outbox and a lib-streaming producer, and consumes settlement events for BR Code. The streaming variables (`STREAMING_ENABLED`, `STREAMING_BROKERS`, `STREAMING_CLOUDEVENTS_SOURCE`, `STREAMING_COMPRESSION`, `STREAMING_REQUIRED_ACKS`, `STREAMING_EVENT_POLICIES`) follow the shared backbone — see [Streaming and outbox](/en/reference/byoc-configuration#streaming-and-outbox). Set `STREAMING_BROKERS` whenever `STREAMING_ENABLED=true`. | Variable | Description | Default | Required | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | -------- | | `OUTBOX_ENABLED` | Enable the transactional outbox dispatcher. | `true` | No | | `OUTBOX_DISPATCH_INTERVAL_MS` | Interval between dispatch cycles (milliseconds). | `2000` | No | | `OUTBOX_BATCH_SIZE` | Rows drained per dispatch cycle. | `50` | No | | `OUTBOX_MAX_DISPATCH_ATTEMPTS` | Dispatch attempts before an event is parked. | `10` | No | | `OUTBOX_PROCESSING_TIMEOUT_MS` | Per-batch processing timeout (milliseconds). | `600000` | No | | `OUTBOX_RETRY_WINDOW_MS` | Retry window before a stuck row is reclaimed (milliseconds). | `300000` | No | | `BRCODE_SETTLEMENT_CONSUMER_ENABLED` | Enable the BR Code settlement consumer. Requires `STREAMING_BROKERS`. | `false` | No | | `BRCODE_SETTLEMENT_CONSUMER_TOPIC` | Topic the BR Code settlement consumer reads. | `br-spi.spi.payment` | No | | `BRCODE_SETTLEMENT_CONSUMER_GROUP` | Consumer group for BR Code settlement. | `br-spi-brcode-settlement-consumer` | No | | `BRCODE_SETTLEMENT_CONSUMER_CLIENT_ID` | Client ID presented to the broker by the settlement consumer. | — | No | | `SPI_PARTICIPANT_CONSUMER_ENABLED` | Enable the Core-to-SPI participant-request consumer. It requires `STREAMING_BROKERS`, BACEN transport, and persistence when enabled. | `false` | No | | `SPI_PARTICIPANT_CONSUMER_TOPIC` | Topic carrying Core participant requests. | `br-spi.core.participant` | No | | `SPI_PARTICIPANT_CONSUMER_GROUP` | Durable consumer group for participant requests. | `br-spi-participant-request-consumer` | No | | `SPI_PARTICIPANT_CONSUMER_CLIENT_ID` | Client ID presented to the broker by the participant-request consumer. Falls back to the shared streaming client ID. | Shared client ID | No | ## Schedulers Background jobs, each independently gated. All default off. | Variable | Description | Default | Required | | --------------------------------------------------- | ------------------------------------------ | ------- | -------- | | `SCHEDULER_ENABLED` | Master switch for the scheduler subsystem. | `false` | No | | `SCHEDULER_MED_DEADLINE_ENABLED` | Run the MED (special return) deadline job. | `false` | No | | `SCHEDULER_QUOTA_RESET_ENABLED` | Run the outbound-quota reset job. | `false` | No | | `SCHEDULER_CLAIM_DEADLINE_ENABLED` | Run the DICT claim-deadline job. | `false` | No | | `SCHEDULER_DICT_RECONCILIATION_INCREMENTAL_ENABLED` | Run incremental DICT reconciliation. | `false` | No | | `SCHEDULER_DICT_RECONCILIATION_FULL_ENABLED` | Run full DICT reconciliation. | `false` | No | | `SCHEDULER_DICT_AUDIT_RETENTION_ENABLED` | Run the DICT audit-retention job. | `false` | No | | `SCHEDULER_INBOUND_DISCOVERY_ENABLED` | Run the inbound-message discovery job. | `false` | No | | `SCHEDULER_APPROVAL_EXPIRY_ENABLED` | Run the approval-expiry sweep. | `false` | No | | `SCHEDULER_PORTABILITY_DEADLINE_ENABLED` | Run the portability-deadline job. | `false` | No | ## Rate limiting, idempotency, and connectivity | Variable | Description | Default | Required | | ------------------------------------- | --------------------------------------------------------------------- | ------- | ------------------------------------ | | `RATE_LIMIT_ENABLED` | Enable request rate limiting. | `true` | No | | `RATE_LIMIT_MAX` | Maximum requests per window. | `100` | No | | `RATE_LIMIT_EXPIRY_SEC` | Rate-limit window (seconds). | `60` | No | | `IDEMPOTENCY_RETRY_WINDOW_SEC` | How long (seconds) an idempotency key is retained. | `86400` | No | | `IDEMPOTENCY_RESPONSE_ENCRYPTION_KEY` | AES-256 key for encrypting stored idempotency responses. Sensitive. | — | Protected SPI, DICT, or Core runtime | | `INFRA_CONNECT_TIMEOUT_SEC` | Connect timeout for infrastructure dependencies at startup (seconds). | `30` | No | ## Systemplane and runtime configuration | Variable | Description | Default | Required | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | `SYSTEMPLANE_ENABLED` | Enable the systemplane runtime-configuration admin API, mounted at `/v1/system` on the main port. When `false`, the service runs in env-only mode with no admin plane. **SPI defaults this on** — unlike most Lerian services, where systemplane is off by default. | `true` | No | See [Systemplane](/en/reference/systemplane/overview) for the API, namespaces, and required permissions. ## Observability | Variable | Description | Default | Required | | -------------------------------------- | ---------------------------------------------------------------------- | ---------------- | -------------------- | | `ENABLE_TELEMETRY` | Enable OpenTelemetry tracing and metrics. | `false` | No | | `TELEMETRY_REQUIRED` | Fail startup if telemetry cannot initialize. | `false` | No | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint. | `localhost:4317` | If telemetry enabled | | `OTEL_RESOURCE_SERVICE_NAME` | Service name attached to exported telemetry. | Service default | No | | `OTEL_RESOURCE_SERVICE_VERSION` | Service version label. | `1.0.0` | No | | `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT` | Deployment environment label. | `development` | No | | `OTEL_LIBRARY_NAME` | Instrumentation library name. | Service default | No | | `METRICS_PROMETHEUS_ENABLED` | Expose a dedicated Prometheus scrape endpoint. | `false` | No | | `METRICS_PROMETHEUS_ADDRESS` | Listen address for the Prometheus endpoint. Binds loopback by default. | `127.0.0.1:9090` | No | ## Documentation | Variable | Description | Default | Required | | ----------------- | ----------------------------------------------------------------- | ------- | -------- | | `SWAGGER_ENABLED` | Serve the OpenAPI spec and API docs UI. Forced off in production. | `false` | No | ## Health and readiness Lerian SPI exposes `GET /health` (liveness), `GET /readyz` (readiness), and `GET /version` on the main HTTP port. `/metrics` runs on its own listener when enabled. See [Health and readiness](/en/reference/health-and-readiness) for the probe contract. # Integrating with Lerian SPI Source: https://docs.lerian.studio/en/rails/native/spi/spi-integration Integrating with Lerian SPI: BACEN's ISO 20022 messages, streaming events, ledger integration, and API conventions for Pix. Lerian SPI is event-driven. Operations and settlement changes flow as domain events on the platform's streaming backbone. Downstream systems react to these changes without polling. The native rail has no client-facing webhook consumers. Its coordination is internal to the platform. ## Inbound from BACEN *** The ICOM consumer receives signed ISO 20022 messages from BACEN over the RSFN and passes them to the rail's authenticated internal ingress. A credit-transfer message (`pacs.008`) carries an inbound Pix. A status reply (`pacs.002`) reports a payment you sent. A return message (`pacs.004`) carries a devolução. The rail validates each inbound message before it applies it. An outbound payment stays open until its `pacs.002` arrives; that reply applies `COMPLETED` or `REJECTED`. An inbound `pacs.008` is recorded pending and is completed or refused by the authenticated client's funding decision. The rail records BACEN's fields verbatim. ## Event flow *** Each context on the rail publishes and consumes the events it owns: * The **BR Code** context publishes charge events and the recurring-family (Pix Automático) events. It consumes settlement events to close a charge after its Pix settles. * The **Core** context consumes participant-confirmation events and settlement completion and termination events. It keeps participant and operation state in step with BACEN. ## Ledger integration *** Lerian SPI holds no accounting position of its own. The rail emits settlement events on the streaming backbone, and your ledger consumer records the corresponding position. The rail relays each settled value verbatim. Each settlement event carries a stable `ce-id` that identifies the settlement; delivery is at-least-once, so your ledger consumer must deduplicate redeliveries by `ce-id`. ## API conventions *** * **Auth** follows the platform's standard bearer-token scheme. * **Payments carry an end-to-end ID.** You read a payment and its history back by E2EID. * **Returns are sub-resources.** You create and read a devolução under the inbound parent payment it reverses. A return must be requested within 90 days of that payment's settlement and may not push the sum of the Pix's returns past the Pix's own value. A return of a Pix your client sent is issued by the counterparty and arrives inbound. * **A return concludes on BACEN's answer.** A `pacs.004` the rail dispatched stays in progress until BACEN answers it. A duplicate request for a return already in progress is answered as in progress, and a colliding return identifier is answered as a conflict — never as a receipt. * **Inbound rail messages are signature-validated.** The rail does not apply a message that fails validation. # Operating Lerian SPI Source: https://docs.lerian.studio/en/rails/native/spi/spi-operations Operating Lerian SPI: around-the-clock Pix settlement, certificate rotation, stuck-operation recovery, and DICT reconciliation grains. Lerian SPI works around two operational realities of Pix. Pix settles in real time, 24 hours a day, and BACEN confirms every settlement asynchronously. The return dispatcher attempts to record an outbound `pacs.004` before submitting it, but a recording failure is best-effort and does not necessarily block dispatch. ## Around-the-clock settlement *** Pix settles individually and instantly, around the clock. There is no daily window to open or close. Two adjacent surfaces use their own reference periods instead. Pix Automático scheduling (agendamento) gives each instruction a requested execution time and a last date it can settle. Conta PI reports use a balance or extract reference period, with a date window of up to 92 days. ## Certificate rotation *** The rail manages the Pix signing certificate through its lifecycle. You upload the public `.cer`. You deactivate a certificate you retire. The rail rejects an uploaded private key, fail-closed. The rail stores only SHA-256 fingerprints and public metadata, so a response never exposes key material. The rail emits a days-until-expiry signal, so operators can act before a certificate expires. ## Contingency and recovery *** When a payment gets stuck, the rail exposes only the actions it can perform: * A **stuck-operations** view lists items that need attention. * A **recovery-eligibility** evaluation decides whether an operation is recoverable. * A **recovery action** or **manual resolution** clears an eligible operation. * A **manual-authorization approvals** workflow gates operations that need an explicit sign-off before they proceed. * An **evidence-export** job packages the record of an operation for audit or dispute. The listing declares, per row, which verbs the rail can actually perform on it. A verb the rail cannot perform on a row is absent from that row, so the panel never offers an action that will be refused. Every verb carries a mandatory operator justification, which is persisted with the outcome. A return that never received an answer has its own exit. A `pacs.004` with no status reply leaves the return waiting indefinitely, holding sum-ceiling headroom the Pix can never reuse and — on the full-return surface — blocking any further return of that Pix. An operator resolves such a return as not-realized, and the rail then frees the headroom it held. The rail refuses that resolution unless its own state proves the money never left. Releasing the ceiling of a return whose outcome is unknown is how a Pix gets returned twice, so an unproven return stays where it is and the release is not offered. ## Reconciliation *** Reconciliation capabilities operate at several grains. Automatic incremental and full DICT reconciliation run only when `SCHEDULER_ENABLED=true` and their respective feature gates are enabled, and a run can fail. Orphaned-key cleanup and claim-deadline processing are separate flows: * **DICT reconciliation** can run full or from BACEN's event list. * **Orphaned-key cleanup** can remove keys an interrupted flow left behind. * **Claim-deadline processing** can advance or close claims against their BACEN windows. ## Settlement invariants *** Three invariants hold throughout: * **A submit is dispatch, not confirmation.** An accepted submit confirms only that the rail accepted the dispatch. For an outbound payment, the inbound `pacs.002` status reply applies `COMPLETED` or `REJECTED` and projects BACEN's fields verbatim. An inbound `pacs.008` is recorded pending and is completed or refused by the authenticated client's funding decision. The same holds for a return: a `pacs.004` the rail accepted for dispatch is concluded by BACEN's answer, never by the send. * **A return's identity is recorded, not derived.** The rail stores which surface created a return, so a partial return keyed by a client-chosen identifier is never read as the full return of its Pix. * **The rail is money-neutral.** It forwards declared amounts exactly and computes no position or balance. Your ledger consumer records any accounting position. # What is Lerian SPI? Source: https://docs.lerian.studio/en/rails/native/spi/what-is-lerian-spi Lerian's direct, non-intermediated BACEN connector for Pix — instant payments, the DICT key directory, BR Code QR, Pix Automático, and MED disputes. **Lerian SPI** is Lerian's own messaging connector to the **SPI** (Sistema de Pagamentos Instantâneos). The SPI is the Central Bank of Brazil (BACEN) infrastructure that settles **Pix**, Brazil's instant-payment system, 24 hours a day. Lerian SPI reaches the SPI and its key directory (**DICT**) over the **RSFN**. The RSFN is the private national network for payment-system messages between participants and BACEN. Lerian SPI speaks **ISO 20022** and puts no connectivity partner between the institution and BACEN. Lerian SPI gives a participant institution a direct path to BACEN. The institution sends and receives Pix, manages Pix keys, issues QR charges, runs recurring authorizations, and handles disputes and refunds. The rail emits settlement facts; your ledger consumer records the corresponding positions. ## Who it serves *** Lerian SPI serves financial institutions that participate in Pix under their own **ISPB**. Each participant connects straight to BACEN, with no connectivity partner in the money path. The rail carries the participant's payments, Pix keys, charges, and disputes across every surface below. ## Four API surfaces *** Lerian SPI has four API surfaces. At runtime, it ships four binaries: `spi`, `dict`, `brcode`, and `core`. The surfaces speak the messaging contract of one part of the Pix ecosystem. | Surface | What it covers | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **Payments** | Sending and receiving Pix, payment status and history, returns (devoluções), and Pix Automático recurring authorizations | | **DICT** | The Pix key lifecycle, lookups and batch existence checks, antifraud statistics, and portability and ownership claims (reivindicações) | | **BR Code** | Pix charges — immediate (Cob), due-date (CobV), and batch — and the EMV QR payloads that resolve to them | | **Core & MED** | Participant registration and lifecycle, MED disputes and refunds, and Conta PI settlement reporting | ## Settlement is real-time and asynchronous *** The SPI settles each Pix individually, in real time, around the clock. When you submit a payment, the platform dispatches an ISO 20022 credit-transfer message (`pacs.008`). The platform records the payment as sent to BACEN, not yet settled. BACEN confirms settlement asynchronously through a status reply (`pacs.002`). The rail validates the reply and moves the payment to `COMPLETED` or `REJECTED`. Each payment carries an **end-to-end ID (E2EID)**. ## Money-neutrality *** Lerian SPI is money-neutral by design. It forwards the amounts you declare exactly as declared and computes **no** balance or position of its own. The rail carries the message and its settlement state; your ledger consumer records the money movement. ## Glossary *** | Term | Meaning | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | **Lerian SPI** | Lerian's native Pix Direto rail — a direct BACEN integration over the SPI. | | **Pix** | Brazil's instant-payment system, operated by BACEN and settling 24/7. | | **SPI** — Sistema de Pagamentos Instantâneos | BACEN's instant-payment settlement infrastructure. | | **DICT** — Diretório de Identificadores de Contas Transacionais | BACEN's Pix key directory. | | **Pix key** | An alias — phone, email, tax ID, or random EVP — that resolves to an account. | | **ISPB** | 8-digit code identifying a participant institution in the Brazilian payment system. | | **End-to-end ID (E2EID)** | The 32-character identifier tracking a single Pix payment across the rail. | | **BR Code** | Brazil's EMV QR standard (QRCPS-MPM) for Pix, static or dynamic. | | **Cob / CobV** | An immediate Pix charge (Cob) or a due-date charge with interest and fine (CobV). | | **Pix Automático** | Recurring and scheduled Pix authorizations — recorrência and agendamento. | | **MED** — Mecanismo Especial de Devolução | BACEN's dispute and refund mechanism for fraud or error. | | **Devolução** | A Pix return sent back to the payer against a settled payment. | | **Reivindicação (claim)** | A portability or ownership claim to move a Pix key between participants. | | **Conta PI** | A participant's instant-payment settlement account at BACEN, reported via camt.05x. | | **ISO 20022** | The messaging standard (pacs / pain / camt) BACEN uses for Pix over the RSFN. | | **Participant** | A financial institution connected to Pix, identified by its ISPB. | For how Lerian SPI sits alongside the other native rails and the partner interfaces, see [Native messaging and partner interfaces](/en/rails/native/native-messaging). # How Lerian STA works Source: https://docs.lerian.studio/en/rails/native/sta/how-sta-works How Lerian STA works: outbound submission and inbound file discovery to BACEN, protocol state machine, SHA-256 integrity, retry budgets, and operator actions. Lerian STA moves files in two directions. Each exchange is one **transfer** — a single file that moves between the institution and BACEN. A **protocol number** tracks the transfer through BACEN's protocol lifecycle. Each end verifies the **SHA-256**. ## Outbound submission *** The institution, or an upstream Lerian product, hands Lerian STA a file for BACEN. Lerian STA then obtains a **protocol number** from BACEN. It uploads the file's bytes. BACEN validates the bytes against the declared **SHA-256**. The transfer advances through BACEN's lifecycle: | State | Meaning | | --------------------------- | ------------------------------------------------- | | **Protocol generated** | BACEN has assigned the protocol number. | | **Transmitting** | Lerian STA is uploading the bytes. | | **Integrity check** | BACEN is validating the declared SHA-256. | | **Received** | BACEN has received the file. | | **Delivered** | BACEN has delivered the file to its destination. | | **In processing** | BACEN is processing the file. | | **Accepted** | BACEN has accepted the file — a terminal success. | | **Rejected / Inconsistent** | BACEN has refused the file — a terminal failure. | Upload retries within a **bounded budget**. Once Lerian STA exhausts the retry budget, it cancels the transfer. Each protocol also carries a bounded wall-clock **TTL** — 44 hours by default. Lerian STA must finish the upload before BACEN expires the protocol. ## Inbound discovery and download *** Lerian STA polls BACEN for available files, per active tenant, on a configured cadence. For each file, Lerian STA matches the file's **BACEN system code** against the tenant's **inbound source configurations**. It then downloads the bytes. It verifies the bytes against BACEN's reported **SHA-256**. It stores the file durably in object storage. A hash mismatch or a download failure retries within a bounded budget. Once Lerian STA exhausts the budget, it quarantines the transfer to a terminal failure — inconsistent or canceled. It does not deliver the file to its source product. ## Operator actions *** An operator can act only where the transfer's state and configuration permit it: * **Cancel** only when the canonical state machine permits a transition to `Canceled`; it is not allowed from every non-terminal state. * **Retry** an outbound transfer only when it is non-terminal, has upload budget remaining, and its protocol TTL has not expired. * **Mark urgent** only for an inbound transfer in `AvailableForDownload` whose source configuration has urgency enabled; it fails closed otherwise. Lerian STA also cancels a transfer when its BACEN protocol expires. # Environment variables Source: https://docs.lerian.studio/en/rails/native/sta/sta-environment-variables Deploy-time environment variables for Lerian STA: the BACEN environment target, S3-compatible object storage, envelope-encryption master keys, and trust store. Lerian STA is the Lerian-owned rail that transmits and receives files with the BACEN STA hub. You set these variables at deploy time. A service restart makes them take effect. Every Lerian Go service shares a universal backbone — server, datastores, multi-tenancy, telemetry, plugin authentication, and licensing. [BYOC configuration essentials](/en/reference/byoc-configuration) documents that backbone once. This page covers only the variables distinctive to Lerian STA. In the tables below, the **Default / Required** column shows the default value. A bold qualifier marks a variable you must set, such as **Required** or **Required in production**. `—` means no default. A variable flagged **Sensitive** carries credential or key material. Inject it from your secret manager at deploy time. Never commit a value. ## Service and runtime | Variable | Default / Required | Description | | --------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SERVER_ADDRESS` | — | Main HTTP listen address. Set it explicitly: the `:4028` `envDefault` tag is not materialized at runtime. The liveness, readiness, metrics, and version probes bind this same port. | | `SYSTEMPLANE_ENABLED` | `false` | Enable the [Systemplane](/en/reference/systemplane/overview) runtime-configuration admin API under the `/system` prefix on the main port. Off by default (environment-variable-only mode). | | `DEFAULT_TENANT_ID` | — | Optional UUID fallback used only for the streaming/outbox `ce-tenantid` when an event has no tenant ID; it is not injected into ordinary single-tenant request or transfer context. | | `BACEN_ENVIRONMENT` | `homologation` | Selects the upstream BACEN STA target for the connectivity probe: `homologation` (staging) or `production` (live). An unrecognized value falls back to `homologation` so a non-production stack cannot route traffic to the live BACEN service. | Lerian STA exposes `/health` (liveness) and `/readyz` (readiness) on the main port, plus `/version` and `/metrics`. When you enable multi-tenancy, it also exposes `GET /readyz/tenant/{id}`. See [Health and readiness](/en/reference/health-and-readiness) for the probe contract. ## Object storage Lerian STA persists transfer bytes and inbound downloads to an S3-compatible object store. Each subsystem configures its own connection through the AWS SDK, so the same wiring drives AWS S3, MinIO, or a local store. | Variable | Default / Required | Description | | -------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TRANSFER_OBJECT_STORAGE_BUCKET` | **Required in production** | Bucket holding both transfer directions, keyed by direction prefix. Production boot is refused if it is empty, regardless of whether inbound transfers are enabled. | | `TRANSFER_INBOUND_ENABLED` | `false` | Enable the inbound (BACEN-pulled) transfer pipeline. It does not change the production bucket requirement because outbound transfers also use the bucket. | | `TRANSFER_INBOUND_MAX_FILE_SIZE_BYTES` | `0` | A value of `0` means no ceiling on BACEN-advertised inbound file size. Set a positive value to skip larger files before any bytes are written. | | `AWS_REGION` | — | Region for M2M AWS Secrets Manager and, when `MASTER_KEY_KMS_REGION` is empty, the AWS KMS fallback. It does not configure object storage. | | `AWS_ACCESS_KEY_ID` | — | Access key for the object store. Sensitive. Leave unset when an attached IAM role supplies credentials. | | `AWS_SECRET_ACCESS_KEY` | — | Secret key for the object store. Sensitive. Leave unset when an attached IAM role supplies credentials. | ## Envelope encryption Lerian STA protects operator credentials with AES-GCM envelope encryption. The manager process always needs the master-key material. A missing or malformed value aborts boot. | Variable | Default / Required | Description | | ------------------------------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MASTER_KEY_PROVIDER` | `envvar` | Source of the envelope-encryption key: `envvar` (key read from `MASTER_KEYS`) or `aws-kms` (key wrapped by a cloud KMS). | | `MASTER_KEY_VERSION` | **Required** | Selects the currently active master-key version. Must match a version present in `MASTER_KEYS`. | | `MASTER_KEYS` | **Required** | Comma-separated `version:key` pairs. Sensitive. With `envvar`, each value is a 64-hex-character AES-256 key: `version:<64-hex-key>`. With `aws-kms`, each value is base64 KMS ciphertext: `version:`, decrypted in process. Source from your secret manager — a missing or malformed value aborts boot. | | `MASTER_KEY_KMS_KEY_ID` | **Required if `aws-kms`** | Identifier of the AWS KMS key used to decrypt the `MASTER_KEYS` ciphertext. With `aws-kms`, the ciphertext is decrypted in the STA process. | | `CREDENTIALS_RECOVERY_ON_BOOT` | `false` | Set explicitly to `true` to run the worker-side credential-recovery sweep once at boot for credentials left mid-rotation by a previous process. | ## Trust store Each tenant uploads the X.509 root certificates that Lerian STA uses to validate BACEN's server TLS certificate. STA authenticates to BACEN with HTTP Basic operator credentials and attaches no client certificate. The trust-store module is always on. Lerian STA persists certificate PEM bodies to the object store, so it needs the trust-store bucket connection. | Variable | Default / Required | Description | | --------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------- | | `TRUST_STORE_S3_BUCKET` | **Required** | Bucket holding the per-tenant trust-store certificate PEMs. Boot fails without it. | | `TRUST_STORE_S3_REGION` | `us-east-1` | Region for the trust-store bucket. | | `TRUST_STORE_S3_ENDPOINT` | — | Object-store endpoint. Leave empty to fall back to the AWS S3 default endpoint. | | `TRUST_STORE_S3_PATH_STYLE` | `false` | Use path-style addressing. Required for MinIO or a local store; leave `false` for AWS S3. | | `TRUST_STORE_MAX_CERT_SIZE_BYTES` | `65536` | Maximum accepted size of an uploaded certificate PEM. | | `TRUST_STORE_EXPIRING_SOON_DAYS` | `30` | Window, in days, at which a certificate is reported as expiring soon. Operator-tunable with hot reload. | | `TRUST_STORE_DEFAULT_PAGE_SIZE` | `25` | Default page size for the trust-store listing API. | | `TRUST_STORE_MAX_PAGE_SIZE` | `100` | Maximum page size for the trust-store listing API. | Lerian STA runs its audit pipeline in a dedicated worker. The publisher, consumer, and export generator support both single-tenant and multi-tenant modes; the export generator also needs its S3 bucket. The partition manager, cleanup, and verifier require multi-tenancy. Configure the enabled flags and required knobs explicitly through the `AUDIT_PUBLISHER_*`, `AUDIT_CONSUMER_*`, `AUDIT_PARTITION_*`, `AUDIT_CLEANUP_*`, `AUDIT_VERIFIER_*`, `AUDIT_EXPORT_GENERATOR_*`, and `AUDIT_EXPORT_RATE_LIMIT_*` families: `envDefault` tags are not generally materialized. # Integrating with Lerian STA Source: https://docs.lerian.studio/en/rails/native/sta/sta-integration Integrating with Lerian STA: transactional-outbox audit and business records, inbound claim-checks, and source-product routing. Lerian STA is event-first at its edges. A terminal transition can atomically create separate audit and business records through **transactional outboxes**, with a further post-commit durable-log sink. An outbox record reaches consumers only when the transfer's state change commits. Consumers are the **source-product** services that own the files Lerian STA delivers. ## Event channels *** Lerian STA publishes on two channels: * An **audit channel** carrying inbound-success and inbound-failure facts for observability and reconciliation. * A **signed business channel**, routed as `sta.{direction}.{sanitized-source-product}` with an `unrouted` fallback, for external product consumers. Its terminal event types are `sta.transfer.accepted`, `sta.transfer.rejected`, `sta.transfer.inconsistent`, `sta.transfer.canceled`, and `sta.transfer.download_finished`. Business events carry an enumerated **error code** only for non-success outcomes; successful `Accepted` and `DownloadFinished` events omit it. Inbound-success events carry the **claim-check**. ## The claim-check *** On a completed inbound download, Lerian STA emits its terminal **download-finished** event, routed by the file's source product. The event carries a **claim-check** — `object_key`, `sha256`, `size_bytes`, and `file_name` — but not the bytes themselves. `document_type` is included only when non-empty. The consuming domain product subscribes to its own key. It fetches the durable artefact by its object key and **re-verifies** the SHA-256. It then reconciles the result into the **client's ledger**. Lerian STA is the transport and integrity boundary. The consuming product owns the ledger posting. ## Ledger boundary *** Lerian STA does not write to the client's ledger itself. It delivers the file and the claim-check. The consuming product performs the ledger reconciliation — for example, **Lerian SISBAJUD** for judicial asset-order files. This keeps Lerian STA a pure transport-and-integrity layer, independent of any one file's business meaning. ## Integration conventions *** * **Commit-coupled events.** Every audit or business outbox record becomes available only after its transfer's state change commits. * **Claim-check, not payload.** Consumers receive a reference to the durable artefact, fetch it, and re-verify its SHA-256 before acting on it. * **Keyed subscription.** Clients choose their broker subscriptions. STA routes signed business events by direction and sanitized source product, with an `unrouted` fallback; it does not enforce that only one subscriber can receive a file. # Operating Lerian STA Source: https://docs.lerian.studio/en/rails/native/sta/sta-operations Operating Lerian STA: optional database-per-tenant isolation, BACEN operator credentials, three-phase password rotation, trust-store health, and protocol reconciliation. Lerian STA runs against the operational realities of BACEN's file hub. Three realities shape it: per-institution credentials expire, the trust chain must stay valid, and BACEN does not make the protocol-create step idempotent. Lerian STA manages each one under its selected tenancy mode. It reconciles at the grain of the BACEN protocol number. ## Multi-tenancy and pollers *** Lerian STA supports two tenancy modes. When multi-tenancy is enabled, each tenant's data lives in its own physically isolated database, with no shared tenant column on rows, and STA provisions per-tenant **pollers** on a roughly one-minute resync. In single-tenant mode, it uses the default database/pool with no tenant context. Outbound status polling **self-enqueues** with a state-keyed backoff. Traffic to BACEN respects per-credential ceilings. Command operations reach roughly 10 per minute per credential, a budget shared with rotation. Status polling scales dynamically up to roughly 100 per minute. ## Credentials *** Each credential is a BACEN **operator password**. Lerian STA identifies it by **institution code and operator ID**. Lerian STA stores it **encrypted** with envelope encryption, and never returns or logs it. Operators can **create**, **test connectivity for**, **rotate**, and **revoke** a credential. ## Password rotation *** Rotation is a **three-phase** protocol, so a mid-flight failure never loses authentication: 1. **Stage** the new password locally. 2. **Change** it at BACEN. 3. **Promote** it locally. A **recovery worker** resolves in-flight rotations. If BACEN accepts neither the old nor the new password, Lerian STA locks the credential in an error state for a manual credential reset. ## Password-expiry monitoring *** BACEN enforces periodic password changes. Lerian STA tracks each credential's **BACEN-reported expiry** through a read-through cache. It projects a **health band** — healthy, warning, critical, or expired — with the days remaining. An operator can then rotate the password before BACEN locks the operator out. ## Trust store *** Each tenant uploads the **X.509 root certificates** that Lerian STA uses to validate BACEN's server TLS certificate. STA authenticates to BACEN with HTTP Basic operator credentials and attaches no client certificate. Lerian STA parses the PEM and records each certificate's **fingerprint and validity window**. For each certificate it reports an expiry status, plus a **pool summary** — total, expiring-soon, and expired. The **expiring-soon window** defaults to **30 days**, and an operator can tune it with hot reload. The window warns operators to renew roots before the TLS trust chain breaks. ## Retention and protocol TTL *** * **Outbound protocol TTL** — each outbound protocol has a bounded wall-clock lifetime to finish the upload before BACEN expires it. The default is 44 hours. * **Inbound retention** — each inbound source configuration sets how long Lerian STA keeps its downloaded files. The default is 365 days, tunable from 1 to 3,650 days. ## Reconciliation and contingency *** Lerian STA reconciles at the grain of **one transfer per BACEN protocol number**. On discovery, the protocol number is the **idempotency key**, so Lerian STA never processes a file twice. BACEN offers **no idempotent protocol-create**. A second create would duplicate the file at BACEN. So Lerian STA can fail after it creates a protocol at BACEN but before it persists the protocol locally. If this happens, Lerian STA parks the transfer in a **local-only state**, and an operator must resolve it. Lerian STA never retries it silently. # What is Lerian STA? Source: https://docs.lerian.studio/en/rails/native/sta/what-is-lerian-sta Lerian STA is your client-owned integration to BACEN's STA file-exchange hub — two-way regulatory file transport with SHA-256 integrity and terminal events. **Lerian STA** is the client-owned messaging integration between a financial institution and **BACEN's STA (Sistema de Transferência de Arquivos)**, the Banco Central file-exchange hub. It moves regulatory and operational files in both directions. The institution submits files to BACEN (outbound), and BACEN makes files available for the institution to download (inbound). Lerian STA authenticates every exchange with a stored BACEN operator credential for the institution. It validates BACEN's TLS certificate against the tenant's trust store. It drives each transfer through BACEN's protocol state machine and verifies file integrity by **SHA-256**. It stores inbound artefacts durably and can publish terminal-state facts through separate audit and business outboxes for consumers. It supports a multi-tenant mode with physical database-per-tenant isolation, as well as single-tenant operation, and is API- and event-first. ## What Lerian STA does — and does not — do *** * It is the **transport and integrity boundary** between the institution and BACEN's file hub. It moves the bytes, authenticates the exchange, verifies the SHA-256, and records the outcome — it does not interpret the file's business content. * It **does not post to a ledger**. On a completed inbound download, Lerian STA publishes a terminal-state event that carries a claim-check. The consuming domain product (for example, [Lerian SISBAJUD](/en/rails/native/sisbajud/what-is-lerian-sisbajud)) fetches the artefact and reconciles it into **the client's ledger**. The ledger posting belongs to the consumer, not to Lerian STA. * It supports a **multi-tenant mode** with physical database-per-tenant isolation. In **single-tenant mode**, it uses the default database/pool with no tenant context. ## Who it serves *** Lerian STA serves financial institutions that operate on BACEN rails and need a governed, auditable path for file exchange with the Banco Central. It can run in single-tenant mode or, when multi-tenancy is enabled, serve multiple institutions in one deployment with isolated databases. Downstream, it serves the Lerian products that consume the files it delivers. Lerian STA routes each inbound file to a **source product** — the downstream product that owns the file's business meaning. That product subscribes to its own events and takes it from there. ## Glossary *** | Term | Meaning | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Lerian STA** | Lerian's client-owned integration to BACEN's file-exchange hub (Sistema de Transferência de Arquivos). | | **Transfer** | One inbound or outbound exchange of a single file between the institution and BACEN, tracked through its lifecycle. | | **Outbound transfer** | A file the institution submits to BACEN. | | **Inbound transfer** | A file BACEN makes available for the institution to download. | | **Protocol number** | The BACEN-assigned identifier for a transfer; the reconciliation and idempotency key. | | **Inbound source configuration** | A per-tenant policy binding a BACEN system code to a source product, with polling cadence, credential, retention, and maximum file size. | | **Trust store** | The tenant's uploaded X.509 root certificates that validate BACEN's TLS certificate. | | **Credential** | An encrypted BACEN operator password, identified by institution code and operator ID. | | **Password rotation** | The three-phase change of a BACEN password — stage, change at BACEN, promote locally. | | **Claim-check** | The `object_key`, `sha256`, `size_bytes`, and `file_name` carried on an inbound-success event so a consumer can fetch and re-verify the artefact; `document_type` is included only when non-empty. | | **Source product** | The downstream product a discovered inbound file is routed to (for example, Lerian SISBAJUD). | | **Lerian SISBAJUD** | The judicial asset-order product that consumes Lerian STA inbound-file events. | For how Lerian STA sits alongside the other native rails and the partner interfaces, see [Native messaging and partner interfaces](/en/rails/native/native-messaging). # Collections Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-collections Create Pix collections (cobranças) through the Pix Indirect Plugin via BTG — immediate COB, due-date COBV, lifecycle states, payment linking, and webhooks. A **collection** (cobrança) is a dynamic, single-use Pix charge that requests a specific payment. The Pix Indirect Plugin (BTG) supports two types. Both types use a dynamic QR Code: * **Immediate (COB)** — `cobrança imediata`: a short-lived charge for a fixed amount. Use it for checkout and one-time payments. * **Due-date (COBV)** — `cobrança com vencimento`: a boleto-like charge with a due date and optional fine, interest, discount, and abatement. Use it for bills, installments, and B2B invoicing. This guide covers the collection lifecycle and the payment flow. For QR Code generation details and field-by-field validation, see the [QR Codes guide](/en/rails/pix/btg/indirect-pix-qrcodes). # Lifecycle *** Both collection types share the same status model: | Status | Meaning | | --------------------- | -------------------------------------------- | | `ACTIVE` | Created and available for payment | | `COMPLETED` | Payment received — the collection is settled | | `REMOVED_BY_RECEIVER` | Deleted by the merchant (`DELETE`) | | `REMOVED_BY_PSP` | Expired after its validity window | A collection moves from **`ACTIVE`** to **`COMPLETED`** when the payer pays it. It moves to **`REMOVED_BY_PSP`** when it expires, or to **`REMOVED_BY_RECEIVER`** when the merchant deletes it. Each type has its own validity window: * **Immediate (COB):** the charge expires after `expirationSeconds`. * **Due-date (COBV):** the charge expires at `dueDate` plus `validAfterDue` days. After a collection expires or reaches `COMPLETED`, the payer can no longer pay it. The plugin also rejects any attempt to delete or update a `COMPLETED` collection (`PIX-0704`). # Key fields *** | Field | Required in | Notes | | ----------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------- | | `txId` | COB + COBV | Unique identifier across all collections; used to link the incoming payment | | `amount` | COB + COBV | Decimal with 2 places, greater than 0 | | `receiverKey` | COB + COBV | Pix key receiving the payment; must be owned by the account | | `expirationSeconds` | COB only | Validity window for immediate charges | | `dueDate` / `validAfterDue` | COBV only | Due date and post-due grace period | | `debtor` | COBV (optional in COB) | Name + CPF/CNPJ of the expected payer | | `amount.fine` / `interest` / `discount` / `abatement` | COBV only (optional) | COBV-only charge rules | For COBV, the final value depends on **payment timing**. Early payment applies discounts. On-time payment uses the original amount. Late payment adds fine and interest, minus any abatement. # Creating, retrieving, updating, deleting *** | Action | Immediate (COB) | Due-date (COBV) | | -------- | --------------------------------------- | ------------------------------------ | | Create | `POST /v1/collections/immediate` | `POST /v1/collections/duedate` | | List | `GET /v1/collections/immediate` | `GET /v1/collections/duedate` | | Retrieve | `GET /v1/collections/immediate/{id}` | `GET /v1/collections/duedate/{id}` | | Update | `PATCH /v1/collections/immediate/{id}` | `PATCH /v1/collections/duedate/{id}` | | Delete | `DELETE /v1/collections/immediate/{id}` | — | All requests require the `X-Account-Id` header. You can update or delete a collection only while it is `ACTIVE`. # Payment flow *** The payer settles a collection with an **incoming Pix (cash-in)** that carries the collection's `txId`: 1. The merchant creates a collection and presents its QR Code (or `txId`) to the payer. 2. The payer settles the charge. BTG notifies the plugin of the inbound cash-in. 3. The plugin **links the cash-in to the collection** by matching the payment's `txId` against the collection's `txId` **and** the receiver document. (`FindByTxID(txID, receiverDocument)`.) 4. On a match, the collection moves to `COMPLETED` and the plugin posts the cash-in to Midaz as a ledger transaction. 5. The plugin emits a **collection-paid webhook** to notify your system in real time. When you set a `debtor` on the collection, the charge records the expected payer's CPF/CNPJ. The plugin does not block a different payer at settlement. # Webhook event when paid *** After a payment settles a collection, the plugin queues an **outbound webhook**. The webhook describes the payment and the new `COMPLETED` status. The outbound webhook worker delivers it asynchronously. Configure the destination through the cash-in webhook URLs (`WEBHOOK_TRANSFER_CASHIN_URL`, with `WEBHOOK_DEFAULT_URL` as the fallback). For event types, payloads, retries, and URL resolution, see the [Webhooks guide](/en/rails/pix/btg/indirect-pix-webhooks). # Error cases *** | Case | Behavior | | | | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | - | -------------------- | ---------------------------------------------------------------------------- | | **Expired** | The payer can no longer pay the collection. A late inbound payment stays unlinked and follows the standard unmatched cash-in flow | | | | | **Amount mismatch** | For an immediate charge, a payment whose amount differs from the charge does not complete the collection (`PIX-0729`) | | **Duplicate `txId`** | Creation fails — a `txId` must be unique across all collections (`PIX-0701`) | | **Update/delete after completion** | The plugin rejects the request (`PIX-0704`) — a `COMPLETED` collection is immutable | | | | # Reference *** **Immediate (COB):** [Create](/en/reference/midaz/plugins/indirect-pix/create-an-immediate-charge) · [List](/en/reference/midaz/plugins/indirect-pix/list-immediate-charges) · [Retrieve](/en/reference/midaz/plugins/indirect-pix/retrieve-immediate-charge-details) · [Update](/en/reference/midaz/plugins/indirect-pix/update-an-immediate-charge) · [Delete](/en/reference/midaz/plugins/indirect-pix/delete-an-immediate-charge) **Due-date (COBV):** [Create](/en/reference/midaz/plugins/indirect-pix/create-a-dynamic-charge-with-due-date) · [List](/en/reference/midaz/plugins/indirect-pix/list-dynamic-charges-with-due-date) · [Retrieve](/en/reference/midaz/plugins/indirect-pix/retrieve-dynamic-charge-with-due-date-details) · [Update](/en/reference/midaz/plugins/indirect-pix/update-a-dynamic-charge-with-due-date) # Next steps *** * [QR Codes](/en/rails/pix/btg/indirect-pix-qrcodes) — QR Code types and the decoder * [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp) — Internal settlement when payer and payee share your ISPB * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Payment and status notifications # DICT Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-dict How the Pix Indirect Plugin (BTG) manages Pix keys in DICT — entries, key queries, portability and ownership claims, reconciliation (VSync), and fraud markers. **DICT** (Diretório de Identificadores de Contas Transacionais) is BACEN's directory that maps **Pix keys** to transactional accounts. The Pix Indirect Plugin (BTG) connects you to DICT through BTG. You register and resolve keys, transfer keys between institutions with claims, reconcile your local data with BACEN, and manage MED fraud markers. The DICT API spans several domains: entries and keys, claims, reconciliation, statistics, and the MED fraud tools. This guide covers key lifecycle, claims, reconciliation, statistics, and MED. Account-scoped operations require the `X-Account-Id` header. # Entries and keys *** An **entry** links a Pix key to one of your accounts. The plugin resolves the account and holder data from CRM. You create entries by key type instead of account details. **Supported key types:** | Type | Value source | | ------- | ---------------------------------------------------------- | | `CPF` | Provided in the request (must match the CRM holder's CPF) | | `CNPJ` | Provided in the request (must match the CRM holder's CNPJ) | | `EMAIL` | Provided in the request (valid email, ≤ 77 chars) | | `PHONE` | Provided in the request (`^\+[1-9][0-9]\d{1,14}$`) | | `EVP` | Random UUID generated by the system (do not send `key`) | ```json theme={null} POST /v1/dict/entries X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d { "keyType": "EMAIL", "key": "john.doe@example.com" } ``` Manage entries with **create / list / retrieve / update / delete** (`/v1/dict/entries`). Create and delete validate against active claims and check the key against the holder document. For example, a `CPF` key must match the holder's CPF. The plugin does not validate keys with Receita Federal and does not run MFA ownership checks. It assumes you completed those checks before you call it. See the [integration guide](/en/rails/pix/btg/indirect-pix-integration) for prerequisites. **Key queries** (`GET /v1/dict/keys/{key}`) resolve a key for payment. The response returns the current owner and account, so you can start a payment. The query requires the `X-End-To-End-Id` header for payment tracking. Use `POST /v1/dict/keys/check` to check existence in bulk. The plugin returns the data as it receives it from BTG. Mask sensitive fields before you display them on your side. **Reference:** [Create entry](/en/reference/midaz/plugins/indirect-pix/create-entry) · [List](/en/reference/midaz/plugins/indirect-pix/list-entries) · [Retrieve](/en/reference/midaz/plugins/indirect-pix/retrieve-an-entry) · [Update](/en/reference/midaz/plugins/indirect-pix/update-an-entry) · [Delete](/en/reference/midaz/plugins/indirect-pix/delete-an-entry) · [Retrieve a key](/en/reference/midaz/plugins/indirect-pix/retrieve-a-key) · [Check keys](/en/reference/midaz/plugins/indirect-pix/check-keys-existence) # Claims: portability and ownership *** A **claim** transfers a Pix key between institutions. There are two kinds: * **PORTABILITY** — moves a key to another bank **for the same holder**. Allowed for `CPF`, `CNPJ`, `PHONE`, and `EMAIL`. * **OWNERSHIP** — claims a key from a **different person**. Allowed only for `PHONE`. The two parties are the **donor** (the participant that currently holds the key) and the **claimer** (the participant that requests it). The plugin pulls the claimer's account data from CRM via `X-Account-Id`. BTG sets `claimerParticipant` and `donorParticipant` automatically. ## Claim lifecycle | Status | Meaning | | -------------------- | --------------------------------------------------- | | `OPEN` | Claim created; awaiting the donor's acknowledgment | | `WAITING_RESOLUTION` | Donor acknowledged; resolution period running (D+7) | | `CONFIRMED` | Donor confirmed; key is blocked pending completion | | `COMPLETED` | Key transfer finalized | | `CANCELLED` | Cancelled by donor or claimer | While a claim is active (`OPEN`, `WAITING_RESOLUTION`, or `CONFIRMED`), the claim locks the key. The plugin blocks new entries and deletes. During `OPEN` and `WAITING_RESOLUTION`, the donor can still update account data, and key queries return the donor's data. After `CONFIRMED`, queries return "key not found" until the claim reaches `COMPLETED` or `CANCELLED`. * **PORTABILITY** can complete immediately after confirmation. * **OWNERSHIP** adds a completion window. BTG returns `resolutionPeriodEnd` (D+7) and `completionPeriodEnd` on the claim. ## Claim operations | Operation | Role | Endpoint | | ----------- | ---------------- | --------------------------------------- | | Create | Claimer | `POST /v1/dict/claims` | | Acknowledge | Donor | `POST /v1/dict/claims/{id}/acknowledge` | | Confirm | Donor | `POST /v1/dict/claims/{id}/confirm` | | Complete | Claimer | `POST /v1/dict/claims/{id}/complete` | | Cancel | Donor or claimer | `POST /v1/dict/claims/{id}/cancel` | CLAIM outbound webhooks deliver claim status changes to your system. See the [Webhooks guide](/en/rails/pix/btg/indirect-pix-webhooks). **Reference:** [Create a claim](/en/reference/midaz/plugins/indirect-pix/create-a-claim) · [List](/en/reference/midaz/plugins/indirect-pix/list-claims) · [Retrieve](/en/reference/midaz/plugins/indirect-pix/retrieve-a-claim) · [Acknowledge](/en/reference/midaz/plugins/indirect-pix/acknowledge-a-claim) · [Confirm](/en/reference/midaz/plugins/indirect-pix/confirm-a-claim) · [Complete](/en/reference/midaz/plugins/indirect-pix/complete-a-claim) · [Cancel](/en/reference/midaz/plugins/indirect-pix/cancel-a-claim) # Reconciliation (VSync) *** **Reconciliation** keeps your local DICT data consistent with BACEN's authoritative records. It uses two concepts: * **CID** (Content Identifier) — a 256-bit HMAC-SHA256 hash of an entry's attributes (key type, key, owner, participant, branch, account, etc.). * **VSync** — a single checksum that XORs every CID of a key type. Because XOR is commutative, you compare your VSync to BTG/BACEN's to reveal whether your entries are in sync without exchanging every record. There are two paths: * **Manual / administrative API** — operators trigger on-demand checks, download CID files, and investigate inconsistencies. Use [Start full reconciliation](/en/reference/midaz/plugins/indirect-pix/start-full-reconciliation) and [List reconciliation jobs](/en/reference/midaz/plugins/indirect-pix/list-all-reconciliation-jobs). * **VSync worker** — an automated background process that periodically compares internal entries against DICT and reconciles drift without user intervention. Configure the reconciliation worker's time window and the DICT write-block window in the [integration guide](/en/rails/pix/btg/indirect-pix-integration#7-dict-reconciliation-vsync). During the write-block window the database temporarily blocks writes to prevent inconsistencies with BACEN. Anchor the window to `America/Sao_Paulo` and schedule it during low-traffic periods. # Statistics *** The **Statistics** domain exposes BACEN's Pix risk and usage aggregates. You can assess a counterparty **before** you settle a payment. Both endpoints query the provider directly and **do not store data locally**. Treat every call as a fresh, real-time lookup. Both endpoints require bearer authentication. | Endpoint | Scope | Use it to | | ------------------------------------------ | ---------------------- | ------------------------------------------------------- | | `GET /v1/dict/statistics/persons/{tax_id}` | A person (CPF or CNPJ) | Assess a payer/payee across all their keys and accounts | | `GET /v1/dict/statistics/keys/{key}` | A single Pix key | Assess a specific key, plus its current owner | ## Person statistics Pass the tax ID (CPF or CNPJ) in the path. The response aggregates settlement data, fraud markers, infraction reports, and entry information. It covers three rolling windows: **d90** (last 90 days), **m12** (last 12 months), and **m60** (last 60 months). ```json theme={null} GET /v1/dict/statistics/persons/12345678901 → 200 OK { "taxId": "12345678901", "statistics": { "settlements": { "d90": 42, "m12": 310, "m60": 1580 }, "fraudMarkers": { "d90": 0, "m12": 1 }, "infractionReports": { "d90": 0, "m12": 2 } } } ``` ## Key statistics Pass the Pix key in the path. The response returns two statistics in a single call: key-level and owner-level. Key-level statistics tie to the key as an entity, independent of its current owner. Owner-level statistics match the person statistics for the key's current owner. ```json theme={null} GET /v1/dict/statistics/keys/john.doe@example.com → 200 OK { "keyStatistics": { "settlements": { "d90": 12 }, "ownershipChanges": { "m12": 1 } }, "ownerStatistics": { "fraudMarkers": { "d90": 0 }, "infractionReports": { "m12": 0 } } } ``` Use key statistics when you pay a specific key. Use person statistics for a broader counterparty risk view. The plugin does not persist either result. Cache responsibly on your side if you reuse a result within a request flow. **Reference:** [Retrieve person statistics](/en/reference/midaz/plugins/indirect-pix/retrieve-person-statistics) · [Retrieve key statistics](/en/reference/midaz/plugins/indirect-pix/retrieve-key-statistics) # Fraud markers and MED 1.0 *** DICT also exposes BACEN's **MED** (Mecanismo Especial de Devolução) fraud-prevention tools. **Fraud markers** flag a key or account as associated with fraud. You can **create** and **cancel** them (fraud types: `APPLICATION_FRAUD`, `MULE_ACCOUNT`, `SCAMMER_ACCOUNT`, `OTHER`). Related **infraction reports** and **refund requests** drive the MED 1.0 dispute workflow. **Reference:** [Create a fraud marker](/en/reference/midaz/plugins/indirect-pix/create-a-fraud-marker) · [Cancel a fraud marker](/en/reference/midaz/plugins/indirect-pix/cancel-a-fraud-marker) · [List fraud markers](/en/reference/midaz/plugins/indirect-pix/list-fraud-markers) ## Infraction reports An **infraction report** tells the counterparty PSP that you dispute a transaction as fraud. You can open a report only within **90 days** of the transaction date. The report follows a **create → acknowledge → close/cancel** lifecycle: | Step | Role | Endpoint | | ----------- | ---------------------- | --------------------------------------------------- | | Create | Reporter (payer's PSP) | `POST /v1/dict/infraction-reports` | | Acknowledge | Counterparty PSP | `POST /v1/dict/infraction-reports/{id}/acknowledge` | | Close | Payee's/payer's PSP | `POST /v1/dict/infraction-reports/{id}/close` | | Cancel | Reporter | `POST /v1/dict/infraction-reports/{id}/cancel` | * **Create** — open the report against the disputed end-to-end ID, e.g. `reason: REFUND_REQUEST`, `situationType: SCAM`. * **Acknowledge** — the receiving PSP confirms receipt of the report. * **Close** — the responding PSP submits its analysis result (for example `TOTALLY_ACCEPTED`) within **7 days**. The payee's PSP closes `REFUND_REQUEST` infractions. The payer's PSP closes `REFUND_CANCELLED` infractions. After close, the report becomes immutable. * **Cancel** — the reporter withdraws a report it opened. ```json theme={null} POST /v1/dict/infraction-reports { "transactionId": "E12345678202411241430ABCDEFGHIJK", "reason": "REFUND_REQUEST", "situationType": "SCAM", "reportDetails": "Customer reported receiving a call from a fake bank employee" } ``` ## Refund requests A **refund request** is the MED 1.0 mechanism to ask the counterparty PSP to return disputed funds. It mirrors the same **create → acknowledge → close/cancel** lifecycle: | Step | Endpoint | | --------------- | -------------------------------------------------------------------- | | Create | `POST /v1/dict/refund-requests` | | Retrieve / List | `GET /v1/dict/refund-requests/{id}` · `GET /v1/dict/refund-requests` | | Close | `POST /v1/dict/refund-requests/{id}/close` | | Cancel | `POST /v1/dict/refund-requests/{id}/cancel` | **Close** records the analysis result and finalizes the request. **Cancel** withdraws a pending request. Outbound webhooks deliver status changes for both infraction reports and refund requests. See the [Webhooks guide](/en/rails/pix/btg/indirect-pix-webhooks). **Reference:** [Create an infraction report](/en/reference/midaz/plugins/indirect-pix/create-an-infraction-report) · [Acknowledge](/en/reference/midaz/plugins/indirect-pix/acknowledge-an-infraction-report) · [Close](/en/reference/midaz/plugins/indirect-pix/close-an-infraction-report) · [Cancel](/en/reference/midaz/plugins/indirect-pix/cancel-an-infraction-report) · [Create a refund request](/en/reference/midaz/plugins/indirect-pix/create-a-refund-request) For the fund-recovery flows, see [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations) and [MED 2.0 — Funds Recovery](/en/rails/pix/btg/indirect-pix-med-2-funds-recovery). # Next steps *** * [QR Codes](/en/rails/pix/btg/indirect-pix-qrcodes) — Generating QR Codes on registered keys * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Claim, infraction, and refund notifications * [Integration](/en/rails/pix/btg/indirect-pix-integration) — DICT reconciliation and worker configuration # Environment variables Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-environment-variables Configure the Pix Indirect via BTG rail — BTG mTLS, DB_* datastores, Midaz and CRM bindings, webhook endpoints, and worker components at deploy time. Pix Indirect via BTG reaches the Pix arrangement through BTG as the direct participant. It ships as several components: an API, plus reconciliation, schedule, and inbound and outbound webhook workers. DevOps sets the environment variables for each component at deploy time. To change a variable, you restart that component. This page covers the variables **distinctive to this rail**. For the multi-tenancy, streaming, telemetry, and authentication knobs shared across every Lerian Go service, see [BYOC configuration essentials](/en/reference/byoc-configuration). In the tables below, the **Default / Required** column shows the default value. A bold qualifier (for example **Required**) marks variables you must set. `—` means no default. `🔒` marks a **secret**. Inject each secret at deploy time from your secret store. Never commit a secret. This page lists variable names and behavior only. It prints no secret values. This rail does **not** mount the systemplane admin API. Its datastore variables use a `DB_*` prefix rather than the shared `POSTGRES_*` shape — see [Datastores](#datastores) below. ## Components and ports The API component listens on `SERVER_PORT` (default `4014`). `SERVER_ADDRESS` derives from it. The reconciliation, schedule, and webhook workers each bind a `WORKER_PORT` for their health probes. Each worker carries its own tuning knobs — batch sizes, polling intervals, concurrency, and circuit breakers — in its `.env.example` file. See [Default network ports](/en/reference/default-network-ports). ## BTG integration and mTLS These variables hold the credentials and mutual-TLS settings for the BTG connection. | Variable | Default / Required | Description | | -------------------------------- | ------------------ | --------------------------------------------------------------- | | `BTG_BASE_URL` | **Required** | Base URL of the BTG API. | | `BTG_CERTIFICATE_URL` | **Required** | Endpoint from which the client certificate is fetched for mTLS. | | `BTG_CLIENT_ID` | **Required** | OAuth client ID for the BTG API. | | `BTG_CLIENT_SECRET` | 🔒 **Required** | OAuth client secret for the BTG API. | | `BTG_WEBHOOK_SECRET` | 🔒 **Required** | Secret used to validate inbound BTG webhook signatures. | | `BTG_WEBHOOK_VALIDATION_ENABLED` | `true` | Verify BTG webhook signatures. Keep `true` in production. | | `MTLS_ENABLED` | `false` | Enable mutual TLS on the BTG connection. Enable in production. | | `MTLS_CERTIFICATE_TTL` | `24h` | How long a fetched client certificate is cached before refresh. | | `MTLS_HTTP_TIMEOUT` | `10s` | Timeout for the certificate-fetch HTTP call. | ## Datastores This rail uses a `DB_*` prefix for datastores, not the shared `POSTGRES_*` shape. It connects to a primary PostgreSQL, a separate read replica, MongoDB, and Redis. | Variable | Default / Required | Description | | ----------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DB_HOST` · `DB_PORT` | **Required** | Primary PostgreSQL host and port. | | `DB_USER` · `DB_PASSWORD` | 🔒 **Required** | Primary PostgreSQL credentials. | | `DB_NAME` | **Required** | Primary database name. | | `DB_SSL_MODE` | `disable` | libpq TLS mode. Use `require` or stronger in production. | | `DB_REPLICA_HOST` · `DB_REPLICA_PORT` · `DB_REPLICA_NAME` · `DB_REPLICA_USER` · `DB_REPLICA_PASSWORD` | 🔒 — | Optional read-replica connection (same shape as the primary). | | `REPLICATION_USER` · `REPLICATION_PASSWORD` | 🔒 — | Credentials for the streaming-replication role. | | `MONGO_URI` · `MONGO_HOST` · `MONGO_PORT` · `MONGO_USER` · `MONGO_PASSWORD` · `MONGO_NAME` | 🔒 **Required** | MongoDB connection. `MONGO_URI` sets the connection scheme (`mongodb` or `mongodb+srv`); `MONGO_HOST`, `MONGO_PORT`, `MONGO_USER`, `MONGO_PASSWORD`, and `MONGO_NAME` supply the host, credentials, and database. All combine into one connection string — they are not URI-versus-discrete alternatives. The password is a secret. | | `MONGO_TLS` | `false` | Enable TLS to MongoDB. | | `REDIS_HOST` · `REDIS_PORT` · `REDIS_PASSWORD` | 🔒 — | Redis/Valkey endpoint and auth password. | | `REDIS_TLS` · `REDIS_CA_CERT` | `false` · — | Enable TLS to Redis and supply a base64-encoded PEM CA. | ## Midaz, CRM, and Fees | Variable | Default / Required | Description | | --------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------- | | `MIDAZ_ONBOARDING_URL` · `MIDAZ_TRANSACTION_URL` | **Required** | Midaz onboarding and transaction service URLs. | | `MIDAZ_ORGANIZATION_ID` · `MIDAZ_LEDGER_ID` | **Required** | Midaz organization and ledger UUIDs for Pix bookings. | | `MIDAZ_CLIENT_ID` · `MIDAZ_CLIENT_SECRET` | 🔒 — | OAuth credentials for Midaz M2M. | | `PLUGIN_CRM_BASE_URL` · `PLUGIN_CRM_CLIENT_ID` · `PLUGIN_CRM_CLIENT_SECRET` | 🔒 — | CRM service URL and OAuth credentials. | | `FEE_SERVICE_URL` · `FEE_CLIENT_ID` · `FEE_CLIENT_SECRET` | 🔒 — | Fees Engine service URL and OAuth credentials. | | `FEE_SERVICE_TIMEOUT` | — | Fee request timeout. | | `CASHIN_FEE_CALCULATION_TYPE` | — | How cash-in fees are calculated. | ## Internal webhooks and scheduling The API and workers exchange events over an internal webhook channel. They also run recurring and scheduled Pix flows. | Variable | Default / Required | Description | | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------- | | `INTERNAL_WEBHOOK_SECRET` | 🔒 **Required** | HMAC secret shared between the API and workers for internal webhook signing. | | `INTERNAL_WEBHOOK_VALIDATION_ENABLED` | `true` | Verify internal webhook signatures. | | `INTERNAL_WEBHOOK_TIMESTAMP_TOLERANCE` | — | Allowed clock skew when validating internal webhook timestamps. | | `WEBHOOK_RECEIVER_MAX_PAYLOAD_SIZE` · `WEBHOOK_RECEIVER_MAX_RETRIES` | `10485760` (10 MB) · `3` | Inbound webhook payload cap and retry ceiling. | | `RECURRING_SCHEDULE_LEAD_DAYS` · `RECURRING_ANTISPAM_WINDOW` · `RECURRING_REPLY_BACKOFF` · `RECURRING_REPLY_MAX_ATTEMPTS` | `1` · `720h` · `150ms` · `3` | Recurring-Pix scheduling and reply-handling knobs. | | `SCHEDULE_DEFAULT_EXECUTE_HOUR_BRT` · `SCHEDULE_MAX_ATTEMPTS` · `SCHEDULE_MAX_FUTURE_DAYS` · `SCHEDULE_MIN_FUTURE_SECONDS` | `6` · `2` · `180` · `60` | Scheduled-payment execution window and bounds. | | `RECONCILIATION_INTERNAL_CIDR` | — | CIDR range trusted for internal reconciliation calls. | ## Pix and ledger scope | Variable | Default / Required | Description | | --------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------- | | `PIX_ISPB` | **Required** | The ISPB (participant code) this deployment operates under. | | `ENTRY_WRITE_BLOCK_START` · `ENTRY_WRITE_BLOCK_END` | — | Daily window during which ledger-entry writes are blocked. | | `ORGANIZATION_IDS` | **Required in production** | Comma-separated Midaz organization UUIDs in the licensing scope. | | `GOOGLE_APPLICATION_CREDENTIALS` | 🔒 — | Path to a Google service-account credentials file, when running against GCP-managed dependencies. | ## Health and readiness Each component exposes `GET /health` (liveness) and `GET /readyz` (readiness) on its port. See [Health and readiness](/en/reference/health-and-readiness) for the response shape and the startup and drain behavior. # Setting up the integration Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-integration A complete guide to configuring the Pix Indirect Plugin (BTG) — from licensing and authentication to Midaz, CRM, BTG connectivity, and worker setup. The Pix Indirect Plugin (BTG) connects to multiple Lerian services and external providers to process Pix payments. Setting it up involves configuring the plugin's connection to each service and preparing the data those services need to operate. The plugin runs as two main layers — an **Application** that exposes the Pix API and processes business logic, and a set of **Workers** that handle inbound webhooks from BTG, outbound event delivery to your system, and DICT reconciliation with BACEN. Both layers share the same foundational configuration (license, Midaz, CRM, BTG) but have their own service-specific settings. # Prerequisites *** Before you begin, make sure you have: * Your **ISPB** (Identificador do Sistema de Pagamentos Brasileiro) — the 8-digit identifier derived from your institution's CNPJ * Access to your **Midaz** and **CRM** instances (deployed and running) * Access to **BTG** provider services (BTG provides the credentials) ```bash theme={null} # Your institution's ISPB (8 digits) PIX_ISPB=12345678 ``` `PIX_ISPB` is also used to detect **intra-PSP (P2P) transfers**: when a transfer's destination ISPB matches this value, the plugin settles it internally instead of routing it to BTG, while still reporting it to BACEN. See [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp). # 1. License *** The plugin is an enterprise solution and requires a valid license to operate. Lerian provides the license key during onboarding. ```bash theme={null} # License key provided by Lerian LICENSE_KEY= # Authorized organization IDs (comma-separated) ORGANIZATION_IDS= ``` **Related documentation:** [Lerian's License](/en/reference/lerians-license) # 2. Access Manager (optional) *** Access Manager handles authentication for the plugin. When enabled, it validates all incoming requests before they reach the plugin's API. ```bash theme={null} # Require authentication for all plugin requests (true/false) PLUGIN_AUTH_ENABLED=false # Access Manager service URL PLUGIN_AUTH_ADDRESS= ``` When `PLUGIN_AUTH_ENABLED=true`, the plugin validates every request's `Authorization` header to ensure: * The token belongs to an authorized user or application * The token grants access to the requested API endpoint and method You only need to provide client credentials (`CLIENT_ID` / `CLIENT_SECRET`) for Midaz, CRM, and Fee services if those services also have Access Manager authentication enabled. **Related documentation:** [Access Manager](/en/platform/access-manager/access-manager) # 3. Midaz *** Midaz is the core ledger for all Pix transactions the plugin processes. The plugin posts every cash-in, cash-out, and refund operation to Midaz as a double-entry transaction. ## Connection *** ```bash theme={null} # Your organization ID in Midaz MIDAZ_ORGANIZATION_ID= # Your ledger ID in Midaz MIDAZ_LEDGER_ID= # Midaz Transaction module URL MIDAZ_TRANSACTION_URL= # Midaz Onboarding module URL MIDAZ_ONBOARDING_URL= # Midaz API credentials (required if Midaz has authentication enabled) MIDAZ_CLIENT_ID= MIDAZ_CLIENT_SECRET= ``` ## Asset requirements *** Configure an asset in your organization and ledger with these properties: | Property | Value | | -------- | ---------- | | Type | `currency` | | Code | `BRL` | Link all accounts to your ledger using the `BRL` asset. The plugin rejects operations on accounts that don't match this configuration. ## Account setup *** Before using the plugin, create an account in Midaz for each customer under your ISPB. Use the **Create an Account** endpoint to set up accounts. Each account must: * Belong to your configured organization and ledger * Use the `BRL` asset ## X-Account-Id header *** Many Pix operations require the `X-Account-Id` header to identify which account performs the action. This ID corresponds to the **Midaz ledger account ID**. When you call a plugin endpoint, the `X-Account-Id` value tells the plugin: * Which account to use for ledger operations * Which customer data to fetch from CRM * Which balance to validate and update ### How the plugin uses the account ID The plugin uses the account ID differently depending on the operation type: | Flow type | How the plugin uses the account ID | | ------------------------------------------------------- | --------------------------------------------------------- | | **Non-transactional** (e.g., creating keys or QR codes) | Fetches customer and bank account data from CRM | | **Transactional** (e.g., payments, refunds) | Fetches customer data for payment initiation | | | Validates account data for incoming payment authorization | | | Executes the transaction in the ledger | ## Account compliance *** The plugin does **not** enforce business-level account restrictions, such as blocked or suspended accounts. Your application must validate account status before it calls the plugin. To prevent Pix settlements on a specific account, block it directly in Midaz. The plugin receives a rejection when it attempts to post the transaction. **Related documentation:** * [Update an Account](/en/reference/midaz/update-an-account) * [Update a Balance](/en/reference/midaz/update-a-balance) # 4. CRM *** The CRM stores customer (holder) information and their associated bank accounts. Every Midaz account must have a corresponding **holder** and **alias account** in the CRM to perform Pix operations. The plugin queries CRM data to: * Register and validate Pix keys * Build payment messages for BACEN * Authorize incoming transactions * Process refund and dispute workflows ## Connection *** ```bash theme={null} # CRM base URL PLUGIN_CRM_BASE_URL= # CRM API credentials (required if CRM has authentication enabled) PLUGIN_CRM_CLIENT_ID= PLUGIN_CRM_CLIENT_SECRET= ``` ## Holders *** Holder data represents the customer who owns the account. Create each holder in the CRM before executing any Pix operation for that customer. ### Required fields | Field | Requirement | Example | | ---------- | ---------------------------------------------------------------------------------------------------- | -------------------- | | `name` | Maximum 120 characters | `Maria Silva Santos` | | `document` | For `NATURAL_PERSON`, a CPF with 11 digits; for `LEGAL_PERSON`, a CNPJ with 14 digits (numbers only) | `12345678900` | | `type` | Person type (enum values from CRM) | `NATURAL_PERSON` | ### Optional fields | Field | When to use | | ----------------------- | ----------------------------------------------- | | `legalPerson.tradeName` | When associating a trade name with the key data | | `addresses.primary` | Required for Due Date collection creation | **Related documentation:** [Create a Holder](/en/reference/midaz/crm/create-holder) ## Alias accounts *** Alias accounts link a Midaz account to its banking details. Each alias account must include the banking information that the Pix ecosystem requires for transaction processing. ### Required fields | Field | Requirement | Example | | ---------------------------- | ------------------------------ | -------------------------------------- | | `accountId` | Midaz account ID (UUID) | `3c90c3cc-0d44-4b50-8888-8dd25736052a` | | `bankingDetails.branch` | Exactly 4 digits | `0001` | | `bankingDetails.account` | 1 to 20 digits | `123456789` | | `bankingDetails.type` | Account type (see table below) | `CACC` | | `bankingDetails.openingDate` | YYYY-MM-DD format | `2024-01-15` | The `bankingDetails.branch` field must contain exactly **4 digits**. Pad with leading zeros if necessary. For example, if the branch number is `1`, register it as `0001`. The plugin can only validate the account in the CRM when the branch code follows this format. ### Supported account types | Code | Description | | ------ | ------------------- | | `CACC` | Checking account | | `SLRY` | Salary account | | `SVGS` | Savings account | | `TRAN` | Transaction account | **Related documentation:** [Create an Alias Account](/en/reference/midaz/crm/create-alias-account) # 5. BTG provider *** BTG is the direct participant that connects your institution to BACEN's Pix infrastructure. BTG provides the credentials directly when your institution signs up for indirect BACEN integration. ## Connection *** ```bash theme={null} # BTG API base URL BTG_BASE_URL= # BTG API credentials BTG_CLIENT_ID= BTG_CLIENT_SECRET= ``` ## mTLS (webhook security) *** Mutual TLS (mTLS) adds an extra layer of security by validating BTG's certificate on webhook requests. This ensures that incoming webhooks genuinely originate from BTG. ```bash theme={null} # Enable mTLS validation (true/false) # Use 'true' in production, 'false' for local development MTLS_ENABLED=false # How long to cache the certificate before refreshing # Format: Go duration (e.g., 24h, 12h, 1h) MTLS_CERTIFICATE_TTL=24h # BTG endpoint that provides the public certificate for signature validation BTG_CERTIFICATE_URL= # Timeout for certificate fetch requests # Format: Go duration (e.g., 10s, 30s) MTLS_HTTP_TIMEOUT=10s ``` Always enable mTLS in production environments. Only disable it during local development. # 6. Fee service (optional) *** Enable fee calculation to automatically charge and distribute fees on incoming payments. This is optional — if you don't configure it, the plugin processes transactions without fee calculation. ## Connection *** ```bash theme={null} # Fee calculation method (currently only 'segment' is supported) CASHIN_FEE_CALCULATION_TYPE= # Fee service URL FEE_SERVICE_URL= # Request timeout in milliseconds FEE_SERVICE_TIMEOUT=5000 # Fee service API credentials (required if the fee service has authentication enabled) FEE_CLIENT_ID= FEE_CLIENT_SECRET= ``` ## How it works *** When you set `CASHIN_FEE_CALCULATION_TYPE=segment`, the plugin: 1. **Retrieves the segment** associated with the receiving account 2. **Calculates applicable fees** before processing the transaction in Midaz 3. **Distributes the incoming payment** according to any package rules linked to that segment ## Setup steps *** 1. **Create segments in Midaz** — Define the account segments that determine fee rules 2. **Configure packages in Fees Engine** — Link each segment to its fee calculation rules 3. **Assign segments to accounts** — Create or update Midaz accounts with the appropriate segment ID **Related documentation:** * [Fees Engine - Guides](/en/midaz/fees/fees-engine-overview) * [Fees Engine - APIs](/en/reference/midaz/plugins/fees-engine/create-package) # 7. DICT reconciliation (VSync) *** VSync reconciles your local DICT data with BACEN by processing all key-related events from the day. This ensures your local state stays consistent with BACEN's authoritative records. ```bash theme={null} # Write block window (24-hour format, UTC) ENTRY_WRITE_BLOCK_START=23:30 ENTRY_WRITE_BLOCK_END=23:35 # Allowed network range for reconciliation services RECONCILIATION_INTERNAL_CIDR= ``` During reconciliation, the database temporarily blocks write operations to prevent data inconsistencies with BACEN. Plan the write block window during low-traffic periods. The CIDR range restricts which networks can trigger reconciliation. The plugin automatically rejects requests from outside this range. ## Redis cache configuration *** VSync uses Redis to cache BTG/DICT entries and CRM holders during reconciliation. In Helm deployments the default `REDIS_HOST` points to the built-in Valkey sidecar, so no extra setup is required for a standard install. ```bash theme={null} # Connection (always used) REDIS_HOST=-valkey:6379 REDIS_MASTER_NAME= REDIS_DB=0 REDIS_PROTOCOL=3 # Connection pool and retries (always used) REDIS_POOL_SIZE=10 REDIS_MIN_IDLE_CONNS=0 REDIS_READ_TIMEOUT=3 REDIS_WRITE_TIMEOUT=3 REDIS_DIAL_TIMEOUT=5 REDIS_POOL_TIMEOUT=2 REDIS_MAX_RETRIES=3 REDIS_MIN_RETRY_BACKOFF=8 REDIS_MAX_RETRY_BACKOFF=512 # Authentication (optional — only used if set) REDIS_PASSWORD= # TLS (optional — only used if REDIS_TLS=true) REDIS_TLS=false REDIS_CA_CERT= # GCP IAM authentication (optional — only used if REDIS_USE_GCP_IAM=true) REDIS_USE_GCP_IAM=false REDIS_SERVICE_ACCOUNT= GOOGLE_APPLICATION_CREDENTIALS= REDIS_TOKEN_LIFETIME=60 REDIS_TOKEN_REFRESH_DURATION=45 # VSync cache TTLs (always used) CACHE_BTG_ENTRY_TTL=30m CACHE_CRM_HOLDER_TTL=30m ``` In Helm deployments, the default `REDIS_HOST` points to the built-in Valkey sidecar (`-valkey:6379`) — no additional Redis setup is needed unless you connect to an external instance. ### Connection (always used) | Var | Type | Default | Description | | ------------------- | ----------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `REDIS_HOST` | string (host:port, CSV) | `-valkey:6379` (Helm chart) | Redis address(es). Default points to the built-in Valkey (`-valkey:6379`). Multiple addresses separated by comma define Sentinel/Cluster topology. | | `REDIS_MASTER_NAME` | string | `""` | Sentinel master name. Empty = standalone (direct connection). | | `REDIS_DB` | int | `0` | Redis logical database number. | | `REDIS_PROTOCOL` | int | `3` | RESP protocol version (2 or 3). | ### Connection pool and retries (always used) | Var | Type | Default | Description | | ------------------------- | ------------------ | ------- | ----------------------------------------------------------------------------------------- | | `REDIS_POOL_SIZE` | int | `10` | Maximum connections in the pool. | | `REDIS_MIN_IDLE_CONNS` | int | `0` | Minimum idle connections kept ready. | | `REDIS_READ_TIMEOUT` | int (seconds) | `3` | Read operation timeout. | | `REDIS_WRITE_TIMEOUT` | int (seconds) | `3` | Write operation timeout. | | `REDIS_DIAL_TIMEOUT` | int (seconds) | `5` | Timeout to establish the initial connection. | | `REDIS_POOL_TIMEOUT` | int (seconds) | `2` | Timeout waiting for a free connection from the pool. | | `REDIS_MAX_RETRIES` | int | `3` | Maximum retries for a failed command. | | `REDIS_MIN_RETRY_BACKOFF` | int (milliseconds) | `8` | Minimum delay between retries. | | `REDIS_MAX_RETRY_BACKOFF` | int (seconds) | `512` | Maximum delay between retries. Note: stored in seconds in code (different unit from min). | ### Authentication (optional — only used if set) | Var | Type | Default | Description | | ---------------- | --------------- | ------- | --------------------------------------------------- | | `REDIS_PASSWORD` | string (secret) | `""` | Redis password. Empty = no password authentication. | ### TLS (optional — only used if `REDIS_TLS=true`) | Var | Type | Default | Description | | --------------- | ------------------- | ------- | --------------------------------------------------------------------------- | | `REDIS_TLS` | bool | `false` | Enables TLS. Required `true` in `DEPLOYMENT_MODE=saas` (validated at boot). | | `REDIS_CA_CERT` | string (PEM base64) | `""` | CA certificate (base64) to validate the server when TLS is active. | ### GCP IAM authentication (optional — only used if `REDIS_USE_GCP_IAM=true`) | Var | Type | Default | Description | | -------------------------------- | ------------- | ------- | ------------------------------------------------------------------------------------ | | `REDIS_USE_GCP_IAM` | bool | `false` | Enables GCP IAM authentication (Memorystore) instead of password. | | `REDIS_SERVICE_ACCOUNT` | string | `""` | GCP service account that generates the access token. | | `GOOGLE_APPLICATION_CREDENTIALS` | string | `""` | GCP credentials path (read via CredentialsBase64FromEnvValue) to generate the token. | | `REDIS_TOKEN_LIFETIME` | int (minutes) | `60` | Lifetime of the generated IAM token. | | `REDIS_TOKEN_REFRESH_DURATION` | int (minutes) | `45` | Token refresh interval (must be less than lifetime). | ### VSync cache TTLs (always used) | Var | Type | Default | Description | | ---------------------- | ----------- | ------- | --------------------------------------------------------------------------- | | `CACHE_BTG_ENTRY_TTL` | Go duration | `30m` | Cache TTL for BTG/DICT entries in Redis. Negative value reverts to default. | | `CACHE_CRM_HOLDER_TTL` | Go duration | `30m` | Cache TTL for CRM holders in Redis. Negative value reverts to default. | # 8. Internal webhook security *** The plugin uses an internal communication channel between the Worker and Application services. **HMAC-SHA256 signatures** secure this channel to prevent tampering and replay attacks. ```bash theme={null} # Shared secret for signing internal requests (Worker -> Application) # Must be at least 32 characters # Generate one with: openssl rand -base64 32 INTERNAL_WEBHOOK_SECRET= # Validate signatures on incoming internal webhooks (true/false) # Always use 'true' in production INTERNAL_WEBHOOK_VALIDATION_ENABLED=true # Maximum age (in seconds) for request timestamps before rejection # Default: 300 (5 minutes) INTERNAL_WEBHOOK_TIMESTAMP_TOLERANCE=300 ``` Use the exact same `INTERNAL_WEBHOOK_SECRET` value in both the Application and Worker services. A mismatch causes the plugin to reject all internal webhooks. # 9. Worker layers *** The plugin operates with three worker layers, each handling a different part of the Pix lifecycle. All workers run as separate services alongside the main application. ## Inbound worker *** The inbound worker receives webhook notifications from BTG and forwards them to your application for processing. ```bash theme={null} # URL where the application receives internal webhooks WEBHOOK_INBOUND_BASE_URL= # Shared secret for signing requests (must match the application's INTERNAL_WEBHOOK_SECRET) INTERNAL_WEBHOOK_SECRET= ``` ## Outbound worker *** The outbound worker sends event notifications from the plugin to your application via webhooks. This is how your system stays informed about Pix events (transfers, refunds, claims, disputes). ### URL resolution priority The plugin resolves webhook URLs in this order, using the first match: 1. **Entity URL** — A URL specific to the event type (e.g., `WEBHOOK_DICT_CLAIM_URL`) 2. **Flow URL** — A URL for the broader category (e.g., `WEBHOOK_DICT_URL`) 3. **Default URL** — The fallback URL (`WEBHOOK_DEFAULT_URL`) ```bash theme={null} # Default fallback URL WEBHOOK_DEFAULT_URL= # DICT-related events WEBHOOK_DICT_URL= WEBHOOK_DICT_CLAIM_URL= WEBHOOK_DICT_INFRACTION_REPORT_URL= WEBHOOK_DICT_REFUND_URL= # MED 2.0 Funds Recovery events (DICT flow) # These route under the DICT flow and fall back to WEBHOOK_DICT_URL, # then WEBHOOK_DEFAULT_URL, if no entity-specific URL is set. WEBHOOK_DICT_FUNDS_RECOVERY_URL= WEBHOOK_DICT_FUNDS_RECOVERY_EVENT_URL= # Refund events WEBHOOK_REFUND_CASHIN_URL= WEBHOOK_REFUND_CASHOUT_URL= # Transfer events WEBHOOK_TRANSFER_CASHIN_URL= WEBHOOK_TRANSFER_CASHOUT_URL= ``` You can start with just `WEBHOOK_DEFAULT_URL` to receive all events at a single endpoint, then gradually split into entity-specific URLs as your system evolves. For a deep dive into webhook event types, payloads, retry behavior, and best practices, see the [Webhooks guide](/en/rails/pix/btg/indirect-pix-webhooks). ## Reconciliation worker *** The reconciliation worker needs the same credentials as the Application layer for these services: * **CRM** — URL and credentials * **BTG** — URL and credentials * **Midaz** — Organization ID See the corresponding sections above for details on each configuration. You can restrict when the worker operates by configuring a specific time window. This is especially useful for scheduling reconciliation tasks during off-peak hours. The plugin supports time windows that span midnight. ```bash theme={null} # Start time in HH:MM format (24-hour, BRT). Example: "23:00" for 11 PM BRT RECONCILIATION_START_TIME= # End time in HH:MM format (24-hour, BRT). Example: "05:00" for 5 AM BRT RECONCILIATION_END_TIME= ``` If you leave both fields empty, the worker runs without time restrictions — 24/7. The worker always interprets the reconciliation window in the **`America/Sao_Paulo`** (BRT) timezone, whatever the container clock or the `TZ` variable. The worker embeds the IANA timezone data. The window stays aligned with the BACEN DICT write-block, even on minimal or distroless images. You do not need to set `TZ`. Indirect Pix cashout and refund endpoints support idempotency through the `X-Idempotency` request header, with configurable TTL via `X-TTL`. For retry strategies and implementation details, see [Retries and idempotency](/en/reference/retries-idempotency). # 10. Observability (OpenTelemetry) *** The plugin ships with full **OpenTelemetry** instrumentation (traces, metrics, and logs) across the Application and Worker layers. Every Pix flow is traced end to end — **transfers** (cash-in and cash-out), **refunds**, inbound and outbound **webhooks**, DICT **reconciliation**, and **intra-PSP** settlement — so you can follow a single payment across the plugin, Midaz, CRM, and BTG calls. Telemetry is disabled by default. Enable it and point the OTLP exporter at your collector: ```bash theme={null} # Master switch — telemetry is off until this is true ENABLE_TELEMETRY=true # Resource attributes that identify this service in your backend OTEL_RESOURCE_SERVICE_NAME=plugin-br-pix-indirect-btg OTEL_LIBRARY_NAME=github.com/LerianStudio/plugin-br-pix-indirect-btg OTEL_RESOURCE_SERVICE_VERSION=${VERSION} OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT=${ENV_NAME} # OTLP collector endpoint (gRPC, default port 4317) OTEL_EXPORTER_OTLP_ENDPOINT_PORT=4317 OTEL_EXPORTER_OTLP_ENDPOINT=otel-collector:${OTEL_EXPORTER_OTLP_ENDPOINT_PORT} ``` When `ENABLE_TELEMETRY=true`, `OTEL_EXPORTER_OTLP_ENDPOINT` is required. Set the same OTel variables on the Application and on each Worker so traces correlate across services. ## Exporter: gRPC and TLS *** The plugin exports traces, metrics, and logs over **OTLP/gRPC**. The endpoint scheme controls transport security: | Endpoint value | Transport | Security | | ----------------------------------- | --------------- | ----------------------------------- | | `https://collector:4317` | gRPC over TLS | Secure (recommended for production) | | `http://collector:4317` | gRPC, plaintext | Insecure — inferred automatically | | `collector:4317` (bare `host:port`) | gRPC, plaintext | Insecure — inferred automatically | Insecure (plaintext) exporters are rejected outside development environments. In staging or production, use an `https://` endpoint, or explicitly acknowledge the risk via the lib-commons insecure-OTEL allowance only when you fully control the network path to the collector. ## Example: collector endpoint *** Point the plugin at any OTLP-compatible collector (the OpenTelemetry Collector, Grafana LGTM/Alloy, etc.) listening on the gRPC port: ```bash theme={null} # Local / development (plaintext gRPC) ENABLE_TELEMETRY=true OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 # Production (TLS gRPC) ENABLE_TELEMETRY=true OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4317 ``` The collector then fans telemetry out to your tracing, metrics, and logging backends. # 11. Health and readiness *** The Application and Workers expose a readiness probe at **`/readyz`** (alongside the standard liveness checks). Point your orchestrator's readiness probe at this endpoint so traffic is only routed once the service and its dependencies are ready. # Transfer purpose (MED 2.0) *** The cashout endpoint accepts an optional `X-Purpose` header that identifies the transaction purpose, used for MED 2.0 refund transfers. When omitted, it defaults to `TRANSFER`. ```bash theme={null} # Example: a MED 2.0 refund transfer POST /v1/transfers/cashout/process X-Purpose: INSTANT_PAYMENT_REFUND ``` Supported values are `TRANSFER` and `INSTANT_PAYMENT_REFUND`. See [MED 2.0 — Funds Recovery](/en/rails/pix/btg/indirect-pix-med-2-funds-recovery) for details. # Next steps *** With the plugin fully configured, you're ready to start operating Pix. Explore these topics to go deeper: * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Event types, payloads, retries, and best practices * [MED 2.0 — Funds Recovery](/en/rails/pix/btg/indirect-pix-med-2-funds-recovery) — Cross-account fraud recovery and the X-Purpose header * [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations) — Distributed partial refunds and unblocking * [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp) — Internal P2P settlement and TRCK002 reporting * [Main domains: DICT](/en/rails/pix/main-domains-dict) — Understanding Pix key management * [Main domains: transactions](/en/rails/pix/main-domains-transactions) — Transaction flows and lifecycle * [Main domains: QR Codes](/en/rails/pix/main-domains-qrcodes) — Static and dynamic QR code generation * [API reference](/en/reference/midaz/plugins/indirect-pix/create-entry) — Full API documentation for DICT, Claims, Transactions, QR Codes, and MED operations # Intra-PSP transfers Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-intra-psp How the Pix Indirect Plugin via BTG settles intra-PSP (P2P) transfers and refunds internally without BTG settlement, while reporting to BACEN via TRCK002. An intra-PSP transfer (also called P2P) is a Pix transfer between a payer and a payee at the **same participant**. The source and destination ISPB are identical. The money never leaves the institution, so the plugin settles the transfer internally and does not route it to BTG. The plugin still reports the transfer to BACEN for regulatory compliance. The plugin supports intra-PSP transfers and refunds. It settles them internally and reports each one to BACEN through TRCK002. # Detection *** The plugin marks a transfer as intra-PSP when the destination ISPB matches your configured `PIX_ISPB`: | Initiation type | Source of destination ISPB | | ----------------- | -------------------------------------------- | | `KEY` / `QR_CODE` | DICT lookup response (`account.participant`) | | `MANUAL` | `destination.ispb` in the request payload | Detection is internal. The initiation response and the cashout status match an external transfer. The plugin routes an intra-PSP transfer through the internal settlement path instead of BTG. # Processing model *** An intra-PSP transfer uses the same Midaz routing as an external transfer, through the `@external` transit account. The ledger behavior is identical. The plugin settles the transfer synchronously and does not wait for BTG webhooks. The plugin creates two Midaz transactions per transfer: 1. **Cashout** — `source → @external` (`pending: false`) 2. **Cashin** — `@external → destination` (`pending: false`) The internal cash-in reuses the same `CashinApprovalCommand` and `CashinSettlementCommand` pipelines as an external cash-in. These pipelines run CRM alias validation, balance checks, Pix key ownership, collection completion, and fee calculation. ## Flow *** ``` Process Cashout (intra-PSP detected) → Midaz debit: source → @external → Write inbound record to the webhook queue → Cashout status → PROCESSING (intermediary) → Return PROCESSING to client Inbound worker (existing) → Picks up the record and delivers it (HTTP + HMAC) to POST /v1/payment/intra-psp/transfers/webhooks Intra-PSP endpoint (orchestrates the full lifecycle) → CashinApprovalCommand → ACCEPTED / DENIED → ACCEPTED → CashinSettlementCommand → Midaz credit → Cashout COMPLETED → DENIED / settlement fails → revert Midaz debit → Cashout FAILED → Report to BTG TRCK002 (async, non-blocking) → Outbound webhooks: cashout.completed/failed + cashin.completed ``` The cashout responds with `PROCESSING`, like an external cashout that waits for BTG. The plugin delivers the final status (`COMPLETED` or `FAILED`) asynchronously through an outbound webhook. The intra-PSP endpoint is idempotent, so worker retries never duplicate transactions. # TRCK002 regulatory reporting *** The plugin reports every successful intra-PSP transaction to BACEN through BTG's **TRCK002** endpoint. * TRCK002 reporting is **non-blocking**. A report failure never rolls back the Midaz transaction or the transfer completion. The plugin retries a failed report. * The plugin creates a `TransactionReport` for each transaction. After BTG accepts the submission, the report status becomes `PROCESSING` and carries a `pactualId`. * BTG sends report status updates through a **CAMT025** webhook, typed `PIX_INTERNAL_TRANSACTIONS_REPORT`. The webhook moves the report to `CONFIRMED` (terminal) or `ERROR` (recoverable). * You can also query a report by end-to-end ID or return identification when a webhook does not arrive. # Intra-PSP refunds *** The plugin also processes a refund internally when the original cash-in was intra-PSP: * The plugin detects intra-PSP from the original transfer, when its source and destination ISPB match. * It debits the refund requester (`requester → @external`). It then delivers the refund cash-in to the original sender through the same queue and endpoint pattern. * The plugin reports the refund to TRCK002 with a `returnIdentification`. * The plugin persists a `REFUND` outbound webhook (DICT flow) to notify the requester, and the intra-PSP cash-in settlement queues `cashin.completed` for the original sender. Do not call unblock for an intra-PSP transfer. The unblock flow queries BTG for the transfer status. BTG never processes an internal transaction, so the query does not apply. See [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations). # Failure reasons *** The plugin delivers a cash-in validation failure asynchronously through the `cashout.failed` webhook. For an intra-PSP failure, the webhook carries the reason `INTRA_PSP_REJECTED` and a sanitized message. Common messages: | Message | Meaning | | -------------------------------------------- | ------------------------------------------------------- | | `pix key not found` | The Pix key does not exist in DICT. | | `pix key is not active` | The Pix key exists but is inactive. | | `pix key does not match account` | The Pix key does not belong to the destination account. | | `account cannot receive payment` | The destination account cannot receive the payment. | | `collection validation rejected the payment` | The dynamic QR code collection rejected the payment. | | `duplicate transaction` | A cash-in with the same end-to-end ID already exists. | # Next steps *** * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Event envelope, retries, and routing * [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations) — Distributed refunds and unblocking * [Setting up the integration](/en/rails/pix/btg/indirect-pix-integration) — ISPB and worker configuration * [API reference](/en/reference/midaz/plugins/indirect-pix/create-entry) — Full API documentation # MED 2.0 — Funds Recovery Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-med-2-funds-recovery Run BACEN's MED 2.0 Funds Recovery through the Pix Indirect Plugin via BTG — tracking graphs, infraction report analysis, refunds, and webhook events. MED 2.0 (Mecanismo Especial de Devolução) is BACEN's enhanced mechanism to recover funds in cases of fraud, scams, and operational errors. MED 1.0 handles single-transaction disputes through infraction reports. **MED 2.0 introduces a Funds Recovery flow** that tracks how fraudulent funds moved across multiple accounts. The flow coordinates blocking, analysis, and refunds across the participating institutions. The Pix Indirect Plugin (BTG) exposes the full Funds Recovery lifecycle as REST endpoints. It also sends webhook events, so your system stays in sync with each status change. MED 2.0 is a BACEN requirement for Pix participants. The plugin implements the Funds Recovery flow, so you can meet this requirement through your BTG indirect connection. # Concepts *** | Term | Definition | | --------------------- | ----------------------------------------------------------------------------------------- | | **Funds Recovery** | The MED 2.0 process that recovers funds across multiple accounts after a reported fraud | | **Tracking Graph** | A representation of how funds flowed across accounts, persons, and transactions | | **Root transaction** | The original fraudulent Pix transaction that starts the recovery | | **Infraction Report** | A report of a fraudulent/problematic transaction, now linked to its parent Funds Recovery | | **Refund Request** | A request to return blocked funds to the victim | # Lifecycle and status *** A Funds Recovery moves through the following states: Funds Recovery | Status | Description | | ------------------- | ---------------------------------------------------------- | | `CREATED` | Initial state after creation | | `TRACKED` | Tracking graph generated | | `AWAITING_ANALYSIS` | Blocking flow started, awaiting infraction report analysis | | `ANALYSED` | All infraction reports analysed, ready for refund | | `REFUNDING` | Refund requests initiated | | `COMPLETED` | All refunds completed | | `CANCELLED` | Recovery cancelled (only allowed before refund starts) | # Endpoints *** All Funds Recovery endpoints live under the DICT domain and require the `X-Account-Id` header. | Method | Endpoint | Description | | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | `POST` | [`/v1/dict/funds-recoveries`](/en/reference/midaz/plugins/indirect-pix/create-a-funds-recovery-request) | Create a funds recovery | | `GET` | [`/v1/dict/funds-recoveries/{id}`](/en/reference/midaz/plugins/indirect-pix/retrieve-funds-recovery-details) | Consult a funds recovery | | `PATCH` | [`/v1/dict/funds-recoveries/{id}`](/en/reference/midaz/plugins/indirect-pix/update-a-funds-recovery-request) | Update situation type and contact info | | `POST` | [`/v1/dict/funds-recoveries/{id}/cancel`](/en/reference/midaz/plugins/indirect-pix/cancel-a-funds-recovery-request) | Cancel (before refund starts) | | `GET` | [`/v1/dict/funds-recoveries/{id}/tracking-graph`](/en/reference/midaz/plugins/indirect-pix/retrieve-funds-recovery-tracking-graph) | View the tracking graph | | `GET` | [`/v1/dict/funds-recoveries/{id}/infraction-reports`](/en/reference/midaz/plugins/indirect-pix/list-funds-recovery-infraction-reports) | List linked infraction reports | | `POST` | [`/v1/dict/funds-recoveries/{id}/refund`](/en/reference/midaz/plugins/indirect-pix/request-funds-recovery-refund) | Request refunds (status must be `ANALYSED`) | | `GET` | [`/v1/dict/funds-recoveries/{id}/refunds`](/en/reference/midaz/plugins/indirect-pix/list-funds-recovery-refunds) | List refund requests | ## Create a funds recovery *** ```json theme={null} POST /v1/dict/funds-recoveries { "rootTransactionId": "E9999901012341234123412345678900", "situationType": "SCAM", "contactInformation": { "email": "fraud-ops@example.com", "phone": "+5511999999999" }, "reportDetails": "Customer reported unauthorized Pix transfer", "trackingGraphParameters": { "minTransactionAmount": "10.00", "maxTransactions": 100, "hopWindow": "PT24H", "maxHops": 5 } } ``` ### Validation rules | Field | Requirement | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `rootTransactionId` | Required, 32 alphanumeric characters | | `situationType` | Required — one of `SCAM`, `ACCOUNT_TAKEOVER`, `COERCION`, `FRAUDULENT_ACCESS`, `OTHER`, `UNKNOWN` | | `contactInformation` | Required — object with `email` and/or `phone` | | `trackingGraphParameters.minTransactionAmount` | Optional, positive decimal | | `trackingGraphParameters.maxTransactions` | Optional, 1–1000 | | `trackingGraphParameters.hopWindow` | Optional, ISO 8601 duration (e.g. `PT24H`) | | `trackingGraphParameters.maxHops` | Optional, 1–10 | A successful call returns **HTTP 201**. The response contains the new funds recovery and its tracking graph data. The plugin persists the record locally with status `CREATED`. ## Tracking graph *** The plugin fetches the tracking graph fresh from BTG on every call. The graph has no local state. It lists the persons, accounts, and transactions in the fraud flow, with the refundable amount for each transaction. ``` GET /v1/dict/funds-recoveries/{id}/tracking-graph ``` Response includes: * `parameters` — the graph generation parameters * `persons[]` — natural and legal persons involved * `accounts[]` — accounts in the flow with their participant ISPBs * `transactions[]` — Pix transactions with amounts and refundable amounts ## Request refunds *** Once the recovery reaches `ANALYSED`, request the return of blocked funds: ``` POST /v1/dict/funds-recoveries/{id}/refund ``` The plugin calls BTG, transitions the recovery to `REFUNDING`, and returns **HTTP 200**. Track individual refund statuses with [List Refunds](/en/reference/midaz/plugins/indirect-pix/list-funds-recovery-refunds). # X-Purpose header (MED 2.0 transfers) *** MED 2.0 refund transfers must carry a transaction purpose. The cashout endpoint accepts an optional `X-Purpose` header that the plugin maps to BTG's `transactionType`. ``` POST /v1/transfers/cashout/process X-Purpose: INSTANT_PAYMENT_REFUND ``` | Value | Description | BTG `transactionType` | | ------------------------ | ------------------------------------------------------ | ------------------------ | | `TRANSFER` | Standard Pix transfer (default when header is omitted) | `TRANSFER` | | `INSTANT_PAYMENT_REFUND` | MED 2.0 refund transfer | `INSTANT_PAYMENT_REFUND` | Only `TRANSFER` and `INSTANT_PAYMENT_REFUND` are currently supported. The values `CHANGE`, `WITHDRAWAL`, `REFUND_AUTOMATIC_PIX`, and `INSTALLMENT_PIX` return **HTTP 400** with error `PIX-0429` (Unsupported Purpose). Transfer responses also include the `purpose` value ([Retrieve a Pix Transfer](/en/reference/midaz/plugins/indirect-pix/retrieve-a-pix-transfer) and the list endpoints). Existing records default to `TRANSFER`. # Correlation fields *** Two existing entities now carry a `fundsRecoveryId` field that links a dispute to its parent recovery: * **Infraction reports** — [Retrieve an Infraction Report](/en/reference/midaz/plugins/indirect-pix/retrieve-an-infraction-report) and [the list endpoint](/en/reference/midaz/plugins/indirect-pix/list-infraction-reports) * **Refund requests** — [Retrieve a Refund Request](/en/reference/midaz/plugins/indirect-pix/retrieve-a-refund-request) and [the list endpoint](/en/reference/midaz/plugins/indirect-pix/list-refund-requests) Records created outside the MED 2.0 flow do not carry this field. # Webhooks *** Two inbound BTG webhooks drive the Funds Recovery flow. Each one produces a matching outbound event to your system: | Outbound `entityType` | Trigger | Behavior | | ----------------------- | ----------------------------------- | ------------------------------------------------------------------------------- | | `FUNDS_RECOVERY` | BTG `FUNDS_RECOVERY` webhook | Plugin updates the local record, then notifies your system with the full entity | | `FUNDS_RECOVERY_EVENTS` | BTG `FUNDS_RECOVERY_EVENTS` webhook | Pass-through lifecycle event — no database update | Both use `flowType: DICT`. See the [Webhooks guide](/en/rails/pix/btg/indirect-pix-webhooks) for envelope format, retries, and routing. **Funds recovery entity event:** ```json theme={null} { "entityType": "FUNDS_RECOVERY", "flowType": "DICT", "payload": { "id": "91d65e98-97c0-4b0f-b577-73625da1f9fc", "externalId": "ca1b9c01-ff9e-4a58-90ab-d31512e15ce0", "accountId": "01989f9e-6508-79f8-9540-835be49fbd0d", "status": "CREATED", "rootTransactionId": "E9999901012341234123412345678900", "situationType": "SCAM", "reporterParticipant": "99999010", "contactInformation": {}, "reportDetails": "Details to help receiving participants", "createdAt": "2020-01-17T10:00:00.000Z", "updatedAt": "2020-01-17T10:00:00.000Z" } } ``` **Lifecycle event (pass-through):** ```json theme={null} { "entityType": "FUNDS_RECOVERY_EVENTS", "flowType": "DICT", "payload": { "id": "10001", "event": "FUNDS_RECOVERY_COMPLETED", "entityType": "FUNDS_RECOVERY", "entityId": "527179ce-b991-4add-a70f-e0fdbb98e6da", "timestamp": "2025-01-11T10:00:00.000Z" } } ``` Lifecycle `event` values: `FUNDS_RECOVERY_ANALYSED`, `FUNDS_RECOVERY_COMPLETED`, `FUNDS_RECOVERY_INFORMATION_UPDATED`, `FUNDS_RECOVERY_CANCELLED`. # Deprecation notice *** Do not use [Create an Infraction Report](/en/reference/midaz/plugins/indirect-pix/create-an-infraction-report) for new integrations. MED 2.0 deprecates this endpoint and creates infraction reports automatically through the Funds Recovery flow. The endpoint still works for backward compatibility. New integrations must use the Funds Recovery APIs. # Next steps *** * [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations) — Distributed partial refunds and unblocking stuck refunds * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Event envelope, retries, and routing * [Main domains: MED](/en/rails/pix/main-domains-med) — MED dispute and refund concepts * [API reference](/en/reference/midaz/plugins/indirect-pix/create-entry) — Full API documentation # What is Pix Indirect (BTG)? Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-overview BTG Pactual acts as the direct participant that connects your institution to BACEN's Pix infrastructure through its Banking as a Service offering. BTG Pactual is one of the largest investment banks in Latin America. It operates as a **direct participant** in Brazil's Pix ecosystem. Through its Banking as a Service (BaaS) infrastructure, BTG connects indirect participants to BACEN's SPI and DICT. SPI is the Instant Payment System. DICT is the directory of transactional account identifiers. # BTG's role in the Pix ecosystem *** As the direct participant, BTG handles the layers that require regulatory licensing and direct BACEN integration: * **SPI connectivity** — Sends and receives Pix payment and refund messages through BACEN's real-time settlement infrastructure * **DICT access** — Manages Pix key lookups, registrations, and claim resolutions at the BACEN directory level * **Settlement and liquidity** — Processes financial settlement between institutions through BACEN's infrastructure * **Regulatory compliance** — Maintains the certifications, audits, and operational requirements BACEN mandates for direct participants * **Webhook notifications** — Delivers real-time event callbacks for transaction status changes, key events, and dispute updates * **mTLS security** — Provides certificate-based authentication for secure webhook communication Your institution retains full control of its ledger, customer data, business rules, and operational infrastructure. BTG provides connectivity — not account management. Beyond standard transfers and DICT operations, the plugin supports the advanced Pix flows BACEN requires: * **MED 2.0 Funds Recovery** — Track fraudulent fund movements across accounts and request coordinated refunds * **Distributed partial refunds** — Return one refund debited from several internal accounts in a single operation * **Intra-PSP (P2P) transfers** — Settle internal transfers without BTG and report them to BACEN through TRCK002 * **Unblock** — Recover refunds or transfers stuck in `PROCESSING` # Why BTG *** BTG brings specific advantages as a direct participant for Pix operations: * **Production infrastructure** — BTG already operates at scale in the Pix ecosystem * **Built-in BTG integration** — The plugin includes support for BTG's APIs, authentication, and event model. You do not build custom middleware * **Faster launch** — BTG absorbs the regulatory and infrastructure complexity, so your institution launches Pix sooner * **Simplified onboarding** — BTG provides credentials directly when your institution signs up for indirect BACEN integration. You do not need a separate certification process # Next steps *** With this context, explore how to configure and operate the plugin with BTG: * [Setting up the integration](/en/rails/pix/btg/indirect-pix-integration) — Configure the plugin's connection to BTG, Midaz, CRM, and supporting services * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Event types, payloads, retries, and handling of BTG notifications * [MED 2.0 — Funds Recovery](/en/rails/pix/btg/indirect-pix-med-2-funds-recovery) — Cross-account fraud recovery and the X-Purpose header * [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations) — Distributed partial refunds and unblocking stuck operations * [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp) — Internal P2P settlement and TRCK002 reporting * [API reference](/en/reference/midaz/plugins/indirect-pix/create-entry) — Full API for DICT, Claims, Transactions, QR Codes, and MED operations # QR Codes Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-qrcodes Generate and decode Pix QR Codes via BTG — static BR Codes, immediate COB and due-date COBV charges, EMV payloads, and the universal QR Code decoder. The Pix Indirect Plugin (BTG) generates and manages Pix QR Codes (BR Codes) so your customers can receive payments. The plugin supports four QR Code types: static BR Codes, immediate charges (COB), due-date charges (COBV), and a decoder for payment initiation. All QR Codes follow the EMV QCO specification and embed the receiver's Pix key. Before you create a QR Code, the receiver key must already exist in DICT. The requesting account must own the key. You identify the account with the `X-Account-Id` header. See the [DICT guide](/en/rails/pix/btg/indirect-pix-dict) for key registration. # Choosing a QR Code type *** | Type | Characteristics | Best for | | ------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | **Static** | Reusable (multiple payments) · optional amount (fixed or payer-entered) · no expiration | POS displays, printed material, donations, e-commerce with variable amounts | | **Immediate (COB)** | Single payment · amount required · seconds-based expiration | Checkout, invoicing, one-time purchases | | **Due-date (COBV)** | Single payment · amount + charges · due date + grace period | Bills, installments, subscriptions, B2B invoicing (boleto-like) | | **Decode** | Reads any scanned QR Code | Initiating a payment from a scanned code | # Static QR Codes *** Static BR Codes (`/v1/brcode/static`) are reusable. Different payers can pay the same code many times. Each code links to a Pix key and, optionally, to merchant data. **Fixed vs. variable amount:** * **With amount** — the payer scans and confirms a predefined value. Useful for fixed-price items. * **Without amount** — the payer scans and enters the value manually. Useful for donations or open checkout. You can add merchant data to the code: `merchant.name`, `merchant.city`, `merchant.categoryCode` (MCC), and `merchant.postalCode`. You can also add an optional `txId` (alphanumeric, up to 25 characters) for reconciliation. If you omit merchant data, the plugin fills it from CRM holder data. ```json theme={null} POST /v1/brcode/static X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d { "receiverKey": "+5511999999999", "amount": "100.00", "description": "Payment for order #12345", "txId": "TX123ABC", "merchant": { "name": "Loja ABC", "city": "São Paulo", "categoryCode": "5411" } } → 201 Created { "id": "...", "emv": "00020126580014br.gov.bcb.pix..." } ``` Pass `include_base64=true` to also receive a Base64-encoded PNG of the QR Code. The plugin validates that the account owns the receiver key before it creates the code. **Reference:** [Create a static QR code](/en/reference/midaz/plugins/indirect-pix/create-a-static-qr-code) · [List](/en/reference/midaz/plugins/indirect-pix/list-static-qr-codes) · [Retrieve](/en/reference/midaz/plugins/indirect-pix/retrieve-a-static-qr-code) # Immediate charges (COB) *** Immediate collections (`/v1/collections/immediate`), or cobrança imediata, are dynamic, single-use QR Codes. Each charge sets a specific amount and a short validity window. A required `txId` identifies each charge. A payer can settle a charge only once. **Required fields:** `amount`, `expirationSeconds`, `receiverKey`, and `txId`. The optional `debtorName` and `debtorDocument` fields identify the intended payer. **Lifecycle:** | Status | Meaning | | ----------- | --------------------------------- | | `ACTIVE` | Created and available for payment | | `COMPLETED` | Payment received successfully | | `EXPIRED` | Validity window elapsed | | `DELETED` | Cancelled by the merchant | When you create a charge, the plugin schedules an expiration job. After `expirationSeconds` elapses, the charge moves to `EXPIRED` and no payer can settle it. You can update (`PUT`) or delete (`DELETE`) a charge only while it is `ACTIVE`. **Payment confirmation:** when an incoming Pix settles the charge, the plugin moves it to `COMPLETED`. The plugin then emits a webhook to notify your system in real time. See the [Webhooks guide](/en/rails/pix/btg/indirect-pix-webhooks) and the [Collections guide](/en/rails/pix/btg/indirect-pix-collections) for the full payment flow. **Reference:** [Create an immediate charge](/en/reference/midaz/plugins/indirect-pix/create-an-immediate-charge) · [List](/en/reference/midaz/plugins/indirect-pix/list-immediate-charges) · [Retrieve](/en/reference/midaz/plugins/indirect-pix/retrieve-immediate-charge-details) · [Update](/en/reference/midaz/plugins/indirect-pix/update-an-immediate-charge) · [Delete](/en/reference/midaz/plugins/indirect-pix/delete-an-immediate-charge) # Due-date charges (COBV) *** Due-date collections (`/v1/collections/duedate`), or cobrança com vencimento, are dynamic QR Codes for billing with a due date, like a boleto. They support complex amount rules. They require full debtor and receiver data. **Key fields:** `dueDate`, `validAfterDue`, a required `debtor`, and an `amount` object. The required `validAfterDue` field sets the days the charge stays payable after the due date. The `debtor` needs a name and a CPF or CNPJ. It also takes optional email, address, city, state, and zipCode. The `amount` object holds the `original` value and optional charge components: | Component | Modality | Applies | | ----------- | ------------------------------------------------------- | -------------------------- | | `fine` | `FIXED_VALUE` or `PERCENT` | Penalty for late payment | | `interest` | e.g. `PERCENTAGE_PER_MONTH_CALENDAR_DAYS` | Accrues after the due date | | `discount` | a modality with a value, or a `discountDateFixed` array | Reward for early payment | | `abatement` | `FIXED_VALUE` or `PERCENT` | Reduction on the amount | Payment timing determines the final value. Before the due date, the payer gets any discount. On the due date, the `original` amount applies. After the due date, the plugin adds the fine and interest, then subtracts any abatement. A dated discount (`discountDateFixed`) needs a `date` before the `dueDate`. The plugin requires a valid debtor document (CPF or CNPJ). It keeps the charge payable until the due date plus `validAfterDue` days. **Reference:** [Create a due-date charge](/en/reference/midaz/plugins/indirect-pix/create-a-dynamic-charge-with-due-date) · [List](/en/reference/midaz/plugins/indirect-pix/list-dynamic-charges-with-due-date) · [Retrieve](/en/reference/midaz/plugins/indirect-pix/retrieve-dynamic-charge-with-due-date-details) · [Update](/en/reference/midaz/plugins/indirect-pix/update-a-dynamic-charge-with-due-date) # Decoding QR Codes *** The decoder (`POST /v1/qrcodes/decode`) parses any scanned Pix QR Code. It returns the embedded payment data. Use it in payment-initiation flows. A customer scans a QR Code, and you read the receiver, amount, and charge details before you confirm the payment. The plugin auto-detects the QR Code type and returns a typed response: * **STATIC** — receiver key, optional amount/description, merchant info, `txId`. * **IMMEDIATE (COB)** — all static fields plus required amount, expiration, status, and review number. * **DUE\_DATE (COBV)** — all immediate fields plus due date, `validAfterDue`, debtor, receiver, and the full fine/interest/discount structure. For dynamic codes, the plugin resolves the payload from BTG before it returns. The response reflects the charge's current state. ```json theme={null} POST /v1/qrcodes/decode X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d { "emv": "00020126580014br.gov.bcb.pix..." } → 200 OK { "type": "IMMEDIATE", "amount": "100.00", "receiverKey": "...", "status": "ACTIVE" } ``` **Reference:** [Decode a Pix QR code](/en/reference/midaz/plugins/indirect-pix/decode-a-pix-qr-code) # Next steps *** * [Collections](/en/rails/pix/btg/indirect-pix-collections) — Collection lifecycle, payment linking, and webhook events * [DICT](/en/rails/pix/btg/indirect-pix-dict) — Registering the Pix keys your QR Codes receive on * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Payment and status notifications # Refund operations Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-refund-operations Handle Pix refund edge cases via BTG — distributed partial refunds (devoluções) across internal accounts and unblocking operations stuck in PROCESSING. The Pix Indirect Plugin (BTG) processes Pix refunds (devoluções) through the [Refund a Received Pix Transfer](/en/reference/midaz/plugins/indirect-pix/refund-a-received-pix-transfer) endpoint. Two capabilities extend that flow for MED and fraud scenarios: * **Distributed partial refunds** — return a refund from multiple internal accounts in a single operation * **Unblock** — recover a refund or transfer that is stuck in `PROCESSING` # Distributed partial refunds *** A received Pix can split internally across several accounts. For example, the principal amount goes to the customer account and a fee goes to a fee account. A later MED or fraud refund can then debit part of the amount from each account. The standard refund flow debits one account. To split the debit, send an optional `operations` array in the request body. **The `operations` array is the only signal** — there is no new endpoint, environment variable, or feature flag. ## Request *** ```json theme={null} POST /v1/transfers/{transfer_id}/refunds { "amount": "1000.82", "description": "MED Cappta", "operations": [ { "accountAlias": "alias-conta-cliente", "amount": "0.82" }, { "accountAlias": "alias-conta-fee", "amount": "1000.00" } ] } ``` * Without `operations` → the current single-account flow runs unchanged. * With `operations` → the plugin debits each `accountAlias` for its `amount` in Midaz, and BTG receives a single pacs.004 for the **total** refund value. ## Validation rules *** | Rule | Error | | ------------------------------------------------- | -------------------------------------- | | `sum(operations[].amount)` must equal `amount` | `400 PIX-0447` Operations Sum Mismatch | | `accountAlias` must not repeat within the request | `400 PIX-0448` Duplicate Account Alias | | each `amount` must be greater than `0` | `400 PIX-0004` Invalid Field Values | | each `amount` must have at most 2 decimal places | `400 PIX-0004` Invalid Field Values | | the request must contain at most 50 operations | `400 PIX-0013` Limit Exceeded | | the original cash-in must exist | `404 PIX-0425` Cashin Not Found | The endpoint, BTG flow (pacs.004 with the total value), idempotency, and authentication match the standard refund. Only the internal Midaz debit composition changes. ## Example — Cappta *** The plugin split a R$ 50,000.00 cash-in: R$ 49,000.00 to the customer account and R$ 1,000.00 to a fee account. After fraud, the refund must be R$ 1,000.82. Only R$ 0.82 remains in the customer account. The `operations` array debits R$ 0.82 from the customer account and R$ 1,000.00 from the fee account. BTG receives a single R$ 1,000.82 refund with no manual ledger consolidation. # Unblocking stuck operations *** A reversal call to BTG can time out before BTG confirms it. The refund or transfer then stays stuck in `PROCESSING`, and Midaz still holds the funds. Two endpoints re-query BTG and drive the operation to its terminal state. | Method | Endpoint | Unblocks | | ------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | `POST` | [`/v1/refunds/{refund_id}/unblock`](/en/reference/midaz/plugins/indirect-pix/unblock-a-pix-refund) | A refund stuck in `PROCESSING` | | `POST` | [`/v1/transfers/{transfer_id}/unblock`](/en/reference/midaz/plugins/indirect-pix/unblock-a-pix-transfer) | A transfer stuck in `PENDING`/`PROCESSING` | Both require the `X-Account-Id` header. ## How unblock works *** The plugin re-queries the reversal or transfer status at BTG and: * If BTG reports `CONFIRMED` or `ERROR` → dispatches the corresponding settlement and moves the operation to its terminal state. * If BTG still reports `INITIATED`/`PROCESSING` → returns **HTTP 200** with no action — retry later. A consistency guard aborts the operation if BTG's `entity`, `returnIdentification`, or `originalEndToEndId` diverge from the local record. This prevents settlement against the wrong transaction. ```json theme={null} POST /v1/refunds/{refund_id}/unblock X-Account-Id: ``` The response includes the updated `refund`, a `message`, and the `btgStatus`. ## Handling a 404 from BTG *** BTG can return `404` when it no longer has the reversal or transfer. The result then depends on the operation type and the opt-in `allowNotFoundUnblock` flag in the request body. | Operation | Default (strict) | With `allowNotFoundUnblock: true` | | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Transfer** ([`/v1/transfers/{id}/unblock`](/en/reference/midaz/plugins/indirect-pix/unblock-a-pix-transfer)) | Returns an error | Reverts the Midaz hold (best-effort, idempotent), marks the cashout `FAILED` with `BTG_NOT_FOUND`, emits a `CASHOUT` outbound webhook, and returns `200` with `btgStatus: "NOT_FOUND"` | | **Refund** ([`/v1/refunds/{id}/unblock`](/en/reference/midaz/plugins/indirect-pix/unblock-a-pix-refund)) | Returns `PIX-1012` (`ErrProviderRefundNotFound`) | Reverts the hold and drives the refund to its terminal state | ```json theme={null} POST /v1/transfers/{transfer_id}/unblock { "allowNotFoundUnblock": true } ``` `allowNotFoundUnblock` defaults to `false`. An empty or absent body, or `false`, preserves the strict behavior. A BTG 404 then returns an error and never reverts the hold. Opt in only when you have confirmed that the operation must be released. The opt-in 404 recovery applies only to `CASHOUT` operations in `PENDING`/`PROCESSING` with a non-empty `endToEndId`. Cash-ins and terminal statuses never enter this branch. ## Intra-PSP limitation *** The transfer unblock flow resolves state by querying BTG. It does not apply to intra-PSP (internal) transfers, which have no BTG transaction. See [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp). # Next steps *** * [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp) — Internal P2P transfers and refunds * [MED 2.0 — Funds Recovery](/en/rails/pix/btg/indirect-pix-med-2-funds-recovery) — Cross-account fraud recovery * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Refund and transfer event handling * [API reference](/en/reference/midaz/plugins/indirect-pix/create-entry) — Full API documentation # Transfers and cash-out Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-transfers Send Pix cash-out through the Indirect Pix Plugin via BTG — the two-step initiate-and-process flow that confirms the destination before moving funds. A Pix cash-out moves money from an account to an external destination. The **Indirect Pix Plugin (BTG)** runs it in two steps — **initiate**, then **process**. You confirm *where* the money goes before any funds leave the ledger. ## Why two steps *** Splitting a cash-out into initiate and process gives you a checkpoint between "who is the payee?" and "send the money": * **Verify the destination first.** Initiate validates and resolves the payee account without touching balances. A wrong Pix key or an invalid account fails here — before any money moves. * **Show the payer who receives the money.** The initiate response returns the resolved account owner. Your app can display the real name and let the payer confirm first. * **Move funds only on confirmation.** Nothing is debited until you process the transfer. If the payer abandons the flow, there's no reversal to make — there was never any movement to undo. This mirrors how a good payment experience works: look up the destination, confirm the details, then pay. Both steps are idempotent — safe to retry without creating duplicate transfers. See [Retries and idempotency](/en/reference/retries-idempotency). ## Step 1 — Initiate: confirm the destination *** Initiating a transfer creates a short-lived record that validates and resolves the payee **without moving funds**. How the plugin finds the destination depends on what you start with: | You start with | Initiation type | What the plugin does | | ------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------- | | A Pix key (CPF, CNPJ, email, phone, or EVP) | `KEY` | Looks the key up in [DICT](/en/rails/pix/btg/indirect-pix-dict) and resolves the destination account for you. | | A QR code (BR Code) | `QR_CODE` | Decodes the code and resolves the destination via DICT. See [QR Codes](/en/rails/pix/btg/indirect-pix-qrcodes). | | The payee's full account details | `MANUAL` | Uses the branch, account, participant, and owner document you provide — no DICT lookup needed. | For `KEY` and `QR_CODE`, you never supply the destination yourself. The plugin resolves it and returns it in the response, ready to show the payer for confirmation. ### Request — pick the tab for your initiation type ```json KEY theme={null} POST /v1/transfers/cashout/initiate X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d { "initiationType": "KEY", "key": "john.doe@example.com" } ``` ```json QR_CODE theme={null} POST /v1/transfers/cashout/initiate X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d { "initiationType": "QR_CODE", "emv": "00020126...5802BR5913Fulano..." } ``` ```json MANUAL theme={null} POST /v1/transfers/cashout/initiate X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d { "initiationType": "MANUAL", "destination": { "account": { "branch": "0001", "number": "123456789", "participant": "12345678", "type": "CACC" }, "owner": { "document": "12345678901", "name": "John Doe" } } } ``` Account `type` values are `CACC` (checking), `SVGS` (savings), `TRAN` (transaction), and `OTHR` (other). `endToEndId` is optional for all types — it's auto-generated when omitted. ### Response The response returns the initiation `id` (used as `initiationId` in step 2) and the resolved `destination`: ```json theme={null} → 201 Created { "id": "019c96a0-0c82-7c3d-8dcc-c180868b45c4", "initiationType": "KEY", "endToEndId": "E1234567820240101000001234567890", "destination": { "account": { ... }, "owner": { ... } }, "expiresAt": "2024-01-15T11:00:00Z", "createdAt": "2024-01-15T10:30:00Z" } ``` Initiations expire. The response includes an `expiresAt` timestamp — process the transfer before it lapses, or initiate again. This keeps a confirmed destination from going stale between the lookup and the payment. **API reference:** [Initiate a Pix Transfer](/en/reference/midaz/plugins/indirect-pix/initiate-a-pix-transfer) ## Step 2 — Process: move the money *** Processing executes the cash-out from the initiation you confirmed. It debits the source account, then routes the payment to BTG for settlement with BACEN. Settlement with the Pix network is **asynchronous**. The transfer comes back as `PROCESSING` while BTG settles. The final result — completed or failed — arrives later through a `cashout` webhook. Build your flow to react to that event, not to wait on the process response. See [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks). ### Request Pass the `id` from the initiate response as `initiationId`, along with the `amount` to transfer: ```json theme={null} POST /v1/transfers/cashout/process X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d X-Purpose: TRANSFER { "initiationId": "019c96a0-0c82-7c3d-8dcc-c180868b45c4", "amount": "100.50" } ``` `amount` is required. You can also pass an optional `description` (max 140 characters) and `metadata` (custom key-value attributes). #### The `X-Purpose` header Use the optional `X-Purpose` header to declare the reason for the cash-out. It defaults to `TRANSFER` when omitted: | Value | When to use | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `TRANSFER` | A regular Pix cash-out — the default for ordinary payments. | | `INSTANT_PAYMENT_REFUND` | When the cash-out is refunding a previously received instant payment, so the network can classify it as a refund rather than a new transfer. | **Fixed-amount QR codes:** the initiation can be a `QR_CODE` whose EMV payload carries a fixed amount. In that case, the `amount` you send to process **must equal** that encoded amount. A mismatch is rejected before any funds move. ### Response ```json theme={null} → 201 Created { "id": "019c96a0-0c21-71f9-a487-66a1258278a1", "endToEndId": "E1234567820240101000001234567890", "amount": "100.50", "status": "PROCESSING", "type": "CASHOUT", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z" } ``` When the destination belongs to your own institution, the money never leaves for BTG — it settles internally as a P2P transfer. See [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp). **API reference:** [Process a Pix Transfer](/en/reference/midaz/plugins/indirect-pix/process-a-pix-transfer) ## Tracking a transfer *** Every transfer follows a predictable lifecycle. It starts in `PENDING`/`PROCESSING` while in flight, then reaches a terminal `COMPLETED`, `FAILED`, or `CANCELLED`. To check where a transfer stands, retrieve a single one by its id. You can also list transfers filtered by status, type (cash-out or cash-in), or date range. ```json theme={null} GET /v1/transfers?type=CASHOUT&status=COMPLETED&limit=10&page=1 X-Account-Id: 01989f9e-6508-79f8-9540-835be49fbd0d ``` List filters include `status`, `type` (`CASHOUT`/`CASHIN`), `end_to_end`, and `modified_after`/`modified_before`, plus `page`/`limit`/`sort_order` pagination. **API reference:** [List transfers](/en/reference/midaz/plugins/indirect-pix/list-pix-transfers) · [Retrieve a transfer](/en/reference/midaz/plugins/indirect-pix/retrieve-a-pix-transfer) ## How transfers land in Midaz *** The plugin posts every settled movement to Midaz as a ledger transaction, with the external leg against the `@external/BRL` account. Midaz records the accounting entry and correlation metadata — not the transfer's full banking details. Counterparty branch, account number, account type, and Pix key never reach Midaz; the one exception is the payer's identity on cash-in (`sourceBank`, `sourceDocument`, `sourceName`), stamped when known. The full counterparty detail lives in the plugin's transfer record. The metadata stamped on the Midaz transaction depends on the flow: | Flow | Metadata keys | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Cash-out | `initiationId`, `endToEndId`, `accountId`, `transferType: CASHOUT`, `paymentType`, `initiationType` | | Cash-in | `endToEndId`, `accountId`, `transferType: CASHIN`, `paymentType`, `initiationType` — plus `sourceBank`, `sourceDocument`, and `sourceName` when the payer is known | | Refund (out) | `refundId`, `originalEndToEndId`, `returnIdentification`, `accountId`, `transferType: REFUND_CASHOUT`, `refundType`, `reason` | | Refund (in) | The same refund keys with `transferType: REFUND_CASHIN` | The Midaz transaction `code` also carries the `endToEndId` (or the `returnIdentification` for refunds), so the E2E identifier is visible directly on the ledger entry. Correlation works in both directions: * The plugin stores the Midaz transaction and operation identifiers on its own transfer and refund records, and uses them to commit, cancel, or revert ledger entries. * The Midaz transaction carries correlation keys in its metadata: filter by `metadata.endToEndId` for cash-outs and cash-ins, or by `metadata.originalEndToEndId` / `metadata.returnIdentification` for refunds. The transaction `code` is the common fallback — it carries the E2E ID on transfers and the return identification on refunds. Custom `metadata` you pass when processing a cash-out is stored with the plugin's transfer record and returned by the plugin's own API. It is **not** copied onto the Midaz transaction — the Midaz metadata keys above are fixed by the plugin. ## When a transfer gets stuck *** If the settlement call to BTG times out before BTG confirms, a transfer can stay in `PROCESSING` with its funds on hold. The plugin provides an **unblock** operation. Unblock re-checks the transfer with BTG and drives it to the correct final state. It settles the transfer if BTG confirms, or releases the hold if BTG never received it. Unblock doesn't apply to intra-PSP transfers — there's no BTG transaction to re-check. For the full unblock behavior and its options, see [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations). ## Next steps *** * [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp) — Internal P2P settlement * [QR Codes](/en/rails/pix/btg/indirect-pix-qrcodes) — Generating and decoding QR codes * [Refund operations](/en/rails/pix/btg/indirect-pix-refund-operations) — Refunds and unblocking * [Webhooks](/en/rails/pix/btg/indirect-pix-webhooks) — Cash-out and cash-in event handling * [API reference](/en/reference/midaz/plugins/indirect-pix/initiate-a-pix-transfer) — Full request/response details, headers, and field schemas # Webhooks Source: https://docs.lerian.studio/en/rails/pix/btg/indirect-pix-webhooks Configure and consume webhooks for the Pix Indirect Plugin via BTG — DICT claim, infraction, refund, transfer, and MED 2.0 event types with HMAC signing. Webhooks are the primary mechanism the **Pix Indirect Plugin (BTG)** uses to notify you about Pix-related events in real time. You do not rely on synchronous responses. Instead, you receive **asynchronous, event-driven callbacks** when relevant changes occur in Pix operations — transfers, refunds, key claims, or MED events. This model gives you: * Near real-time updates * Decoupled integrations * Reliable reconciliation and operational traceability These webhooks apply only to the **Indirect Pix model via BTG**. Direct Pix webhooks can differ by connectivity model. A separate page documents them. # Prerequisites *** Before you configure webhooks, make sure you have: * The Pix Indirect Plugin configured and running (see [How indirect participation works](/en/rails/pix/pix-overview)) * An HTTPS endpoint ready to receive webhook requests * Basic understanding of Pix event lifecycle and transaction flows # Why webhooks matter in Pix *** Pix is an asynchronous, multi-party system. An API request can succeed before the transaction reaches its **final state**. The system confirms that state later, after settlement and counterparty acknowledgment. Webhooks allow your system to: * Track **authoritative transaction status** * React to **refunds, reversals, and MED events** * Maintain **ledger and operational consistency** * Reduce polling and operational overhead # Event types *** You receive events grouped by **flow** and **entity**, aligned with BACEN (Banco Central do Brasil) domains. | Flow | Entity | Description | | -------- | ---------------------- | ------------------------------------------------------------------------------ | | DICT | CLAIM | Pix key portability and ownership claim events | | DICT | INFRACTION\_REPORT | MED (Mecanismo Especial de Devolução) infraction reports and dispute lifecycle | | DICT | REFUND | MED refund request events | | DICT | FUNDS\_RECOVERY | MED 2.0 Funds Recovery entity status changes (DB-backed) | | DICT | FUNDS\_RECOVERY\_EVENT | MED 2.0 Funds Recovery lifecycle events (pass-through) | | TRANSFER | CASHIN | Incoming Pix payment status updates | | TRANSFER | CASHOUT | Outgoing Pix payment status updates | | REFUND | CASHIN | Incoming Pix refund status updates | | REFUND | CASHOUT | Outgoing Pix refund status updates | Each event reflects a **state transition** in the Pix ecosystem. Treat each event as the source of truth. The two **MED 2.0** entities behave differently. The plugin emits `FUNDS_RECOVERY` after it updates its local record. `FUNDS_RECOVERY_EVENT` is a pass-through of BTG lifecycle events with no database update. See [MED 2.0 — Funds Recovery](/en/rails/pix/btg/indirect-pix-med-2-funds-recovery) for the full flow. **DICT** (Diretório de Identificadores de Contas Transacionais) is the BACEN directory that manages Pix keys and related operations like claims, infractions, and refunds. # Webhook configuration *** To enable webhooks, configure the **destination URLs** and select which event types your system receives. ## Environment variables *** You can configure webhook endpoints at **entity**, **flow**, or **global** level. | Flow | Entity | Entity-level URL variable | | -------- | ------------------ | ------------------------------------ | | DICT | CLAIM | `WEBHOOK_DICT_CLAIM_URL` | | DICT | INFRACTION\_REPORT | `WEBHOOK_DICT_INFRACTION_REPORT_URL` | | DICT | REFUND | `WEBHOOK_DICT_REFUND_URL` | | TRANSFER | CASHIN | `WEBHOOK_TRANSFER_CASHIN_URL` | | TRANSFER | CASHOUT | `WEBHOOK_TRANSFER_CASHOUT_URL` | | REFUND | CASHIN | `WEBHOOK_REFUND_CASHIN_URL` | | REFUND | CASHOUT | `WEBHOOK_REFUND_CASHOUT_URL` | Each flow also has a **flow-level URL** for all its entities. The plugin uses it when no entity-level URL exists: `WEBHOOK_DICT_URL`, `WEBHOOK_TRANSFER_URL`, and `WEBHOOK_REFUND_URL`. ## URL resolution priority *** When you configure multiple URLs, the plugin resolves them in this order: 1. **Entity-level URL** Example: `WEBHOOK_DICT_CLAIM_URL` 2. **Flow-level URL** Example: `WEBHOOK_DICT_URL` 3. **Default URL** `WEBHOOK_DEFAULT_URL` This gives you fine-grained routing control and no duplicate infrastructure. # Request format *** ## Headers *** Every webhook request includes standardized headers for traceability and security. | Header | Description | | ----------------- | ----------------------------------------- | | `Content-Type` | `application/json` | | `X-Request-ID` | Unique request identifier | | `X-Entity-Type` | Event entity (e.g. `INFRACTION_REPORT`) | | `X-Flow-Type` | Source domain (e.g. `DICT`) | | `Idempotency-Key` | Unique event identifier for deduplication | ## Body structure *** ```json theme={null} { "entityType": "INFRACTION_REPORT", "flowType": "DICT", "payload": { ... } } ``` | Field | Description | | ------------ | ------------------- | | `entityType` | Event entity | | `flowType` | Pix domain | | `payload` | Event-specific data | The payload schema varies per event type but always represents a **state change**. # Responses and retry behavior *** ## Expected response *** Your endpoint must return an **HTTP 2xx** status to confirm successful delivery. | Response | Result | | -------- | ---------------------- | | 2xx | Delivered successfully | | Non-2xx | Retried automatically | ## Retry strategy *** The plugin retries failed deliveries automatically with **exponential backoff**: | Attempt | Delay | | ------- | --------- | | 1 | 1 second | | 2 | 2 seconds | | 3 | 4 seconds | **Defaults** * Max retries: 3 * Timeout per request: 30 seconds After all retries fail, the plugin moves the event to a **dead-letter queue** for operational follow-up. ### Custom retry settings You can customize retries and timeouts per event: ```bash theme={null} WEBHOOK_DICT_INFRACTION_REPORT_MAX_RETRIES=5 WEBHOOK_DICT_INFRACTION_REPORT_REQUEST_TIMEOUT=60s ``` ## Circuit breaker protection *** A **circuit breaker** protects webhook delivery and prevents cascading failures. When the Pix Plugin detects **repeated delivery failures** (typically consecutive `5xx` responses or timeouts), it temporarily **pauses webhook calls** to the affected endpoint. After a configurable cooldown period, the system performs **controlled retry attempts** to check if the endpoint has recovered. When the endpoint returns successful responses, the plugin resumes normal delivery automatically. This mechanism gives you: * Protection against overloaded or unstable endpoints * Graceful recovery without manual intervention * Higher overall system stability in production environments The circuit breaker works alongside retries and exponential backoff. It adds an extra safety layer for webhook delivery. ## Transport errors and orphan events *** When the plugin receives a refund webhook, it looks up the original transfer along the cash-in → cash-out chain. If no local source matches, the plugin persists the refund as an **orphan record** for BACEN auditability. It does not drop the refund, so the record stays visible for reconciliation and follow-up. If a source lookup fails at the transport layer, the plugin skips that source and continues. When no source resolves — from a clean miss or a swallowed transport error — the plugin records the refund as an orphan. The plugin aborts only when the transfer-lookup bridge is not configured. `originalEndToEndId` is the canonical key for all refund lookups. The plugin resolves refunds from both the cash-in → refund and cash-out → refund directions with this field. Always key refunds by `originalEndToEndId` (the original transfer's end-to-end ID), not by a single direction-specific lookup path. # Internal transaction reports (intra-PSP) *** The plugin settles intra-PSP (P2P) transfers internally. They never reach BTG for settlement, but the plugin still reports them to BACEN through the **TRCK002** abstraction. BTG confirms report status through a **CAMT025** webhook that carries the entity `PixInternalTransactionsReport`. | Field | Description | | -------------------------------- | --------------------------------------------------- | | `pactualId` | BTG-assigned report identifier | | `clientRequestId` | Your idempotency key, sent during report submission | | `entity` | Always `PixInternalTransactionsReport` | | `status` | `PROCESSING`, `CONFIRMED`, or `ERROR` | | `errorCode` / `errorDescription` | Populated when `status = ERROR` | The plugin updates the report status when the CAMT025 report webhook confirms or fails. The outbound webhooks fire earlier, when the intra-PSP transfer settles: `cashin.completed` for the cash-in leg and `cashout.completed` or `cashout.failed` for the cash-out leg. For the full internal flow, see [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp). # Best practices *** | Practice | Why it matters | | ------------------------- | ------------------------------------------------------------------------------------------------- | | **Ignore unknown fields** | Remain forward-compatible as new fields are added | | **Idempotent processing** | Use `Idempotency-Key` to avoid processing duplicates | | **Fast acknowledgment** | Return `202 Accepted` and process asynchronously | | **Async processing** | Avoid blocking the webhook thread | | **Handle compression** | Payloads >1KB are gzip-compressed. Check the `Content-Encoding` header and decompress accordingly | # Event examples *** Below are representative examples of webhook payloads you receive from the Pix Indirect Plugin. Expand each entry to view its payload. Ownership or portability lifecycle events. Use these to track Pix key disputes across institutions. ```json theme={null} { "entityType": "CLAIM", "flowType": "DICT", "payload": { "id": "claim-7f8a9b2c-1234-5678-abcd-ef0123456789", "key": "+5511999998888", "keyType": "PHONE", "claimType": "PORTABILITY", "claimer": { "ispb": "12345678", "name": "Banco Exemplo S.A." }, "donor": { "ispb": "87654321", "name": "Outra Instituição S.A." }, "status": "CONFIRMED", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T14:45:00Z" } } ``` Dispute and fraud signaling events aligned with BACEN MED rules. ```json theme={null} { "entityType": "INFRACTION_REPORT", "flowType": "DICT", "payload": { "id": "infraction-3e4f5a6b-7890-1234-cdef-567890abcdef", "endToEndId": "E12345678202401151030abcdefghij12", "infractionType": "FRAUD", "reportedBy": { "ispb": "12345678", "name": "Banco Exemplo S.A." }, "reportedAgainst": { "ispb": "87654321", "name": "Outra Instituição S.A." }, "status": "OPEN", "analysisResult": null, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z" } } ``` Refund requests and decisions related to MED cases. ```json theme={null} { "entityType": "REFUND", "flowType": "DICT", "payload": { "id": "refund-9a8b7c6d-5432-1098-fedc-ba0987654321", "endToEndId": "E12345678202401151030abcdefghij12", "infractionId": "infraction-3e4f5a6b-7890-1234-cdef-567890abcdef", "refundAmount": 150.00, "refundReason": "FRAUD", "status": "REQUESTED", "requestedBy": { "ispb": "12345678", "name": "Banco Exemplo S.A." }, "createdAt": "2024-01-16T09:00:00Z", "updatedAt": "2024-01-16T09:00:00Z" } } ``` Funds Recovery entity status changes. The plugin updates its local record before forwarding the full entity. ```json theme={null} { "entityType": "FUNDS_RECOVERY", "flowType": "DICT", "payload": { "id": "91d65e98-97c0-4b0f-b577-73625da1f9fc", "externalId": "ca1b9c01-ff9e-4a58-90ab-d31512e15ce0", "accountId": "01989f9e-6508-79f8-9540-835be49fbd0d", "status": "CREATED", "rootTransactionId": "E9999901012341234123412345678900", "situationType": "SCAM", "reporterParticipant": "99999010", "contactInformation": {}, "reportDetails": "Details to help receiving participants", "createdAt": "2020-01-17T10:00:00.000Z", "updatedAt": "2020-01-17T10:00:00.000Z" } } ``` Lifecycle events arrive as `entityType: FUNDS_RECOVERY_EVENT` (pass-through, no DB update), with `event` values such as `FUNDS_RECOVERY_ANALYSED` and `FUNDS_RECOVERY_COMPLETED`. Incoming and outgoing Pix transfer events. **Cash-in (incoming transfer):** ```json theme={null} { "entityType": "CASHIN", "flowType": "TRANSFER", "payload": { "id": "transfer-1a2b3c4d-5678-90ab-cdef-1234567890ab", "endToEndId": "E12345678202401151030abcdefghij12", "amount": 250.00, "payer": { "ispb": "87654321", "name": "João Silva", "cpfCnpj": "12345678901" }, "payee": { "ispb": "12345678", "name": "Maria Santos", "cpfCnpj": "98765432100", "accountNumber": "12345-6" }, "status": "SETTLED", "createdAt": "2024-01-15T10:30:00Z", "settledAt": "2024-01-15T10:30:05Z" } } ``` **Cash-out (outgoing transfer):** ```json theme={null} { "entityType": "CASHOUT", "flowType": "TRANSFER", "payload": { "id": "transfer-2b3c4d5e-6789-01bc-def0-2345678901bc", "endToEndId": "E87654321202401151045zyxwvutsrqp98", "amount": 500.00, "payer": { "ispb": "12345678", "name": "Maria Santos", "cpfCnpj": "98765432100", "accountNumber": "12345-6" }, "payee": { "ispb": "87654321", "name": "Empresa ABC Ltda", "cpfCnpj": "12345678000199" }, "status": "SETTLED", "createdAt": "2024-01-15T10:45:00Z", "settledAt": "2024-01-15T10:45:03Z" } } ``` Refund settlement events for Pix transactions. **Refund cash-in (receiving a refund):** ```json theme={null} { "entityType": "CASHIN", "flowType": "REFUND", "payload": { "id": "refund-4d5e6f7g-8901-23cd-ef01-4567890123cd", "originalEndToEndId": "E87654321202401151045zyxwvutsrqp98", "refundEndToEndId": "D12345678202401161000refund123456", "amount": 500.00, "reason": "CUSTOMER_REQUEST", "status": "SETTLED", "createdAt": "2024-01-16T10:00:00Z", "settledAt": "2024-01-16T10:00:02Z" } } ``` **Refund cash-out (sending a refund):** ```json theme={null} { "entityType": "CASHOUT", "flowType": "REFUND", "payload": { "id": "refund-5e6f7g8h-9012-34de-f012-5678901234de", "originalEndToEndId": "E12345678202401151030abcdefghij12", "refundEndToEndId": "D87654321202401161015refund789012", "amount": 250.00, "reason": "OPERATIONAL_FLAW", "status": "SETTLED", "createdAt": "2024-01-16T10:15:00Z", "settledAt": "2024-01-16T10:15:04Z" } } ``` # Key takeaway *** Webhooks are **not optional** in Pix Indirect operations. They are the **authoritative channel** for transaction state, refunds, and dispute handling. A correct webhook implementation gives you: * Accurate reconciliation * Regulatory compliance * Operational resilience * Predictable customer experience For production environments, always design webhook consumers as **idempotent, asynchronous, and observable systems**. # Next steps *** Now that you understand how webhooks work in Indirect Pix, explore these related topics: * [Pix main domains: transfers](/en/rails/pix/main-domains-transactions) - Deep dive into transfer operations * [Pix main domains: DICT](/en/rails/pix/main-domains-dict) - Understanding DICT operations and key management * [Pix main domains: MED](/en/rails/pix/main-domains-med) - MED dispute and refund handling * [MED 2.0 — Funds Recovery](/en/rails/pix/btg/indirect-pix-med-2-funds-recovery) - Cross-account fraud recovery and its webhooks * [Intra-PSP transfers](/en/rails/pix/btg/indirect-pix-intra-psp) - Internal P2P settlement and TRCK002 reporting * [API reference](/en/reference/midaz/plugins/indirect-pix/create-entry) — Full API documentation for DICT, Claims, Transactions, QR Codes, and MED operations # Direct Pix via JD Source: https://docs.lerian.studio/en/rails/pix/jd/direct-pix-via-jd Direct Pix via JD — participate as a direct participant on SPI and DICT through JD's regulated connectivity, using Lerian's unified Pix Switch API. **Direct Pix via JD** is the connectivity path for **direct participants** in Pix. JD's regulated infrastructure connects your institution to BACEN's SPI and DICT. Your institution owns its Pix participation. JD provides the certified connectivity that reaches BACEN's network. Lerian's Pix integration abstracts the connectivity provider behind a single API. The same Pix documentation applies to every provider: * **[What is Pix?](/en/rails/pix/pix-overview)** — the rail itself: modalities, SPI, DICT, and participation models. * **[Pix Switch](/en/rails/pix/pix-switch)** — the orchestration layer that handles DICT management, transactions, QR Codes, and MED through one API. * **[Plugin use cases](/en/rails/pix/pix-use-cases)** — end-to-end scenarios that combine Pix with fees, transaction routes, and the ledger. ## How Pix movements land in Midaz *** The direct participation plugin posts every settled Pix movement — cash-out, cash-in, refunds (devolução), MED effectuation legs, and Pix Automático settlement — to Midaz as a ledger transaction, with the external leg against the clearing account (for example, `@external/BRL`). Midaz records the accounting entry; the payer and payee identity (bank, branch, account, holder name and document, Pix key) and every BACEN reference live only in the plugin's own transaction record. Correlation between the two systems works through identifiers, not metadata: * The **end-to-end ID (E2E)** is the idempotency key of the Midaz posting, so a retried settlement can never double-post to the ledger. Follow-up legs of the same E2E — a devolução or a MED effectuation — derive their key from the E2E with a flow suffix, so they never collide with the original posting. * The plugin stores the **Midaz posting ID** on its own transaction record, and uses it to commit or cancel the pending debit of a two-phase cash-out. To see the banking detail behind a ledger entry, query the plugin's transaction API by the plugin transaction id or the E2E ID. The Midaz transaction itself carries no counterparty data and no custom metadata — by design, the ledger stays pure accounting. Lerian provisions the provider-specific configuration for direct participation via JD with your integration. To set up direct Pix participation, [get in touch with our team](https://lerian.studio/contact). # Environment variables Source: https://docs.lerian.studio/en/rails/pix/jd/pix-jd-environment-variables Configure the Pix Direct via JD rail — JD OAuth and JDPI endpoints, dynamic QR code hosting with JWS, Midaz ledger binding, and notification settings. Pix Direct, via JD connects your ledger straight to the Pix arrangement through JD's DICT and SPI gateway. DevOps sets its behavior through environment variables at deploy time. To change a variable, restart the service. This page covers the variables **distinctive to this rail**. For the datastore, multi-tenancy, streaming, telemetry, and authentication knobs that every Lerian Go service shares, see [BYOC configuration essentials](/en/reference/byoc-configuration). In the tables below, the **Default / Required** column shows the default value. A bold qualifier (for example **Required**) marks variables you must set. `—` means no default. `🔒` marks a **secret** — inject it at deploy time from your secret store, and never commit it. This page lists variable names and behavior only. It prints no secret values. ## Server and port The service listens on the address in `SERVER_ADDRESS` (default `:8080`). The liveness, readiness, and version probes bind this same port. See [Server](/en/reference/byoc-configuration#server) for the shared server knobs and [Default network ports](/en/reference/default-network-ports). ## JD integration Credentials and endpoints for JD's OAuth-secured API and its Pix (JDPI) surface. | Variable | Default / Required | Description | | -------------------------- | -------------------- | -------------------------------------------------------- | | `JD_BASE_URL` | **Required** | Base URL of the JD API. | | `JD_CLIENT_ID` | **Required** | OAuth client ID for the JD API. | | `JD_SECRET` | 🔒 **Required** | OAuth client secret for the JD API. | | `JD_GRANT_TYPE` | `client_credentials` | OAuth grant type used against JD. | | `JD_BANK_ID` | **Required** | Your institution identifier at JD. | | `JD_USE_SERVICE_SEGMENTS` | `false` | Route calls through JD service segments when `true`. | | `JD_PIX_URL` | **Required** | Base URL of the JD Pix (JDPI) API. | | `JD_PIX_CLIENT_ID` | **Required** | OAuth client ID for the JD Pix API. | | `JD_PIX_CLIENT_SECRET` | 🔒 **Required** | OAuth client secret for the JD Pix API. | | `JDPI_MAX_RETRIES` | `2` | Retry attempts on a failed JDPI call. | | `JDPI_RETRY_BASE_DELAY_MS` | `100` | Base backoff delay in milliseconds between JDPI retries. | ## Dynamic QR code hosting The rail hosts signed (JWS) dynamic-QR payloads and the JWK set used to verify them. The plugin serves those documents under the host in `QRCODE_PUBLIC_BASE_URL`. Set a bare host (FQDN) with no URL scheme, because JDPI rejects a scheme. | Variable | Default / Required | Description | | -------------------------- | -------------------------- | ------------------------------------------------------------------------------------ | | `QRCODE_PUBLIC_BASE_URL` | **Required** | Scheme-less host (FQDN) where the plugin serves dynamic-QR payloads and the JWK set. | | `QRCODE_PAYLOAD_PATH` | `v1/qrcodes/payload` | Path segment where the plugin exposes signed QR payloads. | | `QRCODE_JWK_PATH` | `v1/qrcodes/jwks` | Path segment where the plugin exposes the JWK set. | | `QRCODE_JWS_CONTENT_TYPE` | `application/jose` | `Content-Type` returned for the signed payload. | | `QRCODE_JWKS_CONTENT_TYPE` | `application/jwk-set+json` | `Content-Type` returned for the JWK set. | ## Midaz ledger binding Which Midaz organization, ledger, asset, and external account this rail books Pix movements against, plus the ledger service endpoints and machine-to-machine credentials. | Variable | Default / Required | Description | | ----------------------- | ------------------ | --------------------------------------------------------- | | `MIDAZ_ORGANIZATION_ID` | **Required** | Midaz organization UUID that owns the Pix ledger. | | `MIDAZ_LEDGER_ID` | **Required** | Midaz ledger UUID for Pix bookings. | | `MIDAZ_ASSET_ID` | `1` | Asset (currency) booked for Pix operations. | | `MIDAZ_EXTERNAL_ID` | **Required** | External account used as the Pix settlement counterparty. | | `MIDAZ_URL_ONBOARDING` | **Required** | Midaz onboarding service URL. | | `MIDAZ_URL_TRANSACTION` | **Required** | Midaz transaction service URL. | | `MIDAZ_AUTH_ADDRESS` | — | Access Manager URL for Midaz M2M tokens. | | `MIDAZ_CLIENT_ID` | — | OAuth client ID for Midaz M2M. | | `MIDAZ_CLIENT_SECRET` | 🔒 — | OAuth client secret for Midaz M2M. | | `MIDAZ_TIMEOUT` | `30000` | Midaz request timeout in milliseconds. | ## Transaction and operation routes The plugin maps Pix cash-in, cash-out, reversal, and intra-PSP flows onto named Midaz transaction and operation routes. Set one value per flow so the plugin books each Pix event onto the correct route. | Variable | Default / Required | Description | | ---------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------- | | `TRANSACTION_ROUTE_CASHIN` · `TRANSACTION_ROUTE_CASHIN_QRCODE` · `TRANSACTION_ROUTE_CASHIN_REVERSAL` | **Required** | Transaction-route IDs for the cash-in family (standard, QR code, reversal). | | `TRANSACTION_ROUTE_CASHOUT` · `TRANSACTION_ROUTE_CASHOUT_REVERSAL` | **Required** | Transaction-route IDs for the cash-out family. | | `TRANSACTION_ROUTE_INTRAPSP` · `TRANSACTION_ROUTE_INTRAPSP_REVERSAL` | **Required** | Transaction-route IDs for intra-PSP transfers and their reversals. | | `OPERATION_ROUTE_CASHIN_*` | **Required** | Operation-route IDs for each cash-in leg (credit, debit, QR code, external, reversal variants). | | `OPERATION_ROUTE_CASHOUT_*` | **Required** | Operation-route IDs for each cash-out leg (credit, debit, external, reversal variants). | `OPERATION_ROUTE_CASHIN_*` and `OPERATION_ROUTE_CASHOUT_*` stand for exactly twelve concrete per-leg route-mapping variables (eight cash-in, four cash-out). Each maps one pipeline leg to a Midaz operation-route ID — set each one explicitly: **Cash-in (8):** `OPERATION_ROUTE_CASHIN_CREDIT`, `OPERATION_ROUTE_CASHIN_CREDIT_QRCODE`, `OPERATION_ROUTE_CASHIN_CREDIT_REVERSAL`, `OPERATION_ROUTE_CASHIN_DEBIT`, `OPERATION_ROUTE_CASHIN_DEBIT_QRCODE_EXTERNAL`, `OPERATION_ROUTE_CASHIN_DEBIT_REVERSAL_EXTERNAL`, `OPERATION_ROUTE_CASHIN_REVERSAL_CREDIT`, `OPERATION_ROUTE_CASHIN_REVERSAL_DEBIT` **Cash-out (4):** `OPERATION_ROUTE_CASHOUT_CREDIT_EXTERNAL`, `OPERATION_ROUTE_CASHOUT_CREDIT_REVERSAL`, `OPERATION_ROUTE_CASHOUT_DEBIT`, `OPERATION_ROUTE_CASHOUT_DEBIT_REVERSAL_EXTERNAL` ## Jobs and limits | Variable | Default / Required | Description | | ---------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------ | | `JOBS_CRON` | `*/10 * * * * *` | Cron expression for the reconciliation/housekeeping job. | | `JOBS_CRON_TRANSACTIONS` | `*/10 * * * * *` | Cron expression for the transaction-processing job. | | `JOBS_RECONCILE_STUCK_THRESHOLD_SEC` | `80` | Age in seconds after which a pending cash-out is flagged as stuck. | | `TRANSACTION_LIMIT_DAILY_PERIOD_INIT` · `TRANSACTION_LIMIT_DAILY_PERIOD_END` | — | Start and end of the daily window used for transaction-limit accounting. | | `MAX_PAGINATION_LIMIT` · `MAX_PAGINATION_MONTH_DATE_RANGE` | `100` · `3` | Upper bounds on list page size and date-range span. | ## Notifications Optional end-customer notifications for Pix events. Leave the provider blocks unset to disable that channel. | Variable | Default / Required | Description | | ------------------------ | ------------------ | ----------------------------------------------- | | `SENDGRID_API_KEY` | 🔒 — | SendGrid API key for email notifications. | | `SENDGRID_FROM_EMAIL` | — | Sender address for email notifications. | | `SENDGRID_FROM_TEMPLATE` | — | SendGrid template ID used for the message body. | | `TWILIO_ACCOUNT_SID` | 🔒 — | Twilio account SID for SMS notifications. | | `TWILIO_AUTH_TOKEN` | 🔒 — | Twilio auth token for SMS notifications. | | `TWILIO_PHONE_NUMBER` | — | Sender phone number for SMS notifications. | ## CRM | Variable | Default / Required | Description | | ------------------- | ------------------ | ----------------------------------------- | | `CRM_URL` | — | CRM service URL for counterparty lookups. | | `CRM_CLIENT_ID` | — | OAuth client ID for CRM M2M. | | `CRM_CLIENT_SECRET` | 🔒 — | OAuth client secret for CRM M2M. | ## Runtime configuration (systemplane) This rail mounts the systemplane admin API on its main port, gated by `SYSTEMPLANE_ENABLED`. | Variable | Default / Required | Description | | --------------------- | ------------------ | ------------------------------------------------------- | | `SYSTEMPLANE_ENABLED` | `false` | Enable the systemplane runtime-configuration admin API. | When enabled, the service exposes an authenticated plane for reading and writing runtime configuration. See [Systemplane](/en/reference/systemplane/overview) for the API, namespaces, and required permissions. ## Health and readiness The rail exposes `GET /health` (liveness) and `GET /readyz` (readiness) on the main port, plus `/metrics` and `/version`. When you enable multi-tenancy (`MULTI_TENANT_ENABLED=true`), the rail adds an auth-gated per-tenant probe at `GET /readyz/tenant/{id}`. See [Health and readiness](/en/reference/health-and-readiness) for the response shape and startup/drain behavior. # DICT Source: https://docs.lerian.studio/en/rails/pix/main-domains-dict Learn how DICT, BACEN's Pix key directory, handles CPF, email, phone, and EVP key registration, portability claims, and account ownership. The **DICT (Directory of Transactional Account Identifiers)** is the national registry of the Central Bank of Brazil (BACEN). It manages Pix keys and provides secure, interoperable addressing for the Pix ecosystem. Pix keys let users receive payments with simple identifiers, such as a phone number or email. Users do not expose full banking details. DICT keeps these keys **unique, auditable, portable, and correctly linked to the legitimate account holder**. This page gives an overview of DICT: key types, lifecycle, claims, validation rules, and the compliance rules for institutions that integrate Pix. # What DICT does *** DICT is the backbone of Pix’s usability and security. It provides: ### Addressing Maps each Pix key to a specific transactional account. ### Uniqueness Prevents duplicate registrations across the entire Brazilian financial system. ### Portability Lets users move their keys between institutions and keep the same identifier. ### Ownership integrity Supports dispute flows when a key links to the wrong user. ### Regulated lookup Institutions can securely fetch recipient information before they process a transfer. **One key, one account. Always.** In Pix, a Pix key links to **only one transactional account at a time**. You **cannot reuse the same key across multiple accounts or institutions at the same time**. DICT enforces this strict one-to-one rule. The rule is fundamental to: * prevention of fund misrouting * clear ownership * system-wide consistency and auditability DICT keeps Pix **simple for users and interoperable for institutions**. # Key types *** Pix keys are **unique identifiers** that link a user’s bank account to the Pix payment system. They let others send or receive instant payments without full account details. The sender uses an alias such as a CPF, email, phone number, or random key (EVP). BACEN defines four official key types: ## 1. CPF / CNPJ (Registration Number / Tax Identifier) *** ### What it is: Uses the customer’s **official tax identification number** (CPF for individuals, CNPJ for businesses) as the Pix key. ### Format: * **CPF:** 11 digits → 12345678901 * **CNPJ:** 14 digits → 12345678000195 ### Characteristics: * No confirmation required * One key per document * Easily identifiable but exposes personal data ### Validation: * Must match the account owner’s legal document * Follows BACEN validation algorithms ## 2. Email *** ### What it is: Links a **valid email address** to the customer’s Pix account. ### Format: * Up to 77 characters * Follows standard format → [user@domain.com](mailto:user@domain.com) * Case-insensitive ### Characteristics: * Requires **token confirmation** to the email address * Easy to share and remember * Does not expose personal data ### Registration flow: 1. The customer requests an email key through Pix Switch 2. The institution sends a confirmation token to the email address 3. The customer confirms the token 4. The key becomes active in DICT ### Validation: * Requires confirmation within **5 calendar days** * Token is unique per request ### Best for: * Freelancers or professionals (e.g., [payments@studio.com](mailto:payments@studio.com)) * Companies with multiple email addresses ## 3. Phone number *** ### What it is: Uses a **mobile number** in international format as a Pix key. ### Format: * E.164 international standard → +5511987654321 * +55 (country) + area code + number ### Characteristics: * Requires confirmation via **SMS token** * Widely recognized and easy to use * Doesn’t expose personal data ### Rules: * Accepts only **mobile phones** (no landlines) * Requires confirmation within **5 calendar days** * Can register multiple numbers per account ### Common use cases: * Small business owners or service providers * Customers who prefer simplicity (“Pix to my phone number”) ## 4. Random key (EVP) *** ### What it is: A **system-generated unique identifier** (UUID v4) for maximum privacy. ### Format: 123e4567-e89b-12d3-a456-426614174000 ### Characteristics: * No confirmation required * Does not reveal any personal or business data * Available instantly * Hard to memorize manually ### When to use: * Privacy-focused users * Large companies or franchises that need **separate receiving accounts** * One-off or temporary payment flows ### Rules: * Generated automatically by the provider * Unique and non-reusable * Up to **five keys total per account** across all types (BACEN regulation) ## Key limits and rules (BACEN Regulation) *** | Type | Limit | | -------------------------- | --------------------------------- | | Total Pix keys per account | **5** | | CPF / CNPJ | **1 per account** | | Phone | 1 per number | | Email | 1 per address | | EVP | Unlimited, within the 5-key limit | # Key lifecycle *** A Pix key follows a **standardized lifecycle** from BACEN. The lifecycle keeps consistency, security, and interoperability across all participating institutions. ### 1. Registration A user or institution registers a Pix key. The request associates an identifier — a CPF, email, phone number, or random key — with a specific transactional account. At this stage, the key enters a **pending state**. It waits for validation according to its type. ### 2. Confirmation Some Pix keys require explicit confirmation of ownership before they become active: * **Email / Phone number** → Confirmation token sent to the contact method * **CPF / CNPJ** → Automatically validated and confirmed * **EVP (Random key)** → Automatically generated and confirmed This step makes sure the identifier truly belongs to the user. ### 3. Activation Once confirmed, the Pix key becomes **active**. At this point, the key can: * Appear in DICT lookups * Receive Pix transfers * Work with QR Codes and payment flows ### 4. Update Certain changes may require **new validations or confirmations**. Examples include a change to the linked account or the ownership details. The exact requirement depends on the key type and the update. ### 5. Removal Two parties can remove a Pix key: * The user removes it voluntarily. * The institution removes it for compliance, account closure, or regulatory reasons. Once removed, the key becomes unavailable for new transactions. ### 6. Synchronization Institutions must continuously synchronize their local state with the **BACEN DICT**. This guarantees that: * Key status remains consistent across the ecosystem * Portability and ownership claims stay correct * No outdated or invalid keys remain active Ongoing synchronization is mandatory for regulatory compliance and operational reliability. Flow for creating a Pix key and registering it in DICT so key status stays consistent across the ecosystem # Portability & Ownership claims *** Pix key **claims** are official requests in the **BACEN DICT**. A claim modifies the ownership or association of a Pix key. They keep every key — phone, email, CPF, or CNPJ — correctly linked to its legitimate owner and financial institution. **Pix Switch** automates the claim process, from request to confirmation. It provides **secure ownership validation, institutional interoperability, and traceability** under the **BACEN DICT standard**. ## Claim types *** In the Pix ecosystem, a **claim** is a formal request in the **DICT**. A claim changes the ownership or association of a Pix key. There are two distinct types of claims. Each type serves a different purpose and follows specific business logic: ### Portability (`PORTABILITY`) **What it is:** Applies when a customer wants to **transfer an existing Pix key** — an email, phone number, CPF, or CNPJ — from one financial institution (**Bank A**) to another (**Bank B**). **Goal:** Keep the same Pix key identifier and change the linked institution. This is similar to **mobile number portability**. **Example:** A user closes their account at Bank A and opens a new account at Bank B. They want to keep the same phone number as their Pix key. → Bank B initiates a **portability claim** with DICT. → DICT notifies Bank A to release the key. → Once confirmed, ownership moves to Bank B. **Key points:** * Requires customer confirmation in the original institution. * Involves both institutions (old and new). * Result: the **key changes institution** but keeps the same identifier. ### Ownership (`OWNERSHIP`) **What it is:** Applies when a Pix key links to the wrong account or institution. This is a dispute over **who truly owns the key**. **Goal:** Correct an erroneous key registration so the legitimate owner regains control of the identifier. **Example:** The telecom provider recycles a phone number that belonged to a user. When a new user registers the same number, DICT identifies that the number already belongs to someone else. → The institution initiates an **ownership claim** to check and correct the key’s rightful owner. **Key points:** * Focused on **ownership integrity**, not migration. * Starts from a system validation or a user report. * May require verification documents or confirmation from both parties. * Result: the **key remains in the same institution**, but ownership moves to the rightful owner. ### Summary table | **Claim Type** | **Purpose** | **Triggered by** | **Result** | | --------------- | ----------------------------------------------- | -------------------------------------- | ------------------------------------------------------ | | **PORTABILITY** | Move key from one institution to another. | Customer request at new institution. | Key transferred to new institution. | | **OWNERSHIP** | Correct ownership when a key is wrongly linked. | Conflict detection or customer report. | Key remains in same institution, with corrected owner. | Decision path for choosing between a portability claim and an ownership claim when moving or correcting a Pix key ## Claim flow — Portability & Ownership *** Pix **claims** manage key reassignment and portability requests within **BACEN’s DICT**. They keep every Pix key correctly linked to its legitimate owner and financial institution. **Pix Switch** handles two types of claims: * **Portability:** transfers a Pix key between institutions and keeps the same holder. * **Ownership Claim:** corrects the key’s ownership when it links to the wrong user. Both processes share the **same DICT API flow and endpoints**. They differ only in **business logic** and **validation rules**. ### Participants * **Claimant Institution (Bank B):** initiates the claim — either to transfer a key (Portability) or to correct ownership (Ownership Claim). * **Current Institution (Bank A):** receives the DICT notification and must confirm or reject the request. ### Scenario 1 — You are the Claimant Institution (Bank B) In this case, the customer belongs to **Bank B** (you), which will receive the key. Bank B creates, tracks, and cancels the claim when necessary. **1. Create Claim** The customer requests a Pix key change. The change transfers the key from another institution or corrects its ownership. Bank B initiates the claim in DICT. **Result:** * The claim starts with status OPEN. * DICT notifies **Bank A** (current owner) to validate or reject the request. **2. Monitor Claim status** Bank B tracks the claim lifecycle through periodic queries or webhook notifications from DICT. Possible transitions: OPEN → WAITING\_RESOLUTION → CONFIRMED → COMPLETED **3. Complete Claim** After approval, DICT finalizes the process and synchronizes the change across institutions. * For **Portability**, the key becomes inactive in Bank A and active in Bank B. * For **Ownership Claim**, the key remains in the same institution, but ownership moves to the legitimate owner. **4. Cancel Claim (optional)** If the customer or institution decides not to proceed, they can cancel the claim while it waits for resolution. How Bank B initiates a Pix key claim, from requesting it through DICT to its approval, completion, or optional cancellation ### Scenario 2 — You are the Current Institution (Bank A) In this scenario, the customer belongs to **Bank A** (you). Bank A currently holds the key that another institution requests. Bank A must validate the claim request from DICT. **1. Receive notification** DICT notifies **Bank A** about the claim request from **Bank B**. The claim appears with status WAITING\_RESOLUTION. **2. Validate Claim** When DICT notifies Bank A of an incoming claim, the institution must check the request. The claim type sets the next step. It may require **customer confirmation** or **document review**. **Result:** * If approved → DICT transfers the key to Bank B. * If denied → the claim status becomes CANCELLED. **3. Completion** After approval, DICT finalizes the process and synchronizes the change across institutions. * For **Portability**, the key becomes inactive in Bank A and active in Bank B. * For **Ownership Claim**, the key remains in the same institution, but ownership moves to the legitimate owner. How Bank A, the institution currently holding the key, handles an incoming Pix key claim through to completion and cross-institution synchronization # Security requirements (BACEN + Market Standards) *** Institutions must enforce: * End-to-end encryption of key data * Rate limits for registration attempts * Token expiration and one-time usage * Strict validation of CPF/CNPJ, phone, and email * Internal audit logs for all DICT operations * Event tracking for confirmation, cancellation, and deletion Institutions must minimize PII exposure in logs and user interfaces. # Synchronization & Reconciliation *** DICT requires strong consistency between institutions and BACEN. Institutions must implement: ### Periodic Sync Pull updates for all keys that belong to the institution. ### Event-Driven Sync Listen to DICT notifications for: * Key activation * Key deletion * Claim creation/resolution ### Conflict Resolution If a mismatch appears: * Institution must update its records * The institution must replace older or conflicting data * Audit logs must record the transition # Use cases *** ### Registering a Pix Key A freelancer registers an email key for business payments. ### Porting a Key A user moves a phone number key from one institution to another during a bank switch. ### Correcting Ownership The institution reassigns a recycled mobile number to the new legitimate owner. ### Removing an Old Key Business updates its EVP keys for accounting segmentation. **Regulatory reference** This page gives a practical overview of how Pix works. For deeper technical, legal, and regulatory details, always refer to the [official documentation](https://www.bcb.gov.br/estabilidadefinanceira/pix-normas) from the **Central Bank of Brazil (BACEN)**. It covers rule changes, deadlines, and official requirements. BACEN’s materials are the authoritative source for Pix regulations. They contain the most complete and current specifications. # MED Source: https://docs.lerian.studio/en/rails/pix/main-domains-med Learn how the Pix MED process handles fraud, unauthorized transactions, and operational errors under BACEN deadlines, with pacs.004 refunds through SPI. MED (Mecanismo Especial de Devolução) is the special refund mechanism of the **Central Bank of Brazil (BACEN)**. It defines how institutions investigate a disputed Pix transaction, communicate with each other, decide the outcome, and execute a refund. MED is mandatory and follows one standard. It protects the consumer and keeps the process consistent and auditable across every institution. # When MED applies *** A Pix transaction enters the MED process when a financial institution needs a **formal, regulated investigation**. Common cases include: * **Fraud** (phishing, account takeover, social engineering) * **Unauthorized transactions** (the user did not approve the payment) * **Operational errors** (duplicate sends, wrong recipient, wrong amount) * **System or processing failures** that cause financial impact MED runs **independent** of the standard Pix refund flows. A standard refund is voluntary. MED is mandatory and follows BACEN rules. # Regulatory MED lifecycle *** Every MED case runs as an **infraction report** with a fixed set of states. The report opens as `OPEN`, moves to `ACKNOWLEDGED`, and ends as `CLOSED` or `CANCELLED`. Each state carries its own BACEN deadline and response requirement. ### Lifecycle overview Stages and transitions of the Pix MED infraction-report lifecycle: OPEN, ACKNOWLEDGED, CLOSED, and CANCELLED # Integration points *** MED is a regulatory process, but it works with the core Pix components: | Component | Role | | ----------------------- | --------------------------------------------------------------- | | **DICT** | Provides fraud markers, key metadata, and ownership information | | **SPI (pacs.004)** | Executes the refund movement between institutions | | **Midaz Ledger** | Records the balance movements when a refund settles | | **Pix Switch services** | Coordinate the case lifecycle, deadlines, and notifications | # Compliance expectations *** An institution that runs MED must: * Meet **every regulatory deadline** * Keep a full **audit trail** with timestamps and event logs * Send standardized MED messages to the counterpart institution * Track each case status and its SLA * Notify the customer consistently * Use the correct dispute categories and reason codes # Relationship with refunds and reversals *** A **refund** and a **reversal** are standard Pix operations. MED provides the regulated mechanism for these cases: * The refund is tied to fraud or unauthorized activity * The originating institution disputes the transaction * The case needs extra evidence and regulated communication In these cases, the MED decision drives the refund. The institution does not process it as a voluntary action. # Infraction reports *** An infraction report is the formal way a Pix participant reports suspected fraud or unauthorized activity on a transaction. It is the entry point of the MED dispute-resolution process. ## When to file an infraction report A Pix participant files an infraction report when a customer reports fraud or unauthorized activity. The report names the counterparty and starts the investigation. Pix Switch receives the report as the counterparty. It can also cancel a report that it filed as the reporter. An infraction report carries a **situation type** that classifies the dispute: | Situation type | Meaning | | ------------------- | ------------------------------------------------------------------ | | `SCAM` | The customer was tricked into authorizing the payment | | `ACCOUNT_TAKEOVER` | A third party took control of the account | | `COERCION` | The customer paid under coercion | | `FRAUDULENT_ACCESS` | The payment came from unauthorized channel access | | `OTHER` | Another situation, described in the report details | | `UNKNOWN` | The situation is not one of the categories above (BACEN catch-all) | ## Time limits and deadlines An infraction report follows strict regulatory deadlines: | Rule | Time limit | | --------------------- | --------------------------------------------------------------------------------- | | Transaction age limit | A report applies only to a transaction from the **last 80 days** | | Analysis period | The counterparty has **7 calendar days** to analyze and close the report | | Refund request window | After an `AGREED` close, the reporter has **72 hours** to create a refund request | | Fund release | The blocked funds release when no refund request arrives within 72 hours | ## Infraction report lifecycle An infraction report moves through these statuses: | Status | Description | | -------------- | ----------------------------------------------------------------------------------- | | `OPEN` | The report is filed and waits for analysis | | `ACKNOWLEDGED` | The counterparty received and acknowledged the report | | `CLOSED` | Analysis is complete and the report closes with an `AGREED` or `DISAGREED` result | | `CANCELLED` | The reporter cancelled the report (from `OPEN`, `ACKNOWLEDGED`, or `CLOSED` status) | ## Closure and automatic fraud markers When an infraction report closes with an `AGREED` result, the system creates a **fraud marker** for the affected individual automatically. This links the investigation to the anti-fraud system and tracks the flagged individual across the Pix ecosystem. ## Available operations * **List**: Query infraction reports with filters (status, situation type, linked funds recovery) * **Retrieve**: Get the full details of one report * **Acknowledge**: Acknowledge an `OPEN` report as the receiving counterparty * **Close**: Submit the analysis with an `AGREED` or `DISAGREED` result * **Cancel**: Cancel a report you filed (from `OPEN`, `ACKNOWLEDGED`, or `CLOSED` status) # Refund requests *** A refund request returns funds to the original sender after the participant confirms an infraction. It is the financial execution step of the MED process. ## When to create a refund request You create a refund request after the participant investigates and closes an infraction report. The rules depend on the reason: | Reason | Prerequisite | | ------------------ | --------------------------------------------------------------------------------------------------- | | `FRAUD` | Needs a closed infraction report with an `AGREED` result. Create it within **72 hours** of closure. | | `OPERATIONAL_FLAW` | Needs no prior infraction report. Use it for a processing error or operational issue. | ## Time limits | Rule | Time limit | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Transaction age | Create a refund request within **80 days** of the original transaction (**30 days** if the original was itself a refund) | | Post-infraction window | For the `FRAUD` reason, create it within **72 hours** after the infraction closes | | Monitoring period | For a partial refund or an insufficient-balance rejection, monitor the account and process more partial refunds as funds arrive | ## Analysis outcomes When the counterparty analyzes a refund request, one of three outcomes applies: | Result | Description | | -------------------- | --------------------------------------------------------------- | | `TOTALLY_ACCEPTED` | The full refund is approved and executed | | `PARTIALLY_ACCEPTED` | A partial refund is approved, for example on insufficient funds | | `REJECTED` | The refund is denied, with a rejection reason | A rejection carries one of these reasons: `NO_BALANCE` (insufficient funds), `ACCOUNT_CLOSURE` (a closed account), `INVALID_REQUEST` (a request that does not meet the rules), `PARTICIPANT_EXCLUSION` (the participant is excluded from settlement), or `OTHER`. ## Available operations * **Create**: Open a new refund request for a transaction * **List**: Query refund requests with filters (status, refund reason, transaction, infraction report, participant) * **Retrieve**: Get the full details of one request * **Close**: Submit the closure decision with the analysis result * **Cancel**: Cancel a refund request before processing (only from `OPEN` status) # Fraud markers *** A fraud marker flags an individual (by CPF/CNPJ) and a Pix key as linked to fraudulent activity. It is a central part of the Pix anti-fraud ecosystem, managed through BACEN's DICT. ## How fraud markers are created A fraud marker starts in one of two ways: 1. **Directly**: A participant registers a fraud marker for a tax ID and a Pix key through the API 2. **Automatically**: When an infraction report closes with an `AGREED` result, the system creates a fraud marker for the affected individual ## Fraud classification types When an infraction report closes with `AGREED`, the reporter records a fraud classification: | Type | Description | | ------------------- | ------------------------------------------------- | | `APPLICATION_FRAUD` | Identity theft or a false application | | `MULE_ACCOUNT` | An account used to receive and move illicit funds | | `SCAMMER_ACCOUNT` | An account used to scam victims | | `OTHER` | A fraud type outside the specific categories | ## Fraud marker lifecycle | Status | Description | | ---------- | --------------------------------------------------------------------------- | | `ACTIVE` | The marker is active and visible in anti-fraud queries across the ecosystem | | `INACTIVE` | The marker is cancelled and no longer active | Only the participant that created a fraud marker can cancel it. A cancelled marker stays inactive. Create a new marker if you need one again. ## Available operations * **Create**: Register a new fraud marker for a tax ID and a Pix key * **List**: Query fraud markers * **Retrieve**: Get the full details of one marker * **Cancel**: Cancel a marker (only from `ACTIVE` status) # Statistics and risk assessment *** The MED statistics capabilities return aggregated anti-fraud and transaction data for risk assessment. This data helps an institution evaluate the risk of a person or a Pix key before it processes a transaction. ## What statistics are available Statistics come at two levels: ### Person statistics (by CPF/CNPJ) This level aggregates data for one individual or entity. The provider returns: * **Settlement history**: The count of settled transactions over each period * **Fraud markers**: Counts by fraud type * **Total fraud amounts**: The monetary impact of fraud-related transactions * **Infraction reports**: Open and rejected report counts * **Registered accounts**: The count of accounts with Pix keys linked to this person ### Key statistics (by Pix key) This level covers one Pix key, plus the current owner's statistics: * **Key-level data**: Settlement history, fraud markers, infraction reports, and account registrations for the key * **Owner-level data**: The full person statistics for the key's current owner ## Time periods The provider aggregates statistics over three time windows: | Period | Code | Description | | --------- | ----- | --------------------------------------------------------- | | 90 days | `d90` | Recent activity, most relevant for active fraud detection | | 12 months | `m12` | Medium-term view of behavior patterns | | 60 months | `m60` | Long-term historical context | ## Use cases * **Pre-transaction risk scoring**: Query the destination key statistics before you approve a cash-out * **Onboarding verification**: Check person statistics during account opening or key registration * **Monitoring dashboards**: Track fraud-marker trends across your portfolio * **Regulatory compliance**: Show due diligence in fraud prevention to auditors and regulators **Regulatory reference** This page gives a practical overview of Pix. For deeper technical, legal, and regulatory detail, always read the [official documentation](https://www.bcb.gov.br/estabilidadefinanceira/pix-normas) from the **Central Bank of Brazil (BACEN)**. It covers rule changes, deadlines, and official requirements. BACEN's materials are the authoritative source for Pix rules. They hold the most complete and current specifications. # Main domains Source: https://docs.lerian.studio/en/rails/pix/main-domains-overview Explore Pix's four core domains — DICT keys, transactions, QR Codes, and MED refunds — plus BACEN principles for interoperability, security, and settlement. Each domain owns a specific part of the instant payment flow. Together, the domains provide interoperability, security, and real-time execution across the ecosystem. This overview introduces the four core domains that form the foundation of Pix. Each child page in this section gives more detail. ## Core Pix domains *** ### DICT — Pix Key Directory The DICT is the national registry of Pix keys. It maps simple identifiers (CPF/CNPJ, phone, email, random key) to transactional accounts. Users send and receive Pix without full bank details. ### Pix Transactions This domain manages the execution lifecycle of an instant payment — authorization, validation, settlement, status updates, and traceability. Every transaction flows through regulated paths and must settle within seconds in the SPI infrastructure. ### QR Codes Pix supports static and dynamic QR Codes for payment initiation. Static codes are reusable. Pix generates a dynamic code for each transaction. The dynamic code can carry the amount, metadata, and expiration. ### MED — Special Refund Mechanism The *Mecanismo Especial de Devolução* governs fraud-related refund scenarios. It defines mandatory rules, timelines, and workflows for disputes, error handling, and fraud investigation across institutions. ## Additional Pix characteristics *** Pix also follows system-wide principles for safety, inclusiveness, and nationwide compatibility. ### Universal interoperability Every participant in Pix communicates through shared standards — no closed networks, no proprietary protocols. Any participant can send and receive Pix with any other. ### Regulatory standardization BACEN defines the rules, message formats, limits, fraud controls, and operational requirements. All participants must follow the same specifications. ### Open participation model Pix supports many types of institution: * Banks * Payment institutions * Fintechs * Digital wallets * Cooperatives * Government entities ### Security posture Pix enforces a strict security framework with: * Strong encryption * Standardized authentication and authorization * Mandatory fraud-prevention processes * Full auditability and traceability of every event # QR Codes Source: https://docs.lerian.studio/en/rails/pix/main-domains-qrcodes How Pix QR Codes work in Brazil — static BR Codes, dynamic immediate, cobrança with due date, Pix Automático recurrence, and Pix Saque withdrawal. Pix QR Codes let users pay across apps, websites, and physical points of sale. Every Pix participant reads the same format, so the codes work across institutions. Pix uses **EMV®-based structured payloads** that BACEN standardizes. Any institution can encode payment instructions in this scannable, machine-readable format. Pix Switch generates, decodes, and validates EMV Pix QR Codes, and it tracks each collection payment. # Types of Pix QR Codes *** Pix defines two primary categories of QR Codes: * **Static QR Code** → reusable, fixed structure * **Dynamic QR Code** → generated per transaction, with metadata, reconciliation, and expiration The sections below describe each type against BACEN standards and common market usage. # Static QR Code *** ### What it is A **reusable** Pix QR Code holds a fixed payload tied to a Pix key (email, phone, CPF/CNPJ, EVP). It does **not** expire. You can print, share, or display it, and it fits simple, low-volume payment scenarios. ### Characteristics * Permanent and reusable * Does not embed an amount by default (the payer types it) * Simplest form of Pix acceptance * Works offline (print, sticker, image) * Complies with the EMV Pix static format ### Typical use cases * Street vendors, cafés, kiosks * Professionals (delivery drivers, barbers, tutors, freelancers) * Donation campaigns * One payment point shared by many payers ### Static QR Code payment flow 1. The merchant creates the QR Code once. 2. The customer scans it with any Pix-enabled app. 3. The customer types the amount. 4. The payment follows the standard Pix flow: validation → SPI → credit. 5. Settlement happens instantly. ### Benefits * Easy to create and deploy * Reusable without limit * Works well in physical settings * Needs no extra infrastructure ### Limitations * No automatic reconciliation * The payer types the amount, which risks a wrong value * Cannot hold detailed metadata (order ID, invoice, and similar) # Dynamic QR Codes *** Each dynamic QR Code applies to one transaction and carries a structured payload with: * predefined amount * expiration time * reconciliation identifiers * metadata (order ID, description, payer details) They add automatic reconciliation and traceability, which suits digital commerce. Pix Switch generates dynamic immediate charges. It does not yet generate due-date, recurring, withdrawal, or change QR Codes. ## Dynamic (Immediate) *** ### What it is A one-time QR Code for **instant payments**, with: * a preset amount * a short expiration window * a unique transaction correlation ID ### Ideal for * E-commerce checkout * Delivery apps * POS systems * Restaurants (per table or command) ### Characteristics * Strong metadata and reconciliation support * Expires automatically * One-time use only * Real-time webhook confirmation ## Pix Cobrança (Dynamic with Due Date) *** ### What it is A dynamic QR Code for **payments with a due date**, similar to an invoice. It carries: * due date * interest and late fees * discounts or abatements * payer identification ### Ideal for * Schools and universities * Condominium or association fees * Subscription or recurring billing * Professional invoices ### Characteristics * Holds full billing information * Stays valid until expiration * Automatic reconciliation uses the charge ID ## Pix Automático (Recurrent) *** Pix Automático sets up **recurring collection authorizations**. The QR Code serves as the initial consent request for periodic debits. ### Ideal for * Subscriptions * Utility bills * Monthly services ## Pix Saque (Withdraw) *** ### What it is A QR Code that lets a customer **withdraw cash** from an authorized merchant. ### Ideal for * Retail stores that offer ATM-like convenience * Pharmacies, supermarkets, or convenience stores ## Pix Troco (Change) *** ### What it is A QR Code for when the customer pays **more than the purchase amount**. The customer receives the product and gets the difference back as a Pix. ### Ideal for * Supermarkets * Gas stations * Convenience stores # Payment flow (all QR types) *** Payment flow for a Pix QR code, covering both static and dynamic QR types from scan to settlement ## End-to-end Pix QR Code flow *** 1. **QR Code creation** The merchant or system creates a Pix QR Code, either static (reusable) or dynamic (transaction-specific). For a dynamic QR Code, the payload may hold a predefined amount, an expiration, and reconciliation identifiers. 2. **Payment initiation by the payer** The payer scans the QR Code with any Pix-enabled bank or wallet app. The payer reviews the payment details before confirmation. 3. **Pre-transaction validation by the payer’s institution** Before it sends the payment, the payer’s institution runs mandatory checks. These cover account status, available balance, transaction limits, and internal risk. 4. **Settlement through the Pix infrastructure** After approval, BACEN’s SPI routes the payment instruction for real-time settlement. If both accounts belong to the same institution, the institution may settle internally without crossing the SPI. 5. **Instant credit to the receiving institution** The receiving institution credits the funds to the beneficiary’s account right after settlement. This completes the payment. 6. **Confirmation and reconciliation** Pix Switch emits webhook notifications with the payment result to merchant or system backends. The backends then reconcile automatically, confirm the order, and update accounting. # Typical use cases *** ### Static * A bakery prints a single QR Code for the counter * A barbershop shares its QR Code on WhatsApp * A street vendor displays a laminated QR Code ### Dynamic Immediate * E-commerce checkout (one QR per order) * Restaurants that generate a code per table * Delivery apps that present a QR for each drop-off ### Dynamic Due Date * Schools that generate a monthly QR Code invoice * Condominium fees with interest or discount rules * Freelancers that issue billed invoices ### Withdraw / Change * A store offers Pix Saque at the counter * A customer pays R$ 120 for a R$ 98 purchase and gets R\$ 22 back as Pix Troco # Regulatory notes (BACEN) *** Pix QR Codes must comply with: * **EMV® Merchant-Presented Mode (MPM) specification** * Pix “Iniciação por QR Code” rules * Mandatory CRC validation * A defined field structure for dynamic charges * Expiration and billing rules for Cobrança (due-date QR) * Transaction limits and the night window (when it applies) **Regulatory reference** Always refer to the [official documentation](https://www.bcb.gov.br/estabilidadefinanceira/pix-normas) from the **Central Bank of Brazil (BACEN)** for technical, legal, and regulatory details. BACEN publishes rule changes, deadlines, and official requirements there. BACEN’s materials are the authoritative source for Pix regulations. They hold the most complete and current specifications. # Transactions Source: https://docs.lerian.studio/en/rails/pix/main-domains-transactions Follow a Pix transaction end-to-end — initiation, authentication, SPI settlement, E2E ID, and instant credit — plus cash-in and cash-out patterns. They represent the real-time movement of funds between accounts across all Pix participants, following the technical and regulatory standards of the Central Bank of Brazil (BACEN). Pix transactions target speed, traceability, and interoperability. They keep consistent behavior regardless of the institution or front-end channel. # Key characteristics *** Pix transactions share a common foundation across all institutions: ### Instant settlement Funds typically settle within **10 seconds**. SPI guarantees completion or handles the timeout transparently. ### 24/7 availability Pix operates continuously — including weekends, holidays, and outside business hours. ### Irrevocability After a Pix completes and credits the receiver, only regulated flows can reverse it (Refunds, Devoluções, MED). ### End-to-End ID (E2E) Each Pix transaction receives a unique E2E identifier for auditability, reconciliation, and dispute handling. ### Interoperability Any Pix participant can send or receive Pix from any other, independent of institution, platform, or channel. ### Configurable limits Institutions must enforce regulatory and internal limits for security and fraud control. # How a Pix transaction works (End-to-End Flow) *** End-to-end flow of a Pix transaction between the paying and receiving institutions A simplified cross-institution view of a Pix transaction follows: 1. **Initiation** The user starts a Pix transfer using one of the available addressing methods: * Pix key * QR Code (static or dynamic) * Manual bank details * Copy-and-paste EMV code To learn more about Pix QR codes, please visit the [QR Codes documentation](/en/rails/pix/main-domains-qrcodes). 1. **Authentication & authorization** The sending institution must authenticate the user with strong mechanisms (e.g., biometrics, 2FA, device trust). User consent is mandatory. 2. **Validation & risk checks** Before it sends anything to SPI, the institution must validate: * Account condition (active, not blocked, KYC verified) * Balance availability * Limits (transaction, daily, nightly, monthly) * Pix key validity (if applicable) * Risk assessments (anti-fraud indicators, velocity checks) 3. **Settlement attempt (SPI)** The institution submits the transfer to **SPI (BACEN’s Instant Payment System)**, which: * Validates the message * Routes it to the receiving institution * Settles the transaction between institutions 4. **Credit to receiver** The receiving institution must credit the account **immediately**, even if: * It occurs outside business hours * Systems are under load * Amount is high (except for flagged anti-fraud cases) 5. **Notifications & E2E generation** * Both institutions receive the updated transaction status * The transaction receives a unique **E2E ID** * Webhooks or internal notifications update systems and users # Pix Transaction types *** Pix transactions fall into a few standardized categories governed by BACEN. ## 1. Cash-Out (Send Pix) The payer sends funds to another account using a Pix key, QR code, or bank details. **Flow (summary):** 1. User initiates transfer 2. Institution checks balance + limits + key 3. Institution sends the transaction to SPI 4. Receiver credits instantly 5. Both parties receive updates + E2E ID Common scenarios: * P2P transfers * Merchant payments * Bill splitting * Marketplace split payments (when paired with Pix Cobrança) ## 2. Cash-In (Receive Pix) The customer receives a Pix transfer from any other institution. **Flow (summary):** 1. SPI notifies the receiving institution 2. The institution must credit the account immediately 3. The institution dispatches notifications for reconciliation 4. The institution records the transaction with an E2E ID Common scenarios: * Salary or corporate disbursements * Merchant settlements * Recurring customer payments ## 3. Refund (Devolução) Regulated flow that allows institutions to return funds to the original sender. The following can trigger a refund: * Customer request * Merchant refunds * Duplicate payments * Operational errors * Fraud-related MED cases A refund must: * Use BACEN standard messaging * Link to the original transaction * Follow value and eligibility rules ## 4. Reversal (Chargeback / pacs.004) Used in special regulated cases — often tied to MED or operational corrections. Reversals follow strict BACEN rules and are **not** the same as merchant refunds. # Cash-out: two-phase transfer pattern *** The Lerian platform processes a Pix cash-out in a **two-phase pattern** that separates validation from fund movement. This design provides safety, compliance, and a better experience for both institutions and end users. ## Why two phases A Pix transfer involves multiple validations: key lookup, account verification, balance checks, limit enforcement, and anti-fraud evaluation. A single step that runs all of them creates risk — if something fails after partial execution, reversal becomes complex. The two-phase pattern solves this by splitting the process: 1. **Phase 1 — Initiate**: Validate everything without moving funds 2. **Phase 2 — Process**: Execute the fund transfer using the validated data This separation provides several advantages: * **Validation before commitment**: All checks (balance, limits, key validity, anti-fraud) happen before any money moves * **User confirmation**: The initiating application can display validated destination details to the user before they confirm the transfer * **Idempotency support**: The application can retry each phase independently, with no duplicate transfers * **Compliance**: The platform completes and records regulatory validations before fund movement begins ## How it works ### Phase 1: Initiate The initiation phase creates a transfer record. It retrieves and validates the destination information. No funds move at this stage. The platform supports three initiation types: | Type | Description | Input | | --------- | -------------------------------------------------- | -------------------------------------------- | | `MANUAL` | Full destination account details provided directly | Bank, branch, account number, document, name | | `DICT` | Destination resolved via Pix key lookup (DICT) | Pix key value (CPF, email, phone, EVP) | | `QR_CODE` | Destination extracted from a QR code payload | BR Code (EMV) string | For `DICT` and `QR_CODE` initiations, the platform queries DICT to resolve the destination account. The initiation returns the account holder name, document, and institution, which the application can display for user confirmation. The initiation returns a unique identifier and an expiration timestamp. The application uses this identifier in the next phase. ### Phase 2: Process The process phase takes the validated initiation and executes the actual fund movement: * Verifies the initiation is still valid (not expired, not already processed) * Debits the source account in the ledger * Submits the payment to the Pix settlement infrastructure (SPI) * Records the transfer with full traceability (end-to-end ID, timestamps, status) After processing begins, the transfer follows the standard Pix settlement flow. You cannot cancel it. The regulated refund process is the only way to reverse it. ## Lifecycle summary | Step | What happens | Funds moved? | | ----------------- | ----------------------------------------------------------- | :----------: | | **Initiate** | Validate destination, check key/QR, resolve account details | No | | **User confirms** | Application displays details, user approves the transfer | No | | **Process** | Debit source account, submit to SPI, credit destination | Yes | | **Complete** | End-to-end ID assigned, both parties notified | — | # Validation layers (Regulatory + Operational) *** Every Pix transaction goes through multiple validations before it reaches SPI: ### 1. Account checks * Account active * Not blocked * KYC complete ### 2. Funds availability * Sufficient balance * Temporary holds released ### 3. Pix key validation If the user pays with a key, the institution must fetch and confirm the recipient’s details from DICT. ### 4. Regulatory limits * Per transaction * Daily limit * Nightly limit (mandatory) ### 5. Risk & fraud checks Institutions may enforce: * Velocity checks * Device fingerprinting * Behavioral analytics * Known-fraud markers ### 6. Idempotency Each Pix transfer must have a unique transaction ID. This avoids duplicate transfers during retries. # Operational controls and safety *** ### Authentication requirements Strong customer authentication is mandatory for all Pix send operations. ### Notifications & Webhooks After crediting, institutions must notify systems and users. Webhooks allow real-time reconciliation. ### Error handling Common error categories include: * Timeout * Invalid key * Insufficient funds * Daily/nightly limit exceeded * Suspicious transaction blocked ### Auditability Pix mandates full traceability: * Logs * Timestamps * E2E ID * Account metadata # Transaction limits (BACEN Rules) *** Pix transaction limits exist to reduce fraud risk, protect end users, and maintain operational safety across the instant payment ecosystem. BACEN defines **minimum security standards**, and institutions may add restrictions depending on their risk appetite. Pix limits fall into two groups: **mandatory regulatory limits** and **institution-defined limits**. ## 1. Mandatory regulatory limits (BACEN) ### Nighttime limit (Per-Transaction Cap) Institutions **must** enforce a lower maximum limit between **20:00 and 06:00**. Regulatory requirements: * Default maximum: **R\$ 1.000** * Institutions may set a lower default (e.g., R$ 500 or R$ 200) * Customers may **reduce** this limit instantly * Customers may **increase** this limit **only after a minimum wait of 24 hours** * Customers may **adjust the nighttime window** (e.g., extend to 18h–08h) This rule applies to: * Pix transfer (cash-out) * Pix QR Code payment * Pix Saque/Troco * Pix Cobrança payments initiated at night Its purpose is to prevent fraud during higher-risk hours. ### Allowance for Customer-Controlled limits Institutions must offer customers the ability to **configure their own limits**, per: * Transaction * Day * Nighttime maximum * Other forms of risk-based segmentation These changes must respect BACEN timing rules: * **Reductions** → must take effect **immediately** * **Increases** → can only take effect **after a minimum of 24 hours** (security delay) ### Obligation to analyze requests for limit increase Every institution must: * Accept customer requests to increase limits * Evaluate them through risk criteria * Grant or deny based on fraud policy * Apply increases only **after the regulatory waiting period (24h+)** ### Saque/Troco limits (Cash-Out at merchants) Pix Saque and Pix Troco have their own caps: * Default: **R\$ 500** per transaction * Institutions may voluntarily offer up to **R\$ 3.000** per transaction * Customers may reduce this limit instantly * Increases follow the same 24h rule These limits do *not* affect normal transfers — they are specific to cash withdrawal modalities. ### Credit obligations (independent of limits) Even if a transfer exceeds the limits, after the receiver gets the **credit**: * The transaction becomes **irrevocable** * Only MED or refund flows can reverse it * Institutions cannot “undo” a completed Pix on their own initiative ## 2. Institution-defined limits (Configurable) Beyond mandatory rules, institutions may add their own controls, such as: ### Per-Transaction limit Maximum allowed amount for a single Pix operation. ### Daily limit Cap on total Pix amount per user per calendar day. ### Monthly limit (optional) Used mainly by corporate accounts or fintech risk models. ### Risk-based segmentation Institutions may offer: * Stricter limits for new customers (ex.: first 7 days) * Different limits for high-risk users or flagged accounts * Dynamic limits adjusted by customer behavior These internal controls must never conflict with BACEN requirements — only complement them. ## 3. Operational rules required by BACEN ### Immediate limit reduction Customers can lower their limits instantly. ### Delayed limit increase All increases must respect: * **24 hours minimum** before becoming effective * Optional additional waiting period (institution’s choice) ### Peak risk hours Nighttime limits apply specifically to **cash-out flows**, as they represent the main vector for fraud losses. ### Real-time enforcement Institutions must check limits *before* they send a transaction to SPI. The institution must reject a Pix that violates limits **locally**, before it reaches SPI. ## 4. Limit behavior in transaction lifecycle ### During a Pix initiation The sending institution must verify: * Available balance * Transaction limit * Daily limit * Nighttime limit * Saque/Troco limit (if applicable) * Fraud/velocity limits * Customer-configured limits If a transaction violates any rule, the institution must reject it with an appropriate code. ### During retries The institution must validate the limits again. Idempotency prevents duplicate transfers but does **not** bypass limit checks. ### During merchant flows Dynamic QR Codes must also respect: * Customer limits * Time-of-day rules * Saque/Troco caps Even if the merchant sets an amount, the payer’s institution decides whether to approve. ## 5. Recommended customer communication BACEN requires institutions to: * Explain limits clearly * Offer customer self-service to adjust limits * Show pending increases and activation dates * Notify customers in case of suspicious behavior ## 6. Summary table | Limit Type | Mandatory? | Default (BACEN) | Customer Controls | Activation Rules | | ------------------- | ---------- | ------------------- | --------------------- | ----------------------------------- | | Per-transaction | Optional | Institution-defined | Increase/reduce | Reduce = immediate / Increase = 24h | | Daily | Optional | Institution-defined | Increase/reduce | Reduce = immediate / Increase = 24h | | Nighttime (20h–06h) | **Yes** | **Max R\$ 1.000** | Customer customizable | Increase = 24h delay | | Saque/Troco | **Yes** | R\$ 500 default | Customer configurable | Increase = 24h delay | | Monthly | Optional | Institution-defined | Increase/reduce | Institution-defined | **Regulatory reference** This page provides a practical overview of how Pix works. For deeper technical, legal, and regulatory details — and to stay up to date with rule changes, 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)**. BACEN’s materials are the authoritative source for Pix regulations and contain the most complete and up-to-date specifications. # Midaz with Pix Transaction Routes Source: https://docs.lerian.studio/en/rails/pix/midaz-for-pix-transaction-routes Model reusable Pix transfer flows in Midaz with Transaction Routes and Operation Routes — enforce account and fee rules directly in the ledger. Every Pix transaction follows a pattern: debit the sender, credit the receiver, and sometimes collect a fee. When that pattern lives only in application code, every team that touches Pix re-implements the same validation logic. Each implementation is another chance for inconsistency. Transaction Routes move that pattern into the ledger. You define the rules once, and Midaz enforces them on every transaction. The result is a single source of truth for how Pix money flows through your system. This page walks through two scenarios — a simple peer-to-peer transfer and a transfer with a fee. Each scenario shows how to set up the routes and what your team gains from them. ## Why this matters *** For **product and operations teams**, Transaction Routes give you auditable Pix flows without application-level enforcement. Every transaction carries a reference to the route it followed, so compliance reviews and incident investigations stay simple. For **engineering teams**, routes remove repetitive validation code. You configure the account and fee rules once. Midaz then enforces them at the ledger level on every Pix integration. | Without Routes | With Routes | | ---------------------------------------------------------------- | ----------------------------------------------------------------- | | Each integration must enforce its own account rules | Define the rules once, reuse across all Pix transactions | | No automatic validation — constraints live in application code | Midaz rejects transactions that don't match the route's rules | | Adding fees requires changes across every Pix integration | Add a new Operation Route, create a new Transaction Route variant | | Hard to trace which pattern a transaction was supposed to follow | Every transaction stores its route ID — straightforward to audit | For a deeper look at how Transaction Routes and Operation Routes work, see [Accounting Routes](/en/midaz/transaction-routing-entities). ## Prerequisites *** Both scenarios assume a Midaz environment with the following structure already in place: | Entity | Alias | Type | Purpose | | --------------- | ----------------- | ---------- | ------------------------------------------------- | | Alice's account | `@alice_checking` | `checking` | Sender — Alice's main checking account | | Bob's account | `@bob_checking` | `checking` | Receiver — Bob's main checking account | | BRL asset | — | — | Brazilian Real, registered as the operating asset | Values in Midaz are decimal amounts. For BRL, `150.00` means R\$ 150.00. ## Scenario 1: Simple Pix transfer *** Alice sends R\$ 150.00 to Bob via Pix. The money moves from one checking account to another — no fees, no splits, just a clean peer-to-peer transfer. ### The goal * Debit Alice's checking account by R\$ 150.00 * Credit Bob's checking account by R\$ 150.00 * Validate that both accounts are of type `checking` before processing * Make this pattern reusable for every Pix transfer between checking accounts ### Setting up the routes This route defines the debit side of the transfer. The `account_type` rule accepts any account of type `checking` as a source. The route does not hardcode a specific sender. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/operation-routes { "title": "Pix - Debit Sender", "description": "Debits the sender's checking account in a Pix transfer", "code": "PIX-SEND-SRC", "operationType": "source", "account": { "ruleType": "account_type", "validIf": ["checking"] }, "metadata": { "payment_method": "pix", "direction": "outbound" } } ``` Save the returned `id` — you need it when you build the Transaction Route. This route defines the credit side. It uses the same rule type: any `checking` account qualifies as a valid receiver. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/operation-routes { "title": "Pix - Credit Receiver", "description": "Credits the receiver's checking account in a Pix transfer", "code": "PIX-SEND-DST", "operationType": "destination", "account": { "ruleType": "account_type", "validIf": ["checking"] }, "metadata": { "payment_method": "pix", "direction": "inbound" } } ``` Group both Operation Routes into a single Transaction Route. This route represents "Pix Transfer" in your system. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/transaction-routes { "title": "Pix Transfer", "description": "Standard Pix transfer between two checking accounts", "operationRoutes": [ "", "" ], "metadata": { "payment_rail": "pix", "spi_message_type": "pacs.008", "regulation": "BCB_PIX" } } ``` Replace the placeholder IDs with the actual Operation Route IDs from the previous steps. ### Executing a Pix transfer With the route in place, every Pix transfer references the Transaction Route ID in the `routeId` field. Midaz validates that the accounts match the route's rules before it processes the transaction. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/transactions/json { "chartOfAccountsGroupName": "PIX", "description": "Pix transfer from Alice to Bob", "code": "PIX-20260306-001", "routeId": "", "send": { "asset": "BRL", "value": "150.00", "source": { "from": [ { "accountAlias": "@alice_checking", "amount": { "asset": "BRL", "value": "150.00" }, "description": "Pix sent to Bob", "routeId": "" } ] }, "distribute": { "to": [ { "accountAlias": "@bob_checking", "amount": { "asset": "BRL", "value": "150.00" }, "description": "Pix received from Alice", "routeId": "" } ] } }, "metadata": { "pix_end_to_end_id": "E123456782026030614300000000001", "pix_key_type": "cpf", "pix_key": "123.456.789-00" } } ``` ### What happens under the hood The request carries the Transaction Route ID in the `routeId` field. Midaz loads the route configuration. For each `from` entry, Midaz checks the account against the source Operation Route rules. Alice's account is type `checking`, so it matches the `account_type` rule. Validation passes. For each `to` entry, Midaz checks the account against the destination Operation Route rules. Bob's account is type `checking`, so validation passes. Both validations pass, so Midaz creates the transaction atomically. It debits `@alice_checking` by R\$ 150.00 and credits `@bob_checking` by R\$ 150.00. If Alice sends from a `savings` account instead, Midaz rejects the transaction. The route accepts only `checking` accounts as sources, and you write no application-side validation. ## Scenario 2: Pix transfer with fee collection *** This flow matches Scenario 1, but now the bank charges a R\$ 1.50 fee on each Pix transfer. The flow adds a third Operation Route for the fee destination, and Alice's total debit rises to R\$ 151.50. ### What changes You already have the source and destination Operation Routes from Scenario 1. You add one Operation Route for the fee and a new Transaction Route that groups all three. | Entity | Alias | Type | Purpose | | ------------------- | ------------------- | --------- | ------------------------------------------------ | | Fee revenue account | `@revenue_pix_fees` | `revenue` | Internal account that collects Pix transfer fees | ### Setting up the fee route The previous routes use `account_type`. This one uses the `alias` rule type instead. It targets one specific account — `@revenue_pix_fees` — and no other account qualifies. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/operation-routes { "title": "Pix - Fee Collection", "description": "Credits the bank's revenue account with the Pix transfer fee", "code": "PIX-FEE-DST", "operationType": "destination", "account": { "ruleType": "alias", "validIf": "@revenue_pix_fees" }, "metadata": { "fee_type": "pix_transfer_fee" } } ``` This route groups the original source and destination routes with the new fee route. It is a separate Transaction Route from the simple transfer, so your system can offer both variants. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/transaction-routes { "title": "Pix Transfer with Fee", "description": "Pix transfer between checking accounts with fee collection", "operationRoutes": [ "", "", "" ], "metadata": { "payment_rail": "pix", "includes_fee": true } } ``` ### Executing a Pix transfer with fee Alice sends R\$ 150.00 to Bob. The bank collects R\$ 1.50. Alice's total debit is R\$ 151.50. ```json theme={null} POST /v1/organizations/{org_id}/ledgers/{ledger_id}/transactions/json { "chartOfAccountsGroupName": "PIX", "description": "Pix transfer from Alice to Bob (with fee)", "code": "PIX-20260306-002", "routeId": "", "send": { "asset": "BRL", "value": "151.50", "source": { "from": [ { "accountAlias": "@alice_checking", "amount": { "asset": "BRL", "value": "151.50" }, "description": "Pix sent to Bob + transfer fee", "routeId": "" } ] }, "distribute": { "to": [ { "accountAlias": "@bob_checking", "amount": { "asset": "BRL", "value": "150.00" }, "description": "Pix received from Alice", "routeId": "" }, { "accountAlias": "@revenue_pix_fees", "amount": { "asset": "BRL", "value": "1.50" }, "description": "Pix transfer fee", "routeId": "" } ] } }, "metadata": { "pix_end_to_end_id": "E123456782026030614300000000002", "fee_amount": "1.50" } } ``` **Result:** Midaz debits Alice R\$ 151.50. Bob receives R\$ 150.00. The bank collects R\$ 1.50. All in a single atomic transaction — fully balanced, fully auditable. ### What this unlocks * **Transparent fee collection** — the fee is a first-class ledger entry, not hidden metadata. Finance and compliance teams see exactly where the R\$ 1.50 went. * **Reusable building blocks** — the simple and fee variants share the source and destination Operation Routes. You add only what changes. * **Route-level control** — your system can offer both "Pix Transfer" and "Pix Transfer with Fee" as distinct products, each backed by its own Transaction Route. * **Easy evolution** — to add a percentage-based fee or a split across revenue accounts, create new Operation Routes and compose a new Transaction Route. Existing flows stay untouched. ## Understanding rule types *** The two rule types serve different purposes. The right choice depends on whether the account in a route is dynamic or fixed. | Rule type | `validIf` format | Behavior | When to use | | -------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `account_type` | **Array of strings** — e.g., `["checking"]` or `["checking", "savings"]` | Accepts any account that matches one of the specified types | Dynamic participants — the sender or receiver can be any account of that type | | `alias` | **String** — e.g., `"@revenue_pix_fees"` | Must target a specific account by its alias | Fixed participants — the route always hits the same account, like a fee or settlement account | You can combine both rule types within a single Transaction Route. Scenario 2 does exactly that: `account_type` for the dynamic sender and receiver, `alias` for the fixed fee account. ## What you need to get started *** | Requirement | Details | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Midaz** (v3.x.x+) | Core ledger with Transaction Route validation enabled | | **Route validation config** | Enable route validation via the Ledger Settings API: `PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings` with `{"accounting": {"validateRoutes": true}}` | | **Accounts and asset** | At minimum: two customer accounts and a BRL asset registered in the ledger | | **Operation Routes** | One per operation leg (source, destination, fee) | | **Transaction Route** | Groups the Operation Routes into a reusable pattern | You must enable Transaction Route validation for each ledger. See [Working with Accounting Routes](/en/midaz/transaction-routing-entities#working-with-accounting-routes) for the configuration steps. ## Next steps *** Understand how Operation Routes and Transaction Routes work at a deeper level. Learn about Midaz's double-entry transaction model and N:N capabilities. Combine the Pix Plugin with the Fees Engine for automated fee management. Explore the full Pix Plugin architecture and connection models. # Midaz with Pix and automated fees Source: https://docs.lerian.studio/en/rails/pix/midaz-for-pix-with-fees Launch Pix with automated fees by combining Midaz, the Pix Plugin, and the Fees Engine for cash-in and cash-out with full double-entry audit trails. Pix by itself is only the start. What turns a payment rail into a product is how you manage fees, split revenue, and keep a clear audit trail for every transaction. You combine **Midaz** (the ledger), the **Pix Plugin** (Pix connectivity), and the **Fees Engine** (automated fee logic, part of Midaz). Together they launch Pix operations with built-in fee management — no custom middleware, no manual reconciliation, and no gaps in your accounting. This page walks through two real scenarios where this combination delivers value. ## Why this combination matters *** Each component solves a specific layer of the problem: | Component | Role | | --------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **Midaz** | Core ledger — records every debit, credit, and fee movement with double-entry precision | | **Pix Plugin** | Pix connectivity — handles transaction orchestration, DICT, QR Codes, and settlement through your chosen participation model | | **Fees Engine** | Fee automation — calculates, splits, and routes fees based on configurable packages | Together, these three parts form a **closed-loop system**. Pix transactions arrive, the Fees Engine calculates fees automatically, and the ledger records every movement — gross, net, and fee amounts — in one auditable operation. ## Choosing your Pix participation model *** The Pix Plugin supports both participation models defined by BACEN. The fee integration and ledger flow on this page apply to both. Only the connection to BACEN differs. | | Indirect Pix | Direct Pix | | --------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | | **How it connects** | Through a Direct Participant (e.g., BTG) | Directly to SPI/DICT via a certified PSTI (e.g., JD) | | **Best for** | Fintechs, payment institutions seeking fast onboarding | Banks and regulated FIs with full operational autonomy | | **Regulatory burden** | Shared with the Direct Participant | Fully owned by the institution | | **Time to market** | Weeks | Longer, but with maximum control | | **Fees Engine integration** | Native | Native | | **Midaz integration** | Mandatory | Mandatory | **Indirect Pix via BTG** is production-ready. Your institution connects through BTG's infrastructure, so you skip ISPB certification and direct BACEN connectivity. **Direct Pix via JD** — explore its APIs and capabilities in the [API reference](/en/rails/pix/pix-switch). For a detailed comparison, see [Direct vs. Indirect Pix Plugin](/en/rails/pix/pix-switch). ## Scenario 1: Fintech launching Pix with automated fees *** A fintech plans to offer Pix to its customers. It wants to charge a flat fee on every incoming Pix (cash-in). The fee covers operational costs and adds revenue without extra complexity for the customer. The fee logic and ledger structure stay the same for both models. This holds whether the fintech is an indirect participant (via BTG) or a direct participant (via JD). ### The business goal * Offer instant Pix payments to end customers * Charge a transparent, configurable fee on incoming transfers * Record every fee in the ledger for compliance and financial reporting * Launch quickly, without custom fee logic or reconciliation pipelines ### Setting up with Midaz The fintech first structures its Midaz environment: Register the fintech as an Organization in Midaz. This is the top-level entity that owns all ledgers, accounts, and transactions. Create a Ledger to track all customer transactions and fee movements. The Pix Plugin and the Fees Engine share this ledger. Register **BRL** as the operating asset. All Pix transactions and fee calculations use this asset. Each customer gets an account under the ledger. These accounts receive Pix credits and follow the fee rules. Set up internal accounts to receive fee credits — for example, a "Pix Fee Revenue" account. The Fees Engine package configuration references these accounts. Use Segments to group customers by tier (e.g., Standard, Premium). This lets you apply different fee packages to different groups. ### Configuring the Fees Engine With the ledger structure in place, the fintech configures a fee package: * **Fee type**: Flat fee of R\$ 1.50 per incoming Pix * **Scope**: Applied to all cash-in transactions on the main ledger * **Routing**: The Fees Engine routes fee credits to the "Pix Fee Revenue" internal account * **Exemptions**: Add Premium-tier accounts to the waived accounts list The Fees Engine supports multiple calculation methods — flat fees, percentage-based fees, or a "max between" rule that applies whichever is greater. For this scenario, a flat fee keeps pricing predictable for customers. You configure fee packages once, and the Fees Engine applies them automatically. When a Pix transaction matches the package criteria (ledger, segment, amount range), the Fees Engine calculates and routes the fee without any manual step. ### How a transaction flows When a customer receives a Pix transfer, here's what happens behind the scenes: The sending institution initiates a Pix. Your connectivity provider (BTG for indirect, JD for direct) receives the transaction and notifies the Pix Plugin. The plugin validates the recipient: it checks that the account exists, is active, and can receive funds. The plugin also checks CRM data and the Pix key validity. After the plugin confirms the transaction, settlement starts. The plugin fetches the destination account details from Midaz. The Fees Engine evaluates the transaction against available fee packages. It finds a matching package and calculates the R\$ 1.50 flat fee. Midaz records the transaction with the fee distribution in a single operation: * Midaz credits the customer's account with the **net amount** (gross minus fee). * Midaz credits the fee destination account with **R\$ 1.50**. * Every movement is traceable, with double-entry precision. The customer sees the Pix credit in their account. The ledger records the fee transparently for reporting and reconciliation. ### What this unlocks * **No custom fee logic** — the Fees Engine handles calculation, routing, and exemptions. * **Accurate ledger** — every fee is a ledger entry, not just metadata. Auditors see the same numbers your finance team sees. * **Customer segmentation** — the Fees Engine exempts Premium customers automatically through waived accounts. * **Fee flexibility** — change fee values, add percentage-based fees, or introduce amount-based thresholds without changes to your transaction pipeline. ## Scenario 2: Marketplace with Pix and proportional fees *** A B2B marketplace connects buyers and sellers. Buyers pay via Pix, and the marketplace charges a service fee on each transaction before it distributes the rest to sellers. ### The business goal * Accept Pix payments from buyers * Charge a percentage-based service fee on each payment * Distribute the net amount proportionally across multiple sellers when a single payment covers goods from different sellers * Maintain a clear audit trail for every split and fee ### Setting up with Midaz The marketplace structures its Midaz environment to reflect the multi-party nature of its operations: Register the marketplace as an Organization in Midaz. Create a single Ledger to manage all payment flows — buyer payments, seller credits, and marketplace fees. Register **BRL** as the operating asset. Each seller gets an account in the ledger. When a buyer pays via Pix, Midaz distributes the net amount across the relevant seller accounts. An internal account (e.g., "Marketplace Service Fee") receives the fee portion of every transaction. Group sellers into segments (e.g., Standard Sellers, Partner Sellers) to apply different fee rates per group. ### Configuring the Fees Engine The marketplace configures a fee package with proportional distribution: * **Fee type**: 3% of the transaction value * **Scope**: Applied to all incoming Pix payments on the marketplace ledger * **Distribution**: The Fees Engine splits fees proportionally across source accounts when a single transaction covers multiple sellers * **Routing**: Fee credits go to the "Marketplace Service Fee" account * **Deductible**: The Fees Engine deducts the fee from the sellers' amount (`isDeductibleFrom: true`), so the buyer always sends the full amount ### How a transaction flows A buyer purchases R\$ 1,000 worth of goods from three sellers: | Seller | Share | Gross amount | | -------- | ----- | ------------ | | Seller A | 50% | R\$ 500.00 | | Seller B | 30% | R\$ 300.00 | | Seller C | 20% | R\$ 200.00 | The buyer scans a QR Code or uses a Pix key to send R\$ 1,000 to the marketplace. The connectivity provider processes the transaction and notifies the Pix Plugin. The Fees Engine calculates a 3% fee (R\$ 30.00) and distributes it proportionally: * Seller A: R$ 15.00 fee (R$ 485.00 net) * Seller B: R$ 9.00 fee (R$ 291.00 net) * Seller C: R$ 6.00 fee (R$ 194.00 net) Midaz creates the transaction with full distribution: * Seller A's account: credited R\$ 485.00 * Seller B's account: credited R\$ 291.00 * Seller C's account: credited R\$ 194.00 * Marketplace fee account: credited R\$ 30.00 * Total: R\$ 1,000.00 — fully balanced Each seller sees their net credit. The marketplace sees the fee revenue. Every number ties back to a ledger entry. ### What this unlocks * **Automated revenue split** — the Fees Engine handles proportional distribution across any number of accounts. * **Transparent pricing for sellers** — each seller sees the exact fee and the reason for it. * **Balanced ledger** — gross amount in equals the sum of all credits out. No rounding gaps, no manual adjustments. * **Configurable rates** — change the fee percentage, add flat minimums, or exempt specific partners — all through fee package configuration. ## What you need to get started *** To run this setup, your institution needs: | Requirement | Details | | ------------------------- | ------------------------------------------------------------------------------------------------- | | **Midaz** (v4.x.x+) | Core ledger for account management and transaction recording | | **Pix Plugin** | Pix connectivity — choose Indirect (via BTG) or Direct (via JD) based on your participation model | | **Fees Engine** | Automated fee calculation and routing — a Midaz Enterprise capability, deployed independently | | **CRM** | Customer and account data for Pix validation — part of Midaz | | **Infrastructure** | Self-managed cloud (BYOC) or on-premise environment | | **Connectivity contract** | Agreement with BTG (indirect) or JD/PSTI (direct) depending on your model | The Pix Plugin and the Fees Engine require a valid Lerian license — the Fees Engine under the Midaz Enterprise tier. For license details, see the [License documentation](/en/reference/lerians-license). ## Estimating before going live *** Before you process real transactions, use the Fees Engine estimate to check your fee configuration. The estimate lets you: * Preview the exact fee for a given transaction amount * Check that exemptions work as expected * Confirm proportional splits produce the correct distribution * Test edge cases like boundary amounts and waived accounts This helps most during onboarding, before you set final fee structures with business stakeholders. Use the [Estimate Transaction Fees](/en/reference/midaz/plugins/fees-engine/simulate-fees) endpoint to test specific fee packages without writing to the ledger. ## Next steps *** Understand fee packages, calculation rules, and routing options. Step-by-step guide to creating packages and processing fees. Compare participation models and choose the right plugin. Explore the full Pix Plugin architecture and connection models. # Best practices Source: https://docs.lerian.studio/en/rails/pix/pix-best-practices Best practices for reliable Pix integrations — idempotent endToEndId keys, webhook confirmation, HMAC signatures, and retry and observability patterns. Following the practices below helps ensure predictable behavior, operational safety, and compliance with BACEN’s expectations — especially under volume, network instability, or dispute scenarios. These recommendations apply to all Pix use cases: wallets, merchant payments, cash-outs, QR Code flows, recurring operations, and internal transfers. # 1. Use an idempotent payment key *** On the Pix payment flow, the BACEN `endToEndId` is your **idempotency key**. It is a unique, immutable 32-character identifier that you supply, or that Pix Switch generates. You can retry safely only when you own the value. Treat the `endToEndId` as your **idempotency key**: * If you retry a request after a timeout, **reuse the same `endToEndId`**. * If the first attempt already created the payment, Pix Switch replays the existing one. * If the first attempt failed before processing, the retry creates the payment exactly once. This prevents: * Duplicate ledger postings * Double debits * Manual operational fixes * Reconciliation inconsistencies **Never generate a new `endToEndId` for a retry.** For the header-level idempotency behavior of each Lerian product, see [Retries and idempotency](/en/reference/retries-idempotency). # 2. Rely on webhooks for final transaction status *** A `200 OK` from the API **does not guarantee the Pix was completed**. It only means the request entered the orchestration flow. The authoritative status is the webhook: * Display final status to the user **only** after webhook confirmation * Persist webhook status in your system * Handle both success and failure notifications Waiting for the webhook aligns your UI to **SPI-confirmed settlement**, reducing disputes and false positives. # 3. Validate webhook authenticity *** Every webhook includes an HMAC signature (e.g., `X-Signature`). Best practices: * Store the secret in a vault * Recompute the HMAC using the **raw request body** * Compare with the header * Reject and log mismatches This protects against: * Fake callbacks * Tampered payloads * Unauthorized requests hitting your endpoint Webhook security is a PSP-level requirement — treat it as such. # 4. Design for failure and retries *** Pix is instant. The network around it is not. Expect failures in: * PSTI / direct provider connectivity * Telecom / SMS delivery (for key confirmation) * Network timeouts * Internal ledger validations * Limit and anti-fraud checks (regulated) Recommended approach: * Implement **clear retry policies** (exponential backoff, controlled loops) * Never retry blindly * Surface actionable messages to the user * Log all failures with correlation IDs * Treat “pending” as a normal intermediary state A reliable Pix integration is resilient by design. # 5. Monitor pending transactions & background jobs *** A Pix can enter **PENDING** while waiting for: * Provider processing * SPI acknowledgment * Webhook delivery * Retry cycles Pix Switch runs background workers to guarantee eventual consistency: * Retry callbacks * Reconcile intermediate states * Detect stuck operations Your responsibilities: * Monitor pending transactions regularly * Configure alerts for excessive retry attempts * Integrate logs, metrics, and traces for observability **Pending ≠ failure**, but prolonged pending requires investigation. # 6. Keep Pix keys and customer data synchronized *** Because keys are tied to identity: * If a user changes phone/email → update or remove associated Pix keys * Keep CRM records aligned with DICT data * Remove outdated keys to avoid misrouting * For institutions using DICT: Ensure portability and ownership claims follow BACEN rules This reduces: * Payments to wrong recipients * MED cases due to incorrect keys * Support friction Consistency between CRM ↔ DICT ↔ Pix Switch is essential. # 7. Respect SLA expectations and time windows *** Pix settlement occurs in **up to 10 seconds**, but legal SLAs matter too. Design your UX to respect: * Maximum SPI tolerances * Nighttime limit adjustments (20:00–06:00) * Customer-driven limit reduction delays (immediate) * Customer-driven limit increases (may require authentication or waiting period) Always display: * “Processing…” while awaiting confirmation * “Try again” guidance for limit breaches Your UX must mirror Pix’s real regulatory behavior. # 8. Implement reconciliation and accounting validation *** Reconciliation closes the loop between: * Your system * Pix Switch * SPI settlement * Ledger postings (Midaz) Recommended reconciliation cycle: * Confirm every settled Pix using your webhook log * Match every Pix ID with a ledger posting * Compare daily summaries with provider/SPB outputs * Flag any mismatch for manual review This reduces operational noise and supports audit readiness. # 9. Test end-to-end with realistic flows *** Before going live, simulate: * Cash-in and cash-out * High-value limits * Invalid keys * Expired QR Codes * Refunds (inbound + outbound) * MED-related refund triggers * Webhook downtime * API timeout and retry patterns Also test **intra-ledger Pix** scenarios when both accounts exist in Midaz. Your validation checklist: * Correct ledger postings * Correct Pix status transitions * Proper webhook handling * Proper limit enforcement * Proper accounting behavior # 10. Prepare your support and operations teams *** Support teams should understand: * Pix timeframes (including night limit rules) * Difference between “initiated”, “pending”, “completed”, “refunded”, “failed” * How to read E2E IDs * How to track DICT issues * When MED applies vs normal refunds Clear support flows reduce user friction and avoid false MED disputes. # Pix glossary Source: https://docs.lerian.studio/en/rails/pix/pix-glossary Look up Pix terms and BACEN acronyms — DICT, SPI, ISPB, E2E ID, cobrança, portability, MED, and Direct and Indirect participant definitions. Pix is governed and standardized by the Central Bank of Brazil (BACEN). This glossary consolidates the most important concepts, infrastructure components, operational terms, regulatory definitions, and participant types used across the ecosystem. Use this page as a quick reference while reading Pix documentation or integrating Pix into your financial system. *** ## A *** ### Account Identifier The banking details used to receive or send a Pix without a Pix key. Includes: bank (ISPB), branch, account number, and account type (CACC, SVGS, SLRY, TRAN). ### Alias (Pix Key) See **Pix Key** below. *** ## B *** ### Bacen / BACEN Banco Central do Brasil — the regulatory authority responsible for designing, operating, and supervising Pix. ### SPI Gateway Certification A mandatory certification process required for participants that connect directly to the Pix infrastructure (SPI/DICT). Ensures cryptographic and operational compliance. *** ## C *** ### Cash-In A Pix received from an external sender into the institution’s account. ### Cash-Out A Pix sent from the institution to an external recipient. ### Charge (Pix Cobrança) Payment request issued via Pix, often with due dates, penalties, and interest rules. Used mainly for invoices and billing. ### Claim (Portability & Ownership) A regulatory workflow used to transfer or confirm ownership of a Pix key between institutions. There are two types: * **Portability** – moving a key to another institution * **Ownership Claim** – asserting ownership when a key is registered inconsistently Both require user approval and follow DICT rules. ### Confirmation Window The allowed time for a user to approve or reject a Pix key portability or ownership claim. ### CICC / Câmaras Interbancárias Clearing houses connected to BACEN systems. Some regulatory flows reference them. *** ## D *** ### DICT (Directory of Transactional Account Identifiers) BACEN’s official directory of Pix keys. Stores: * key type * owner (CPF/CNPJ) * associated account * creation & update history DICT enables sending money using simple identifiers rather than full account details. ### Direct Participant An institution that connects **directly** to SPI and DICT with its own certified infrastructure. ### Dynamic QR Code A one-time-use QR Code containing full payment details, including value, expiration, and transaction metadata. Supports Cobrança Pix and automated reconciliation. ### Due Date Charge (Cobrança com Vencimento) A Pix charge with a predefined due date and rules for penalties, discounts, and interest. *** ## E *** ### E2E ID (End-to-End Identifier) A unique identifier generated for every Pix transaction. Used for: * traceability * dispute resolution * reconciliation * fraud analysis *** ## F *** ### FRA (Fraud-Related Refund Request) A special refund mechanism triggered when a transaction is identified as fraudulent. Governed by strict deadlines and workflows. *** ## G *** ### Gateway (SPI/DICT Gateway) The secure communication channel used by direct participants to connect to BACEN’s Pix systems. *** ## I *** ### Immediate Pix A transfer settled instantly (typically \< 10 seconds) through SPI. ### Indirect Participant An institution that offers Pix through a **Direct Participant**, instead of connecting directly to BACEN. ### ISPB Identificador de Sistema de Pagamentos Brasileiro — the unique numerical identifier of every financial institution in Brazil. *** ## K *** ### KYC (Know Your Customer) Regulatory verification of customer identity used to prevent fraud and ensure DICT accuracy. *** ## L *** ### Limits (Pix Limits) BACEN-defined and institution-defined rules restricting the maximum value a user can send or receive within certain windows (daytime, nighttime, custom limits). *** ## M *** ### MED (Mecanismo Especial de Devolução) The Special Refund Mechanism for fraud or operational error cases. Allows institutions to: * request refunds * analyze fraud claims * return funds following mandatory rules ### Merchant Initiated Pix When a business triggers a Pix charge using static or dynamic QR Codes. *** ## O *** ### Ownership Claim A process for asserting that the customer is the rightful owner of a Pix key held by another institution. *** ## P *** ### Participant (Direct / Indirect) Institutions authorized by BACEN to offer Pix services. ### Payer The sender of a Pix transaction. ### Payee The receiver of a Pix transaction. ### PSTI (Provedor de Serviços de Tecnologia da Informação) BACEN-regulated technology provider that allows institutions to connect to the national financial network (RSFN) for Pix. Used when the institution does not maintain its own direct infrastructure. ### Portability (Key Portability) Moving a Pix key from one institution to another, with customer consent. ### Pix Key A unique identifier used as a shortcut for receiving payments. Types include: * CPF * CNPJ * Phone number * Email * EVP (random key) ### Pix Saque Withdrawal of physical cash using Pix at ATMs or merchants. ### Pix Troco Cash withdrawal combined with a purchase: customer pays more than the purchase amount via Pix and receives the difference in cash. *** ## Q *** ### QR Code — Static Single QR Code reused for multiple payments. The customer enters the value manually. ### QR Code — Dynamic Single-use QR Code containing full payment data. Automatically expires. *** ## R *** ### Refund (Devolução) Reversing a Pix transaction. Types include: * standard refund * partial refund * special refund (MED) ### Reconciliation Matching Pix events (sent, received, confirmed) across SPI, DICT, ledger, and internal records. ### RSFN (Rede do Sistema Financeiro Nacional) The secure national network used for communication with BACEN systems. *** ## S *** ### SPI (Instant Payment System) BACEN’s infrastructure that routes and settles Pix transactions in real-time. ### Static QR Code Reusable QR Code linked to a Pix key. Does not expire. ### Settlement The moment funds are effectively transferred between institutions in SPI. *** ## T *** ### Transaction ID Unique identifier created by the initiating institution to track a Pix payment. ### Transaction Status Positions include: * Pending * In Analysis * Completed * Refunded * Failed ### Transfer Window The time window that affects Pix limits (daytime/nighttime). *** ## V *** ### Validation Regulatory and operational checks performed before executing a Pix transaction, including limits, anti-fraud, key ownership, and account status. *** ## W *** ### Webhook Callback mechanism used to deliver real-time transaction status to client systems. # Jobs and retry logic Source: https://docs.lerian.studio/en/rails/pix/pix-jobs-and-retry-logic Recover Pix operations automatically with background jobs that retry failed events, reconcile refunds, and keep data consistent. The Pix plugin recovers operations automatically with background jobs and retries. If a provider call times out or an event fails to process, the plugin retries and reconciles the data without manual work. ## Automatic retries *** The plugin processes Pix events asynchronously. If it fails to process an event, the plugin retries in place, then captures the event to a dead-letter queue without loss. An operator replays a parked event, bounded to a maximum number of attempts. After the limit, the record stays parked for inspection and the plugin never drops it. The plugin retries outbound calls to the provider on 5xx responses and timeouts. Each retry uses exponential backoff with jitter. The plugin deduplicates events by ID for a fixed time window. A retried or duplicate event never applies twice, so the jobs stay idempotent. ## Refund reconciliation *** A refund can succeed at the provider but fail to record locally if the service crashes between the two steps. The refund reconciliation sweeper is a background safety net for this case. The sweeper runs on a timer and finds refunds still stuck in pending. For each one, it re-drives the idempotent steps — settle, confirm, or close — which the provider deduplicates by refund reference. It parks a row still at the create step for an operator, because a blind create replay could cause a double refund. ```mermaid theme={null} sequenceDiagram participant Sweeper participant Provider participant DB Sweeper->>DB: Find pending refund rows Sweeper->>Provider: Re-run the missing step alt Success Provider-->>Sweeper: confirmed Sweeper->>DB: Mark as done else Still failing Provider-->>Sweeper: error Sweeper->>DB: Record attempt and back off else Attempts exhausted Sweeper->>DB: Park row and raise alert end ``` Each failed attempt waits longer than the last, from one minute up to one hour. After a maximum number of attempts, the sweeper parks the record and raises an alert. It does not retry forever. The sweeper is optional and off by default. ## Configuration *** You can tune retry and reconciliation behavior with environment variables: | Variable | Description | Default | | :------------------------------------------- | :----------------------------------------------------------------------- | :------ | | `DLQ_REPLAY_MAX_ATTEMPTS` | Times an operator can replay a parked event before it stays parked | 3 | | `CONSUMER_DEDUP_TTL_SEC` | Deduplication window, in seconds, that keeps event processing idempotent | 3600 | | `REFUND_RECONCILIATION_SWEEPER_ENABLED` | Turns on the refund reconciliation sweeper | false | | `REFUND_RECONCILIATION_SWEEPER_INTERVAL_SEC` | Time, in seconds, between sweeps | 30 | | `REFUND_RECONCILIATION_SWEEPER_MAX_ATTEMPTS` | Attempts before the sweeper parks a record and raises an alert | 10 | ## What you need to do *** The plugin manages retries and reconciliation for you. The default settings fit most deployments. To keep operations healthy: * Use unique, traceable IDs for your transactions and accounts. * Monitor event delivery and transaction status. * Contact Lerian for job visibility or event replay support. # Pix modalities Source: https://docs.lerian.studio/en/rails/pix/pix-modalities Compare Pix modalities defined by BACEN — standard transfer, static and dynamic QR Codes, Pix Saque, Pix Troco, Pix Cobrança, and Pix Automático. Pix supports multiple modalities. They range from instant transfers to QR code payments, cash withdrawals, and invoice-based collections. Each modality has its own rules, requirements, and operational flows. The national payment system standardizes all of them. This page describes the core Pix modalities and how they operate within the BACEN regulatory framework. # 1. Pix Transfer (Standard transfer) *** Pix Transfer is the most common modality — an instant transfer between two accounts. The payer identifies the recipient with one of these: * Pix keys (CPF/CNPJ, phone, email, EVP) * Manual entry (bank, branch, account, and recipient name) * Copy-and-paste codes (BR Code string) **Use cases** * Person-to-person transfers * Merchant payments without QR codes * Internal and external account movements **Requirements** * Sender must authenticate * Recipient must be a Pix participant * Transaction must pass fraud and risk checks **Rules & limitations** * Instant settlement (seconds) * Available 24/7 * Daily or nightly limits may apply (risk-based) **Flow (simplified)** 1. The sender authenticates and initiates the payment. 2. The institution validates the account, balance, limits, and risk indicators. 3. SPI settles the transaction. 4. The receiver's account receives the credit instantly. # 2. Pix QR Code *** Pix supports QR codes for payment initiation. They follow BACEN's BR Code standard and fall into two categories: ## Static QR Code *** A reusable QR code that contains: * Fixed recipient information * Optional fixed amount * Optional additional reference **Typical uses** * Small merchants * Donation links * Informal commerce **Characteristics** * The same QR code serves all payments * The payer can edit the amount * Minimal update requirements ## Dynamic QR Code *** The recipient's PSP generates a dynamic QR code for **each transaction**. It provides more features and security. Two official types: ### Dynamic Immediate * One-time use * Predefined amount * Expiration timestamp * Additional data fields for reconciliation Use cases: e-commerce checkout, POS systems, service providers. ### Dynamic with Due Date * Support for future due dates * Multiplier fields * Penalties, fines, interest * Updates via additional Pix Cobrança APIs Use cases: billing, invoicing, utilities, subscription payments. **Rules & limitations** * QR codes follow BACEN's BR Code standard, based on the EMV QR Code specification * Every BR Code carries a CRC16 checksum in the CRC field. A dynamic QR code also references a payload that the payer's app retrieves from a URL # 3. Pix Saque and Pix Troco *** Pix Saque and Pix Troco enable cash withdrawal at participating merchants. ## Pix Saque *** The customer sends a Pix transfer to withdraw cash. Flow: 1. The customer initiates Pix Saque. 2. The merchant receives the Pix. 3. The merchant hands over the equivalent cash. Use cases: cash access in areas without ATMs. ## Pix Troco *** The customer pays a value *higher* than the purchase and receives the difference in cash. Example: item costs R$70 → customer sends R$100 → receives R\$30 in cash. **Requirements** * Merchant must opt into Pix Saque/Troco * Merchant must publish maximum withdrawal thresholds **Limits (BACEN)** * Daily and per-transaction limits may vary * Typically higher limits during daytime # 4. Pix Cobrança (Invoice-Based Pix) *** Pix Cobrança is Pix's equivalent of billing. It supports: * Charge requests with or without due dates * Automatic calculation of penalties, fines, and interest * Updates to charges * Cancellation and reconciliation flows Two formats: ### Cobrança Imediata (Immediate Charge) The payer must pay instantly. This format applies to POS or service scenarios. ### Cobrança com Vencimento (Due Date Charge) This format supports future dates. Use it for: * Utilities * Tuition * Subscriptions * Merchant billing **Rules** * Each charge must contain a unique transaction ID * Penalties must follow the configured rules * Expired charges accept payments only if the configuration allows it # 5. Pix Agendado (Scheduled Pix) *** *(If the institution supports it)* Pix Agendado allows the payer to schedule a Pix payment for a future date. Use cases: * Bill payments * Recurring transfers * Salary disbursements (internal) **Characteristics** * The institution must allow scheduling * The institution validates Pix Agendado at scheduling time and at execution time * The payer can cancel according to institutional rules # 6. Pix Automático (Automatic Pix) *** Pix Automático will support registered agreements. Under an agreement, the system debits the payer's account automatically. Expected features: * Mandate-based authorization * Automatic recurring debits * Pre-notification of due charges * Cancellation support Ideal for: * Subscriptions * Utility bills * Tuition * Insurance premiums # 7. Pix Internacional (Cross-Border Pix) *** Pix Internacional aims to enable instant payments between Brazil and partner jurisdictions. Expected characteristics: * Currency conversion * Risk and AML checks * SPI interoperability with partner infrastructures * Transparent end-to-end flow The characteristics above reflect the intended design of this modality. # 8. When to use each modality *** ### Use standard Pix transfer for: P2P transfers, small purchases, splitting bills. ### Use static QR for: Small merchants, service providers, simple collection. ### Use dynamic QR for: E-commerce, delivery, POS, billing. ### Use Pix Saque/Troco for: Extending cash withdrawal coverage. ### Use Pix Cobrança for: Recurring billing, utilities, formal invoicing. ### Use Pix Agendado for: Future payments without immediate settlement. ### Use Pix Automático for: Recurring agreements (subscriptions, utilities). ### Use Pix Internacional for: Cross-border instant transfers (when released). **Regulatory reference** This page provides a practical overview of how Pix works. For deeper technical, legal, and regulatory details, refer to the [official documentation](https://www.bcb.gov.br/estabilidadefinanceira/pix-normas) published by the **Central Bank of Brazil (BACEN)**. These materials keep you up to date with rule changes, deadlines, and official requirements. BACEN's materials are the authoritative source for Pix regulations and contain the most complete and up-to-date specifications. # What is Pix? Source: https://docs.lerian.studio/en/rails/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. It 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. # Why Pix matters *** 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 dramatically. 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 These requirements ensure consistent, regulated behavior across the entire ecosystem. # In short *** * **SPI** handles settlement * **DICT** manages Pix keys * **Direct or indirect participants** connect through certified gateways * **PSTIs** provide regulated infrastructure * **Pix modalities** support everything from simple transfers to QR Code payments and invoicing * **BACEN regulations** ensure interoperability, security, and traceability Together, these components form Brazil’s instant-payment ecosystem — fast, compliant, and universally accessible. **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 reaches Pix in two ways, and both settle through Midaz. **Lerian SPI** is Lerian's own messaging with BACEN's instant payment system. It connects your institution directly, with no connectivity partner in the path, and it covers the full SPI messaging contract — not only the Pix flows on this page. For the partner path, Lerian provides **Pix Switch** — a unified plugin that implements the complete Pix orchestration layer for financial institutions. Pix Switch handles the complexity described above: DICT management, transaction flows, QR Codes, MED, and refunds. Institutions configure their connectivity provider, and the plugin handles the rest. Key characteristics: * **One plugin, any provider** — A provider adapter layer abstracts the connectivity provider, so your integration stays the same regardless of how you connect to BACEN * **All participation models** — Whether your institution participates directly or indirectly, the API is identical * **Native ecosystem integration** — Works with Midaz (ledger), CRM, Fees Engine, and Access Manager Learn how Pix Switch works, what it provides, and how to get started. Explore the core Pix domains: DICT, Transactions, QR Codes, and MED. Browse the Pix endpoints: DICT, claims, transactions, QR Codes, and MED. # Pix Switch Source: https://docs.lerian.studio/en/rails/pix/pix-switch Pix Switch is Lerian's unified Pix plugin: one application layer, any provider, covering orchestration, keys, QR Codes, and disputes. Pix Switch is Lerian's Pix plugin. It gives your institution what it needs to operate Pix at the application layer -- transaction orchestration, key management, QR Codes, refunds, and dispute resolution -- through a single, unified product. The core idea: **one plugin, any provider**. You choose the connectivity provider that matches your participation model. Pix Switch handles the rest. Lerian is a **technology provider**. We are not a PSP, not a PSTI, and do not provide SPI/DICT connectivity. Regulated connectivity is always performed by an external provider -- either a direct participant or a certified PSTI hired by your institution. The architecture and contracts described on this page reflect the current Pix Switch product design. # Why Pix Switch *** Historically, supporting Pix across different connectivity providers meant maintaining **separate, isolated plugins** -- one per provider, each with its own codebase, API contracts, and integration patterns. This created real problems: * **Duplicated business logic** -- Each plugin had to implement and maintain the same Pix rules independently * **Different API contracts** -- Each provider integration exposed a different API to the client, making it harder to switch or add providers * **Higher maintenance cost** -- You had to apply bug fixes, regulatory updates, and new features separately to each plugin * **No portability** -- Moving from one provider to another required a full re-integration Pix Switch solves this by centralizing all Pix logic into a single product with a **unified API** and a **Provider Adapter Layer** that handles provider-specific translation. The result: one integration, any provider, no rework when your connectivity needs change. # What Pix Switch provides *** Pix Switch delivers a complete **Pix orchestration layer** that handles the full business lifecycle of Pix operations: * **Cash-out and cash-in flows** -- Send and receive Pix payments with full message lifecycle management * **Pix key management (DICT)** -- Create, update, delete, and resolve ownership of Pix keys * **Static and dynamic QR Code generation** -- Issue QR Codes for payment initiation * **MED lifecycle orchestration** -- Manage the Special Refund Mechanism (Mecanismo Especial de Devolucao) end to end * **Refund handling** -- Process full and partial refunds with proper regulatory sequencing * **Webhooks, event processing, and background workers** -- React to provider events in real time and run async jobs reliably * **Native Midaz integration** -- Midaz is the mandatory ledger for Pix Switch, providing account validation, balance checks, debit/credit posting, and routing # How it works *** Pix Switch uses a **Provider Adapter Layer** to separate your integration from the specifics of any single connectivity provider. The plugin exposes a **single API** to your application. Behind that API, a provider adapter translates every operation -- transactions, key lookups, QR Code registrations, refunds -- into the protocol and format required by your configured provider. Your institution selects which provider to use during setup. The plugin resolves everything else. This architecture means that switching providers or supporting additional participation models does not require rewriting your integration. You change the configuration. The API stays the same. Regulated connectivity is always external. Pix Switch does not connect to BACEN directly. A **direct participant** or a **certified PSTI** provides the SPI/DICT gateway layer. Pix Switch operates above that layer, handling all Pix business logic. # Participation models *** BACEN defines two regulatory models for Pix participation: **direct** (your institution connects to BACEN on its own) and **indirect** (you connect through a licensed direct participant). For a detailed explanation of each model, see [What is Pix?](/en/rails/pix/pix-overview). In Pix Switch, the participation model is a **configuration** -- not a different product. Both models are supported through the **Provider Adapter Layer**. You choose a provider that matches your regulatory status, and the plugin API stays the same. Switching providers or upgrading your participation model does not require re-integrating the plugin. # Integrations in the Lerian ecosystem *** Pix Switch requires **Midaz** as its core ledger. This native integration simplifies: * Account validation * Balance checks * Debit/credit posting * Routing * Business rules (fees, authorizations) Pix Switch also connects with other Lerian ecosystem products: * **CRM (Midaz CRM)** -- Customer relationship and entity management * **Fees Engine** -- Automated fee calculation and collection * **Access Manager** -- Authentication and authorization # Deployment model *** Pix Switch follows the same deployment models available across the Lerian platform: **SaaS** and **BYOC** (Bring Your Own Cloud). Both give you access to the full product — the difference is who operates the infrastructure. * **SaaS** -- Lerian hosts and operates Pix Switch in a fully managed environment. Your team focuses on product configuration and integration. Lerian handles infrastructure, updates, and availability. * **BYOC** -- Your institution deploys and operates Pix Switch in its own infrastructure — whether on a public cloud, private cloud, or on-premises. For a detailed comparison of both models, see [SaaS and BYOC](/en/deployment-models). # Target audience *** Pix Switch is designed for institutions that: * Participate in the Pix arrangement (or intend to) * Own a valid **ISPB** (identifier assigned by BACEN to financial institutions in Brazil) * Own the ledger at the business level -- your institution controls accounts and balances, regardless of where the infrastructure runs Whether your team manages its own infrastructure (BYOC) or prefers a fully managed environment (SaaS), Pix Switch adapts to your operational model. **Not suitable for** institutions where a third party controls the ledger or account management at the business level. If your accounts and balances live inside a BaaS provider's domain -- not just its infrastructure -- Pix Switch is not the right fit. The institution must own the ledger layer logically, regardless of deployment model. # Requirements to operate *** Before adopting Pix Switch, your institution must have: * A valid **ISPB** registered with BACEN * A **contract with a connectivity provider** -- a direct participant (for indirect participation) or a certified PSTI (for direct participation), depending on your regulatory model * **Logical ownership of the ledger** -- Midaz must be deployed and operational, with your institution controlling accounts and balances **Additional requirements for BYOC:** * Your own **cloud or on-premise environment** to host the plugin * **DevOps and SRE readiness** -- your team manages deployment, monitoring, updates, and incident response * **Security and compliance ownership** -- your institution is responsible for access control, redundancy, observability, and audit trails In the **SaaS** model, Lerian manages infrastructure, deployment, and operational availability. Your institution remains responsible for **regulated connectivity** and **business configuration**. # Advantages *** ### Ecosystem-native integrations Connect to Midaz, CRM, Fees Engine, and Access Manager out of the box. Each integration is built into the platform. ### Customization Extend or modify business logic to match your institution's specific requirements. Adapt to regulatory changes at your own pace. ### Economic efficiency Remove reliance on multi-layer intermediaries that add cost without adding control. In SaaS, go to production without upfront infrastructure investment. In BYOC, size infrastructure for your actual demand. ### Faster path to production (SaaS) Go live without provisioning or managing infrastructure. Lerian handles operations, updates, and availability -- your team focuses on building financial products. ### Full control and data sovereignty (BYOC) Run everything in your own infrastructure. Apply your own IAM policies, encryption standards, and governance frameworks. Define routing rules, business policies, and operational practices. Your ledger, balances, and stored data stay in your environment. Because Pix is a payment rail, the plugin still exchanges the settlement and directory messages required to move money with the Brazilian payment system (the RSFN network operated by Banco Central) — directly or through your participation provider. Owning the environment means you own the data and the infrastructure, not that no traffic ever leaves it. # Trade-offs *** Each deployment model comes with different trade-offs. ### SaaS * **Less infrastructure control** -- Lerian manages the environment. You configure the product, not the platform underneath. * **Data hosted by Lerian** -- Your data lives in Lerian-managed cloud infrastructure. If your regulatory requirements mandate data residency in your own environment, choose BYOC. ### BYOC * **Requires technical maturity** -- Your institution needs DevOps capabilities, observability tooling, and operational management expertise. * **Requires governance discipline** -- Security, uptime, compliance, and audit readiness must be maintained by your team. * **More configuration upfront** -- Flexibility means more orchestration and parameterization during initial setup. Plan for a structured onboarding process. * **Operational responsibility** -- Your institution manages software updates, patches, scaling, and continuity plans. Lerian provides the software and tooling -- your team operates it. For help choosing the right model, see [SaaS and BYOC](/en/deployment-models#which-model-is-right-for-you). # Pix use cases with Midaz *** See how Pix Switch and Midaz work together in practice: Model validated, reusable Pix transfer flows using Transaction Routes and Operation Routes -- from simple transfers to fee collection. Combine Pix Switch with the Fees Engine for transparent, automated fee management across fintechs and marketplaces. # Plugin use cases Source: https://docs.lerian.studio/en/rails/pix/pix-use-cases Explore end-to-end Pix use cases combining Midaz with the Pix Plugin, Fees Engine, and Transaction Routes to automate fees and enforce ledger rules. Plugins extend Midaz with domain-specific capabilities — Pix connectivity, bank transfers, and more. The use cases below show how these plugins work with the core ledger to build complete financial workflows. Each use case describes a real scenario. It covers the entities involved, the transaction flow, and what your team gains. ### Pix Pix is Brazil's instant payment system. To build a Pix product, you do more than connect to BACEN's infrastructure. Institutions must also calculate and route fees, enforce consistent transaction patterns, and keep a clear audit trail for every movement. * **[Pix with automated fees](/en/rails/pix/midaz-for-pix-with-fees)**: Combine the Pix Plugin with the Fees Engine. Launch Pix with automatic fee calculation, splitting, and double-entry recording — no custom middleware. * **[Pix with Transaction Routes](/en/rails/pix/midaz-for-pix-transaction-routes)**: Define reusable transfer patterns in the ledger with Transaction Routes and Operation Routes. Midaz then enforces the rules on every Pix transaction. For use cases that focus on Midaz Core without plugins, see [Midaz use cases](/en/midaz/use-cases). # TED via BTG Source: https://docs.lerian.studio/en/rails/ted/btg/ted-via-btg TED via BTG on the Lerian platform — how BTG fits the rail-and-provider model for Bank Transfer, with links to the production-ready Via JD integration. **TED via BTG** is the BTG provider path for the TED rail. It uses the same rail → provider model as the rest of Brazil Rails. The rail-level TED documentation stays the same. The provider section covers only the connection details. BTG has no live TED integration yet. Today, TED runs through **JD** only: * **[What is Bank Transfer?](/en/rails/ted/ted-overview)** — the TED rail: transfer types, best practices, and glossary. * **[Developer guide](/en/rails/ted/jd/ted-developer-guide)** — the Via JD integration: webhooks, data model, configuration, and environment variables. The Via JD path is production-ready. To enable TED connectivity, [get in touch with our team](https://lerian.studio/contact). # Cancelling a Transfer Source: https://docs.lerian.studio/en/rails/ted/console/bt-cancelling-a-transfer Cancel a bank transfer from its detail page in Lerian Console while the transfer is in Created or Pending state, using the Cancel Transfer button. You can cancel a bank transfer from its detail page while the transfer is in a state that allows cancellation. Cancellation is permanent. You cannot undo it. ## When a transfer can be cancelled *** The Console shows the **Cancel Transfer** button only while the transfer is in **Created** or **Pending** state. When the transfer advances beyond either state, the button disappears. You can no longer cancel the transfer through the Console. Cancellation is permanent. You cannot reactivate a cancelled transfer. To retry, create a new transfer. ## Cancelling a transfer *** From the Transfers list, click the transfer you want to cancel. The detail page opens. In the header, click the red **Cancel Transfer** button. A confirmation dialog appears. The dialog title is **Cancel Transfer**. It asks *Are you sure you want to cancel this transfer? This action cannot be undone.* Click the confirmation button to proceed. To keep the transfer active, close the dialog. On success, a toast confirms *Transfer cancelled successfully*. The detail page refreshes. The status updates to **Cancelled** and a new entry appears in the status timeline. The **Cancel Transfer** button disappears. ## Error handling *** The request can fail — for example, the transfer advances to a non-cancellable state between the page load and your confirmation. In this case, the transfer stays in its current state. Refresh the page to see the current status before you try again. # Checking Transfer Details Source: https://docs.lerian.studio/en/rails/ted/console/bt-checking-transfer-details Inspect a single bank transfer in Lerian Console — amounts, sender and recipient, references, status timeline, and the Raw Data tab for debugging. The **Transfer Detail** page shows everything recorded for a single bank transfer: amounts, sender and recipient information, references, and the full status history. It also has a **Raw Data** tab for debugging and advanced inspection. ## Opening the detail page *** You can open the detail page in several ways: * From the [Transfers list](/en/rails/ted/console/bt-managing-transfers), open the **Actions** menu on the row you want and select **See details**. (List rows are not clickable. Use the **Actions** menu to open the detail page.) * Click **View Transfer** on the success screen after you create a transfer. * Use **See details** from a row on the [Reconciliation page](/en/rails/ted/console/bt-managing-reconciliation). ## Page header *** The header shows the transfer identifier (truncated) and the creation date in local format. Next to the title, the page shows: * A **status badge** for the current state (Created, Pending, Processing, Completed, Failed, and so on). * An *Auto-refreshing...* hint that pulses while the transfer is still in flight. * A **Cancel Transfer** button when the transfer is in a cancellable state (see [Cancelling a Transfer](/en/rails/ted/console/bt-cancelling-a-transfer)). Auto-refresh polls the transfer every five seconds while it is in flight. Once it reaches a terminal state (Completed, Processed, Failed, Cancelled, Rejected, Expired, Received, Pending Confirmation), polling stops. ## Tabs *** The page has two tabs: **Summary** (the default) and **Raw Data**. ### Summary The Summary tab arranges the transfer in sections. #### Financial Summary * **Amount** — the transfer amount. * **Fee** — the total fee applied. * **Total** — the sum of the amount and the fee (shown in bold). * **Type** — the transfer type badge (TED Out, TED In, or P2P). * **Description** — the description you provided, or *No description* if empty. #### References The page shows these fields only when at least one reference value exists: * **Confirmation Number** — the reference returned at confirmation time. * **Control Number** — the reference from the counterparty system. #### Sender * **Account ID** — the sender account identifier. #### Recipient * **Name** — the beneficiary's name. * **Document** — the beneficiary's CPF/CNPJ, partially masked for privacy (the first three and the last three characters stay visible). * **ISPB / Branch** — the beneficiary bank's ISPB and branch, shown as `ISPB / Branch`. * **Account** — the beneficiary's account number. * **Account Type** — Checking or Savings. #### Dates * **Created at** — the transfer's creation date and time. * **Updated at** — the date and time of the last update. * **Completed at** — the date and time the transfer reached a terminal state. The page shows this field only after completion. #### Metadata If the transfer carries custom metadata, the page lists each key-value pair here. #### Status History On the right-hand column of the Summary tab, a vertical timeline shows every status change for this transfer: * The **status label** (Created, Pending, Processing, Completed, Failed, and so on). * The **timestamp** of the change. * An optional **reason** for the transition. The timeline sorts entries from newest to oldest. If no status history exists yet, the card shows *No status history available*. ### Raw Data The Raw Data tab shows the full payload the plugin returns for this transfer. Use it to inspect fields the summary view does not show, or to share the exact response with support. ## Error state *** A transfer sometimes fails to load, for example after a temporary backend error. The page then shows *Failed to load transfer details* with a **Retry** button that requests the data again. # Configuring Fee Policy Source: https://docs.lerian.studio/en/rails/ted/console/bt-configuring-fee-policy Set the Bank Transfer fee policy in Lerian Console — the Fail Closed Default toggle and the maximum fee cap that guards against runaway fee rules. The **Fee Policy** section controls how the plugin behaves when it cannot compute a fee. It also caps the maximum fee the plugin applies to a single transfer. This policy guards the fee calculation that the [Fees Engine](/en/midaz/fees/console/fees-engine-overview) runs. ## Accessing the section *** In the Bank Transfer sidebar, click **Settings**. Click the section header to expand it. ## Fields *** ### Fail Closed Default This toggle sets the plugin behavior when the fee service is unavailable. It applies to a timeout, a connection failure, or a server error from the fee service: * **On (fail closed)** — the plugin blocks the transfer until it computes the fee reliably. This option is safer but stricter. * **Off (fail open)** — the plugin lets the transfer proceed without the fee. This option is looser but allows transfers during a partial failure. Prefer **Fail Closed** in production for TED Out flows. A transfer that proceeds without a reliable fee can create unbilled operations or reconciliation gaps. ### Max Fee Amount This field sets the hard cap on the total fee for a single transfer. You enter it in currency units, for example `50.00`. The plugin stores the cap in cents. If the calculated fee exceeds this cap, the plugin rejects the transfer. The plugin does not trim the fee to the cap. Use the cap as a guardrail against misconfigured fee rules that produce oversized charges. ## Saving changes *** Click **Save** in the toolbar at the top of the page. The [Settings History](/en/rails/ted/console/bt-viewing-settings-history) tab records every change. The history shows the maximum fee in cents, because the plugin stores that value in cents. ## Tips *** * Review the Fee Policy together with your Fees Engine configuration. The cap blocks a transfer whose fee exceeds the limit. Still fix misconfigured fee rules at the source. * Raise the cap only when you must. A higher cap allows a larger fee per transfer. Tell the team that operates the plugin first, so the team can review the change. # Configuring JD Connection Source: https://docs.lerian.studio/en/rails/ted/console/bt-configuring-jd-connection Configure the JD Connection for the Bank Transfer plugin in Lerian Console — set your origin ISPB and the JD SPB base URL for outbound TED transfers. The **JD Connection** section defines where the plugin sends its requests. It also sets the ISPB that identifies your institution. Together with [JD Credentials](/en/rails/ted/console/bt-configuring-jd-credentials), these values make the integration operational. The section header shows a **Configured** or **Not Configured** badge. The badge shows **Configured** when both fields have a value. ## Accessing the section *** In the Bank Transfer sidebar, click **Settings**. Click the section header to expand it. ## Fields *** ### Origin ISPB The ISPB that identifies your institution as the origin of outbound TED transfers. The counterparty bank sees this value as the sender's institution. By convention, the ISPB is an 8-digit code. The form does not check the length or restrict the value to digits. Make sure you enter the correct value, or the integration does not work. ### Base URL The full base URL of the JD SPB environment the plugin connects to (for example `https://api.jdspb.com`). Always use an **HTTPS** URL. The plugin sends credentials and transfer data to this endpoint, so an `http://` URL exposes them in transit. The form does not check the URL format. Make sure the value is correct before you save. The URL must also match the environment that your JD credentials are valid for. Production credentials against a sandbox URL, or the reverse, cause authentication errors. A change to the Base URL affects every subsequent transfer. Coordinate the change with your integration team, especially when you move between sandbox and production environments. ## Saving changes *** Click **Save** in the toolbar at the top of the page. The [Settings History](/en/rails/ted/console/bt-viewing-settings-history) tab records every change. After you update the connection, check the plugin's **Dashboard** for the health indicator. If the integration cannot reach the new endpoint, the status changes to **Down** and the degradation banner appears. # Configuring JD Credentials Source: https://docs.lerian.studio/en/rails/ted/console/bt-configuring-jd-credentials Set JD SPB authentication for the Bank Transfer plugin in Lerian Console — Legacy Code, User Code, password, and RSA PEM private key for signing. The **JD Credentials** section holds the details the Bank Transfer plugin uses to authenticate against the JD SPB integration. Without these values, the plugin cannot send or receive transfers. The section header shows a **Configured** or **Not Configured** badge. It reads **Configured** when both a Legacy Code and a User Code have a value. ## Accessing the section *** In the Bank Transfer sidebar, click **Settings**. The section sits first on the page and opens by default. ## Fields *** ### Legacy Code The legacy client identifier from JD. The form does not require this field. The JD integration usually needs a value here to authenticate. ### User Code The user identifier for the credentials. The form does not require this field. The JD integration usually needs a value here to authenticate. ### Password The password for the user. The plugin stores it as a secret and never shows it after you save. The field shows placeholder dots instead of the stored value. Leave it empty when you save to keep the current password. ### Private Key (PEM) The PEM-encoded private key that signs requests to JD. It must be an **RSA** key in PEM format. JD SPB signatures use RSA PKCS#1 v1.5 with SHA-256. The key must have at least **3072 bits**. The plugin also stores it as a secret and never shows it back. The field shows placeholder dots. Leave it empty to keep the current value. ### Signature Required Controls whether the plugin signs requests to JD with the private key. Keep it on for production transfers. Outbound TED signing is mandatory, and the plugin rejects unsigned dispatch outside local test use. Treat the Legacy Code, User Code, Password, and Private Key as production secrets. Restrict edit access. Do not share screenshots that show these fields. Rotate the values through your organization's standard secret-rotation process. ## Saving changes *** After you edit the fields, click **Save** in the toolbar at the top of the page. The plugin submits only the fields you changed. Secret fields left empty keep their current stored value. The plugin records every change in the [Settings History](/en/rails/ted/console/bt-viewing-settings-history) tab. It stores secret values but shows them as masked placeholders (`••••`). You can see *when* a secret changed without exposing its content. # Configuring Operating Hours Source: https://docs.lerian.studio/en/rails/ted/console/bt-configuring-operating-hours Define the TED operating window in Lerian Console — open and close times, IANA timezone, and how the plugin handles transfers submitted outside the window. The **Operating Hours** section sets the window when the plugin accepts TED transfers. The plugin rejects a TED transfer that you submit outside this window. The rejection reports the next available time. The [Initiate](/en/rails/ted/console/bt-initiating-a-transfer) page shows a banner when the current time is outside the window. The section header shows a **Configured** or **Not Configured** badge. The badge reads **Configured** when both Open Time and Close Time have a value. ## Accessing the section *** In the Bank Transfer sidebar, click **Settings**. Click the section header to expand it. ## Fields *** ### Open Time The time of day when the TED window opens, in `HH:MM` 24-hour format. ### Close Time The time of day when the TED window closes, in `HH:MM` 24-hour format. If you leave this blank, the plugin applies the default value. ### Timezone The IANA timezone that the plugin uses to read Open Time and Close Time (for example `America/Sao_Paulo`). The plugin makes all time comparisons in this timezone, not in the user or server timezone. ## Default behavior *** If you do not configure Operating Hours, the plugin applies these defaults for the banner and validation: * **Open Time:** `06:30` * **Close Time:** `17:00` * **Timezone:** `America/Sao_Paulo` * **Days:** Monday through Friday, and never on a BACEN holiday (fixed — you cannot change the processing days in the Console) ## How the window is used *** The Console uses the configured window to: * Show an **Outside TED Operating Hours** banner on the Initiate page when the current time is outside the window. * Warn you that the plugin rejects a TED transfer submitted outside the window. The banner only informs you. The Console does not block submission. If you submit a TED transfer outside the window, the plugin rejects it and reports the next available time. The plugin does not queue the transfer for later. The default window is Monday through Friday, 06:30–17:00 (`America/Sao_Paulo`, UTC−3), and it excludes BACEN holidays. ## Saving changes *** Click **Save** in the toolbar at the top of the page. The Console records every change in the [Settings History](/en/rails/ted/console/bt-viewing-settings-history) tab. # Configuring Usage Limits Source: https://docs.lerian.studio/en/rails/ted/console/bt-configuring-usage-limits Cap daily and monthly TED transfer volume in Lerian Console — enable Usage Limits to enforce guardrails against runaway automations and risk policies. The **Usage Limits** section caps how much value each account can move in a given day or month. Use it to enforce operational guardrails — catch configuration mistakes, prevent runaway automations, and comply with internal risk policies. The section header shows an **Enabled** or **Disabled** badge that matches the toggle below. ## Accessing the section *** In the Bank Transfer sidebar, click **Settings**. Click the section header to expand it. ## Fields *** ### Limits Enabled Toggle that turns enforcement on or off. When the toggle is off, the Daily Limit and Monthly Limit fields stay editable but inactive. The plugin applies no cap until you turn the toggle on. ### Daily Limit The maximum total value of transfers the plugin allows per day. Enter it in regular currency units — for example, `100.00` for R\$ 100.00. The plugin stores the value internally in cents. You can edit this field only when you enable limits. ### Monthly Limit The maximum total value of transfers the plugin allows per month. It uses the same input format as the Daily Limit. You can edit this field only when you enable limits. ## How limits are applied *** When you start a transfer, the plugin adds up the account's transfer value for the day and for the month. It compares each total against the configured cap. If the transfer would push either total past its cap, the plugin rejects it before it reaches JD. Limits apply to the total value of transfers that you initiate through the plugin. The JD poller receives inbound TED transfers (TED In) passively, so they do not count against these caps. ## Saving changes *** Click **Save** in the toolbar at the top of the page. The Console records every change in the [Settings History](/en/rails/ted/console/bt-viewing-settings-history) tab. The plugin stores monetary values in cents internally, and the history reflects that. ## Tips *** * When you roll out the plugin, start with conservative limits. Raise them gradually as you gain confidence in the flow. * If a limit blocks a legitimate operation, raise the cap, wait for the save to register in the history, then retry the transfer. You cannot bypass a cap for a single transfer without first adjusting the setting. # Initiating a Transfer Source: https://docs.lerian.studio/en/rails/ted/console/bt-initiating-a-transfer Create a TED Out in Lerian Console with the two-step initiate-and-confirm wizard — collect details, preview fees, and execute the outbound TED transfer. The **New Transfer** page creates a transfer in two steps. In step 1 you enter the transfer details and request a fee preview (**initiate**). In step 2 you review the transfer and confirm it (**process**). The Console shows step 1 as a form and step 2 as a review screen. The Console runs the transfer when you click **Confirm Transfer**. This wizard always creates a **TED Out** (outbound TED). Inbound TED (TED In) arrives automatically through the JD poller, so it has no manual creation step. A P2P transfer uses a separate flow. You cannot create a P2P transfer from this page. ## Accessing the wizard *** In the Midaz Module sidebar, expand **Bank Transfer**. Click **Transfers**. Click **New Transfer** at the top right of the page. The page header shows **New Transfer**. The subtitle reads *Initiate a new bank transfer in two simple steps*. A stepper near the top shows your place in the flow: step 1 is the form and step 2 is the review. The success screen appears after the backend completes the transfer. ## Operating hours banner *** An amber **Outside TED Operating Hours** banner appears at the top when the current time is outside the configured TED operating window. The banner states the operating window: Monday through Friday, between the configured open and close times, in the configured timezone. Do not submit a TED transfer outside the operating window. The Console does not block the button, but the backend rejects the request with a `422` error (`BTF-0010`). The response reports the next time the operating window opens. The default window is Monday through Friday, 06:30–17:00 (`America/Sao_Paulo`, UTC−3). Adjust the operating window in Settings. ## Step 1 — Collect details and initiate *** The form has three sections. The Console marks all required fields. When you click **Continue**, the Console calls the backend `/initiate` endpoint with the values below. The `/initiate` endpoint computes the fees and returns a preview for the review screen. ### Source Account * **Sender Account** *(required)* — choose the source account from the dropdown. The list holds the ledger accounts that allow sending. If the ledger has no eligible sender account, the field becomes a text input. You then type the account ID directly. ### Destination Account * **Recipient Name** *(required)* — the beneficiary's full name (up to 100 characters). * **CPF/CNPJ** *(required)* — the beneficiary's document. A CPF has 11 digits and a CNPJ has 14 digits. * **Account Number** *(required)* — the beneficiary's account number. * **Account Type** *(required)* — **Checking** or **Savings**. * **ISPB Code** *(required)* — the beneficiary bank's 8-digit ISPB. * **Branch** *(required)* — the beneficiary bank's branch code. **ISPB Code** and **Branch** apply to TED transfers (TED Out and TED In). This wizard creates a TED Out, so both fields are required here. ### Transfer Details * **Amount** *(required)* — the transfer amount. The amount must be greater than zero. * **Currency** *(required)* — fixed to **BRL**. * **Description** *(optional)* — a free-text description up to 140 characters. Click **Continue** to move to the review step. The `/initiate` response carries the initiation identifier, the fee breakdown, and the estimated completion. These values drive the next screen. ## Step 2 — Review and confirm *** The Console builds the review screen from the `/initiate` response in step 1. The initiation identifier, the fee breakdown, and the estimated completion carry over. Check the transfer before you run it. The screen shows: * **Transfer Type** — always **TED Out** on this page. * **Sender Account**, **Recipient Name**, **CPF/CNPJ**, **Recipient Account**, **Account Type**, **ISPB**, **Branch**, **Description** — exactly what you entered in step 1. * **Amount** — the amount you entered. * **Fee** — the total fee that the plugin computes. * **Fee Breakdown** — one line per fee entry, with its label and its amount. A *Deductible* entry comes out of the transfer amount. Other entries add to it. * **Total** — the final amount that the plugin debits from the sender. * **Estimated Completion** — the timestamp when the transfer should complete. Click **Back** to return to the form. The Console keeps your previous entries. When you click **Continue** again, the Console re-runs `/initiate` so the fee preview matches your changes. When you click **Confirm Transfer**, the Console sends the reviewed transfer to the backend `/process` endpoint. This call creates and runs the transfer through the JD SPB integration. The button shows *Confirming…* while the call is in flight. The Console redirects to the success screen after the backend responds. ## Success screen *** After `/process` returns successfully, the Console shows a success screen with: * **Transfer ID** — the identifier of the transfer. * **Confirmation Number** — the reference that the JD integration returns. * **Status** — the status after confirmation (typically **Created**, **Pending**, **Processing**, or **Completed**). * **Fee** and **Total** — the applied values. The success screen offers two actions: * **View Transfer** — opens the [Transfer Detail page](/en/rails/ted/console/bt-checking-transfer-details) for this transfer. * **New Transfer** — restarts the wizard with a clean form. # Managing the Bank Transfer Dashboard Source: https://docs.lerian.studio/en/rails/ted/console/bt-managing-dashboard Monitor the Bank Transfer plugin in Lerian Console — health status indicator, KPI cards, volume by TED Out, TED In, and P2P, plus recent activity. The **Dashboard** is the first page under **Bank Transfer** in the sidebar. It gives you a quick view of the plugin health and activity. It shows the overall status indicator, headline counts, volume by transfer type, a health breakdown, and the most recent transfers. ## Accessing the Dashboard *** From the Lerian Console home page, select the Midaz product card. In the left sidebar, expand the **Bank Transfer** section and click **Dashboard** (it is the first item). ## Page header *** The page title **Bank Transfer** sits at the top left, with the subtitle *Dashboard overview*. On the right, a status indicator and a status label summarize the current health. | Indicator | Meaning | | --------- | ------------------------------------------------------- | | Healthy | The plugin operates normally. | | Attention | The plugin detected non-critical issues. | | Degraded | Some data may be stale or partially unavailable. | | Down | The Console cannot reach the plugin to read its health. | Next to the status, a relative timestamp (*Updated just now*, *Updated 5m ago*) shows when the plugin last refreshed the summary. ## Degradation banner *** When the plugin reports degraded performance, a banner appears below the header. The banner warns that performance is degraded and some data may be stale, then lists the specific reasons. The banner is informational. You can still use the plugin. Treat the listed reasons as signals that this page may not show the latest data. ## KPI cards *** Five cards summarize transfer activity for the current organization: * **Total Transfers** — the total count. * **Completed** — transfers in a successful terminal state, with its share of the total. * **Processing** — transfers currently in progress, including those in *Created* status, with its share. * **Pending** — transfers awaiting processing, with its share. * **Failed / Rejected** — transfers in a failed, rejected, or cancelled state, with its share. ## Transfer Volume by Type *** A horizontal bar chart shows how many transfers fall under each type: **TED Out**, **TED In**, and **P2P**. Point to a bar to see the exact count. If the plugin has no transfers yet, the card shows *No transfer data available*. ## System Health card *** On the right, the **System Health** card shows a detailed breakdown: * **Overall Status** — the same health value that the header shows. * **Processing** — number of transfers in the *Processing* state. * **Pending** — number of transfers in the *Pending* state. * **Terminal** — transfers that reached a terminal state. * **Failures** — transfers that ended in a failure state. * **Fail Rate** — percentage of terminal transfers that failed. When the plugin is degraded, the card lists the degradation reasons inside the card body, just above the **Last updated** line in the footer. ## Recent Transfers *** The bottom of the page lists the five most recent transfers with columns for **Type**, **Amount**, **Fee**, **Total**, **Status**, and **Date**. Use the **View All** link in the card header to jump to the full Transfers list. The Recent Transfers card auto-refreshes. While at least one transfer is in flight, it polls every five seconds. When all visible transfers reach a terminal state, it polls every thirty seconds instead. # Managing Reconciliation Source: https://docs.lerian.studio/en/rails/ted/console/bt-managing-reconciliation Review bank transfers pending reconciliation in Lerian Console — failure concentration cards and per-transfer details to investigate rejection patterns. The **Reconciliation** page lists bank transfers that the plugin flags as pending reconciliation. A transfer lands here when it fails, or when the counterparty rejects it. Each one needs manual review to close the loop with the counterparty. The page header carries the subtitle *Transfers requiring manual review due to ambiguous failures*. The page also highlights the most common failure codes, so you can spot patterns at a glance. ## Accessing the Reconciliation page *** From the Midaz Module sidebar, expand **Bank Transfer**. Select **Reconciliation** in the submenu. ## When there is nothing to reconcile *** If no transfers need reconciliation, the page shows a green confirmation icon with two lines: > **No transfers pending reconciliation** > > All transfers have been processed successfully or resolved. An *Auto-refreshing...* hint at the bottom indicates the page continues to check for new items in the background. ## Failure Concentration *** When pending items exist, a row of cards at the top shows the most frequent failure codes. The row also shows how many transfers each code affects. Use it as a quick heat map of what fails today. Each card shows: * The **failure code** returned by the counterparty system. * The **count** of transfers affected (formatted as *N transfer* or *N transfers*). ## Pending items table *** Below the failure concentration, a table lists the individual transfers pending reconciliation: | Column | Description | | ----------- | ------------------------------------------------------------------- | | Transfer ID | Identifier of the transfer. | | Type | Transfer type badge (TED Out, TED In, P2P). | | Control # | Control number assigned by the counterparty system, when available. | | Failure | Failure code, shown as a red badge. | | System | Counterparty system that reported the failure. | | Flagged | Timestamp when the transfer was flagged for reconciliation. | A footer line summarizes the total (*N transfer* or *N transfers* pending reconciliation). ### Actions Each row has an **Actions** menu. Select **See details** to open the full [Transfer Detail page](/en/rails/ted/console/bt-checking-transfer-details) for that transfer. On that page, you inspect the status timeline and the raw response, then decide the next steps. The Reconciliation page stays read-only. You resolve a reconciliation item through the counterparty, or you issue corrective transfers. You do not resolve it on this page. ## Auto-refresh *** While the page is open, it polls the backend periodically. An animated *Auto-refreshing…* label shows that the page refreshes the data. You do not need to take any manual action. ## Error state *** If the page cannot load the reconciliation data, it shows *Failed to load reconciliation data*. A page refresh usually resolves a transient issue. If the error persists, contact your administrator. # Managing Bank Transfer Settings Source: https://docs.lerian.studio/en/rails/ted/console/bt-managing-settings Overview of Bank Transfer settings in Lerian Console — JD credentials, connection, webhook, usage limits, operating hours, fee policy, and change history. The **Settings** page holds the Bank Transfer configuration you manage from the Console. It groups the settings into six sections. It keeps a full change history, so you can see who changed what and when. ## Accessing Settings *** From the Midaz Module sidebar, expand **Bank Transfer**. Select **Settings** in the submenu. The page has two tabs: * **Settings** — six configuration sections (default tab). * **History** — full audit log of every change to the settings. ## Editing and saving *** You expand or collapse each section independently. The **Save** button in the top toolbar submits only the fields you changed. If you made no edits, **Save** has no effect. * **Save** — persists all changed fields across every section at once. * **Discard** — resets the form to the current saved values. A toast confirms *Settings saved successfully* when the save completes, or *Failed to save settings. Please try again.* if something goes wrong. The plugin sends only the fields you changed. You do not need to re-enter values in the sections you did not touch. ## The six configuration sections *** Authentication credentials for the JD SPB integration. Origin ISPB and the JD SPB base URL. Daily and monthly transfer caps. The window when TED transfers run. Fail-closed default and maximum fee amount. Most sections display a status badge in their header: * **Configured** / **Not Configured** for JD Credentials, JD Connection, and Operating Hours — based on whether their key fields have values. * **Enabled** / **Disabled** for the toggle-based sections (Webhook, Usage Limits). The Fee Policy section does not show a status badge. ## Change history *** Switch to the **History** tab to review every change to the settings. Each entry shows who made the change and when. See the full audit log of settings changes. # Managing Transfers Source: https://docs.lerian.studio/en/rails/ted/console/bt-managing-transfers Browse, filter, sort, and export bank transfers from the Lerian Console Transfers list — TED Out, TED In, and P2P with CSV export and status filters. The **Transfers** page lists every bank transfer recorded for the current organization and ledger — TED Out, TED In, and P2P. Use it to search, filter, inspect, export, and create new transfers. ## Accessing the Transfers page *** From the Midaz Module sidebar, expand **Bank Transfer**. Select **Transfers** in the submenu to open the list. The page header shows the title **Transfers** and the subtitle *View, manage, and track all bank transfers for this organization*. ## Header actions *** The header has two buttons: * **Export CSV** — downloads up to 200 of the most recent transfers with the current filters. * **New Transfer** — opens the initiate wizard for a new TED Out. Step-by-step guide to creating a new TED Out. ## Filters *** The filter panel is collapsible. Expand it to narrow the list. | Filter | Values | | ---------- | -------------------------------------------------------------------------------------------- | | Type | All Types, TED Out, TED In, P2P | | Status | All Statuses, Created, Received, Pending, Processing, Completed, Failed, Rejected, Cancelled | | Start Date | Date picker; transfers created on or after this date. | | End Date | Date picker; transfers created on or before this date. | | Min Amount | Numeric input; includes transfers with amount equal to or above. | | Max Amount | Numeric input; includes transfers with amount equal to or below. | A toggle button in the filter toolbar switches the sort direction between **Newest First** and **Oldest First**. The backend default sets the initial order until you toggle it. The page-size control changes how many rows each page shows. The column visibility menu hides or shows columns. Click **Clear filters** to reset all filters at once. ## Columns *** By default, the table includes: * **Type** — a colored badge (TED Out, TED In, P2P). * **Amount** — transaction amount in the transfer currency. * **Fee** — the fee the plugin applies (may be empty for inbound transfers). * **Total** — amount plus fee (may be empty for inbound transfers). * **Status** — a colored badge for the current state. * **ID** — transfer identifier. * **Date** — the creation timestamp. * **Actions** — a kebab menu at the end of each row. Its **See details** item opens the [Transfer Detail page](/en/rails/ted/console/bt-checking-transfer-details). To open a transfer, use the **Actions** menu on its row and click **See details**. ## Exporting to CSV *** **Export CSV** triggers a fresh fetch of up to 200 transfers under the current filters. It downloads a file named `bank-transfers-.csv` with one row per transfer. The export runs against the filtered set. Narrow the filters first if you need a subset. The CSV columns differ from the on-screen table. The file carries the full transfer record, not the display view. | Column | Notes | | ---------- | ------------------------------------ | | ID | Transfer identifier. | | Type | TED Out, TED In, or P2P. | | Status | Current transfer status. | | Amount | Transfer amount. | | Currency | Defaults to BRL when not set. | | Sender | The sender account holder's name. | | Recipient | The recipient account holder's name. | | Created At | Creation timestamp. | | Updated At | Last-update timestamp. | The CSV does not include the **Fee** and **Total** columns from the on-screen table. The export also stops at 200 rows per run. For larger datasets, apply filters — for example, by date range or status — and run more than one export. ## Empty state *** If the list has no transfers and no active filters, the page shows an empty state. The empty state invites you to create the first transfer. If active filters make the list empty, clear them to see all transfers again. # Bank Transfer in Lerian Console Source: https://docs.lerian.studio/en/rails/ted/console/bt-overview Use the Bank Transfer plugin in Lerian Console via JD SPB — dashboard health, transfers list, reconciliation queue, and six configuration sections. The **Bank Transfer** plugin in Midaz Module lets you initiate, track, and reconcile bank transfers through the **JD SPB** integration, directly from Lerian Console. It provides a single surface to monitor transfer health, operate TED flows, investigate failures, and configure the integration. ## What Bank Transfer covers *** The plugin handles three transfer types, each with a different flow in the Console: * **TED Out** — Outbound TED transfers initiated from the Console. It is the only type you create manually on the **New Transfer** page. * **TED In** — Inbound TED transfers received by the JD poller. They appear in the transfers list automatically. There is no manual creation step. * **P2P** — Internal peer-to-peer transfers. The **New Transfer** page does not create them. A separate flow handles them. The **New Transfer** page always creates a TED Out. Incoming TED and P2P flows do not expose a "new transfer" action in this plugin. ## Activating the plugin *** Bank Transfer is an optional plugin. Enable it in **Midaz Settings → Plugins** before you use it. Learn how to enable, disable, and configure plugins in Midaz Module. Once enabled, the **Bank Transfer** section appears in the left sidebar with four pages: * **Dashboard** — health indicator, KPIs, transfer volume by type, and recent activity. * **Transfers** — searchable list of all transfers with filters and CSV export. * **Reconciliation** — transfers flagged as pending reconciliation with failure concentration. * **Settings** — six configuration sections plus a change history tab. Your environment also controls availability: beyond the in-Console **Settings → Plugins** toggle, a runtime plugin flag (`NEXT_PUBLIC_BANK_TRANSFER_ENABLED`) gates the plugin. The flag stays off by default. Bank Transfer becomes visible only when your administrator sets the flag to `true` for your environment. The Console reads the flag at runtime, so you set the environment variable and restart the Console container without a rebuild. If Bank Transfer does not appear after you enable it in Settings, the flag is likely off for your environment. See [Lerian Console via Helm](/en/platform/helm/console/console-helm). ## Available pages *** Monitor system health, transfer volume, and recent activity. Browse, filter, and export transfers across all types and statuses. Review transfers flagged for reconciliation and their failure codes. Configure JD SPB credentials, connection, webhook, limits, hours, and fee policy. ## Transfer statuses *** Transfers move through a set of statuses reflecting their lifecycle. The Dashboard, the Transfers list, and the Transfer Detail page show the same statuses. | Status | Meaning | | -------------------- | -------------------------------------------------- | | Created | Transfer was created and is queued for processing. | | Received | Inbound TED received by the JD poller. | | Pending | Awaiting processing by the counterparty system. | | Pending Confirmation | Waiting for a final confirmation step. | | Processing | In flight with the counterparty. | | Processed | Counterparty reported the transfer as processed. | | Completed | Transfer finished successfully. | | Failed | Processing failed. | | Rejected | Counterparty rejected the transfer. | | Cancelled | Cancelled by a user before completion. | | Expired | Expired before confirmation. | Terminal statuses (Completed, Processed, Failed, Rejected, Cancelled, Expired, Received, Pending Confirmation) stop auto-refresh polling on detail pages. # Viewing Settings History Source: https://docs.lerian.studio/en/rails/ted/console/bt-viewing-settings-history Audit every Bank Transfer setting change in Lerian Console — timestamp, key, old and new value, actor, and revision number for compliance reviews. The **History** tab on the Settings page lists every change to the plugin configuration in chronological order. Use it to answer questions such as *who changed the daily limit last week?* You do not have to search the logs. ## Accessing the history *** In the Bank Transfer sidebar, click **Settings**. At the top of the page, click the **History** tab. ## Columns *** Each row corresponds to a single field change. | Column | Description | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Timestamp | Date and time of the change (format `YYYY-MM-DD HH:mm:ss`). | | Key | The setting key that changed, in dotted notation (for example `usage_limits.daily_limit_cents`, `webhook.timeout_ms`, `operating_hours.open_time`). | | Old Value | The value before the change. | | New Value | The value after the change. | | Actor | The identifier of the user or service that made the change. | | Revision | Monotonically increasing revision number assigned at save time, shown as a badge. | ## Reading secrets *** The history never stores a secret field in clear text. Secret fields include the password, the private key, and the webhook signing secret. When you change a secret, both **Old Value** and **New Value** show `••••`. This keeps the change auditable and does not expose the secret. ## Empty state *** Before you record any change, the tab shows *No changes recorded yet*. The first save on the Settings tab creates the first entry. ## How entries are created *** Every time you click **Save** on the Settings tab: 1. The plugin compares the form values against the last saved state. 2. It sends only the fields that changed. 3. Each changed field becomes a separate entry with the same revision number. So a single save that changes three fields produces three rows. The three rows share one revision number. You can see which changes went together. ## Tips *** * Keep the History tab open when you investigate an issue. A regression often lines up with a setting change that you see here. * When you coordinate a change with a third party (for example, a new JD environment), note the revision number. The revision number is a stable reference to the exact configuration live at that moment. # Configuration Source: https://docs.lerian.studio/en/rails/ted/jd/ted-configuration Configure Bank Transfer across infrastructure, tenant, and account levels — limits, fee policies, and operating hours adjustable at runtime. The Bank Transfer plugin splits configuration across three levels. You can change most business settings at runtime, with no service restart. The tenant identity and the Midaz organization scope serve different purposes. Keep them distinct. The `tenantId` JWT claim in the authenticated request context resolves the tenant. The tenant controls infrastructure-level isolation: it selects the multi-tenancy platform database and grants access to tenant-scoped secrets. The `X-Organization-Id` header defines the Midaz organization scope inside that tenant. Every organization-scoped transfer route requires this header, in all deployment modes. A request returns `400` if it omits the header or sends a value that is not a valid UUID. Background processes, such as the TED IN poller and reconciliation workers, do not receive request headers. In single-tenant deployments, these processes use the `ORGANIZATION_ID` environment variable as the organization context. ## Configuration levels *** The Bank Transfer plugin has three configuration levels: * **Infrastructure configuration** (DevOps manages it) controls URLs, credentials, authentication settings, and timeouts. Changes need a service restart. * **Tenant settings** (the Product team manages them through the Admin API) control transfer limits, fee policies, and operating-hours overrides. Changes take effect with no service restart. * **Account settings** (the Product team manages them through the Admin API) control limits and restrictions for individual accounts. Changes take effect with no service restart. ## Business decisions you can configure *** GPMs and product teams care about these settings. You manage all of them through the Admin API at runtime, with no deploy. ### Transfer limits Set daily and monthly volume caps at two levels: * **Per organization** — applies to transfers for one Midaz organization inside the resolved tenant * **Per account** — applies to a specific end-user account (overrides organization defaults) Limits cover both total amount and number of transactions. Set these to manage risk and comply with BACEN requirements. ### Fee policy Control whether the plugin charges a fee on TED OUT, TED IN, and P2P transfers. The Fees Engine defines the fee rules and applies them per organization. See [Fees Engine](/en/midaz/fees/fees-engine-overview) for configuration details. ### Fail-open vs. fail-closed If the fee calculation service is unavailable when a transfer runs, you have two options: * **Fail-open** — allow the transfer to proceed without a fee * **Fail-closed** — block the transfer until the fee service is available again The default fee-service policy is **fail-open** (`FEES_FAIL_CLOSED_DEFAULT=false`). Change it per organization through the Admin API when you need fee-service outages to block transfers. TED IN has its own safety switch, `BTF_FEES_TED_IN_FAIL_OPEN`, which defaults to `true`. If plugin-fees is down, the plugin credits inbound funds with fee=0. ### TED IN receiving Incoming transfers stay disabled by default. Enable TED IN per organization after you configure your JD SPB credentials and start the polling worker. ### Operating hours overrides The plugin enforces BACEN's TED operating window by default. You can set custom windows per tenant policy within BACEN limits — for example, transfers during business hours only. ## Infrastructure configuration *** DevOps sets the deploy-time environment variables (URLs, credentials, TLS, persistence, integrations, security keys). These variables require a service restart. See the full reference in [Environment variables](/en/rails/ted/jd/ted-environment-variables). ## Runtime configuration (Admin API) *** You manage tenant-level and account-level settings through the Admin API, with no restart. Changes take effect immediately (subject to the cache TTL for tenant settings). Configurable settings include: * Transfer limits (daily and monthly, per organization and per account) * Fee behavior (fail-open or fail-closed when the fee service is unavailable) * TED IN receiving (enabled or disabled per organization) * Operating hours overrides (custom windows within BACEN limits) See the Admin API reference for the full list of configurable fields and request format. ### Systemplane-managed settings The systemplane (Admin API) manages the settings below at runtime, not at deploy time. Each one has a matching environment variable name that still exists in the codebase. The config loader ignores these env vars and logs a deprecation WARN if you set them. Assigning them through the environment has no effect. Use the systemplane to change them. | Area | Settings | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Rate limiting | `RATE_LIMIT_ENABLED`, `RATE_LIMIT_MAX`, `RATE_LIMIT_EXPIRY_SEC` | | CORS | `CORS_ALLOWED_ORIGINS`, `CORS_ALLOWED_METHODS`, `CORS_ALLOWED_HEADERS` (default origin is wildcard `*` when unconfigured; not validated at startup) | | Fee policy | `FEES_FAIL_CLOSED_DEFAULT`, `FEES_MAX_FEE_AMOUNT_CENTS`, `FEES_REFUND_ON_DEVOLUCAO`, `BTF_FEES_TED_IN_FAIL_OPEN` | | Usage limits | `USAGE_LIMITS_ENABLED`, `USAGE_LIMIT_DAILY_CENTS`, `USAGE_LIMIT_MONTHLY_CENTS` | | Operating hours | `TRANSFER_OPERATING_OPEN`, `TRANSFER_OPERATING_CLOSE`, `TRANSFER_OPERATING_TIMEZONE` | | Idempotency / duplicate guard | `IDEMPOTENCY_REQUIRE_REDIS`, `DUPLICATE_GUARD_TTL_SEC` | | Routing | `ROUTING_*` (all routing-rule settings) | | JD timeout & retry tuning | `JD_TIMEOUT_MS`, `JD_MAX_RETRIES`, `JD_VALIDATE_EXTERNAL_SIGNATURE` | | JD polling tuning | `JD_POLL_MAX_MESSAGES_PER_CYCLE`, `JD_POLL_RECOVERY_BATCH_SIZE`, `JD_POLL_DISABLE_OPERATING_HOURS_WINDOW` | | Reconciliation tuning | `BTF_RECONCILIATION_BATCH_SIZE`, `BTF_RECONCILIATION_MAX_ATTEMPTS`, `BTF_RECONCILIATION_STALE_AFTER_SEC`, `BTF_RECONCILIATION_TICK_DEADLINE_SECONDS`, `RECONCILIATION_PENDING_ALERT_THRESHOLD` | | RabbitMQ publish tuning | `RABBITMQ_MAX_RETRIES`, `RABBITMQ_PUBLISH_TIMEOUT_MS`, `RABBITMQ_RETRY_BACKOFF_MS`, `RABBITMQ_ROUTING_KEY_PREFIX` | | Webhook delivery tuning | `WEBHOOK_TIMEOUT_MS`, `WEBHOOK_MAX_RETRIES`, `WEBHOOK_RETRY_BACKOFF_MS`, `WEBHOOK_ALLOW_UNSIGNED_BROKER_EVENTS`, `WEBHOOK_UNSIGNED_BROKER_EVENTS_GRACE_SEC` | Setting any of the names above through the environment has no effect. The loader logs a deprecation WARN instead. These names exist only as deprecated env twins of runtime/systemplane-managed settings. When CORS is unconfigured, the default allowed origin is the wildcard `*`. Any origin can then call the API, and the plugin does not validate it at startup. For production, set explicit origins through the systemplane key `cors.allowed_origins` instead of the wildcard. # Data and reporting Source: https://docs.lerian.studio/en/rails/ted/jd/ted-data-model Reference the data fields and status lifecycles available for TED transfers — reconciliation, audit trails, and compliance reporting. Every transfer generates a complete audit trail. This page describes the data available for reporting, reconciliation, and compliance. ## What data is recorded per transfer *** | Field | What it means | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transferId` | Your internal reference for this transfer | | `confirmationNumber` | Human-readable reference (e.g. 20260205001) — show this to customers | | `controlNumber` | JD SPB reference — use for bank reconciliation | | `type` | TED OUT, TED IN, or P2P | | `status` | Current state of the transfer | | `amount` | Transfer amount before fee | | `feeAmount` | Fee charged | | `totalAmount` | Total debited (amount + fee) | | `senderAccountId` | Sending account in your system. Always present; for TED IN — where the sender is an external bank with no local account — it is a synthetic identifier deterministically derived from the sender's document | | `recipientAccountId` | Recipient account in Midaz, when the recipient is represented internally | | `recipientDetails` | Recipient bank, branch, account number, and holder name | | `originalTransferId` | The original TED OUT transfer that a devolution (return) TED IN compensates | | `devolutionCode` | BACEN devolution reason code, when applicable | | `createdAt` | When the transfer was initiated | | `completedAt` | When settlement was confirmed | ## Transfer status lifecycle *** ### TED OUT TED OUT state machine diagram * **Customer confirmed** (`CREATED`) — the customer confirmed the transfer, now queued for submission * **Submitted to bank network** (`PENDING`) — message sent to JD Consultores, awaiting acknowledgment * **Bank processing** (`PROCESSING`) — JD accepted the transfer and routes it * **Settled** (`COMPLETED`) — transfer successfully settled at the destination bank * **Rejected** (`REJECTED`) — JD returned a business error (e.g., invalid account data) * **Failed** (`FAILED`) — technical failure (timeout or service unavailability) * **Cancelled** (`CANCELLED`) — customer cancelled before the transfer was submitted ### TED IN TED IN state machine diagram * **Transfer detected** (`RECEIVED`) — incoming message persisted, pending internal processing * **Recipient validated** (`PROCESSING`) — the system credits the recipient account * **Amount credited** (`COMPLETED`) — recipient account successfully credited The sending bank can reverse a settled inbound transfer. To handle these chargebacks, TED IN supports a `COMPLETED` → `FAILED` transition. ### P2P P2P state machine diagram * **Confirmed** (`CREATED`) — transfer initiated between internal accounts * **Processing** (`PROCESSING`) — Midaz transaction in progress * **Settled** (`COMPLETED`) — both accounts updated successfully * **Failed** (`FAILED`) — processing error * **Cancelled** (`CANCELLED`) — cancelled before processing began ## Fee review before confirmation (TED OUT only) *** For TED OUT, customers go through a two-step flow: PaymentInitiation lifecycle diagram * **Pending confirmation** — the plugin calculated and presented the fee. The customer has not confirmed it yet * **Processed** — the customer confirmed and the plugin created the transfer * **Expired** — 24 hours elapsed without confirmation Customers can review the full cost (amount + fee) before they confirm the transfer. ## Status history *** The plugin records every status transition with a timestamp, the previous state, the new state, and a reason for errors and cancellations. This gives you a full audit trail for every transfer — who changed what and when. The `changedBy` field records the actor that made the transition — for example, a system process or a reconciliation worker. It can be empty. ## Reconciliation fields *** Use these fields to match transfer records against your bank statements: | Field | Use for | | -------------------- | ------------------------------------------------------------------------ | | `controlNumber` | Matching against JD SPB records | | `confirmationNumber` | Customer-facing reference | | `transferId` | Internal system lookups | | `originalTransferId` | Link a devolution (return) TED IN to the original TED OUT it compensates | | `devolutionCode` | Classify the BACEN reason for a devolution (return) | | `createdAt` | Filtering by initiation date | | `completedAt` | Filtering by settlement date | ## Data retention *** Do not delete transfer records. The plugin never deletes or expires them, so you own their retention. Keep transfer and audit records for at least 5 years, per BACEN record-keeping requirements. ## Querying your data *** Use [List Transfers](/en/reference/midaz/plugins/ted/list-transfers) to query transfers with the following filters: * **By date range** — filter by `createdAt` or `completedAt` * **By type** — TED OUT, TED IN, or P2P * **By status** — e.g., only `COMPLETED` transfers for reconciliation, or `FAILED` for investigation ## For developers *** ### Storage The plugin stores transfer data in PostgreSQL. The `recipientDetails` field uses JSONB. This field holds the different data structures for TED OUT, TED IN, and P2P recipients. The `recipientAccountId` field references a Midaz account when the recipient is internal. Each tenant has its own database, and the organization is the main filter within a tenant. The plugin maintains the following indexes for common query patterns: * `(midaz_organization_id, created_at)` for paginated listings. * `(midaz_organization_id, status, created_at)` for status-based filtering. * `(control_number, date)` (unique) for JD reconciliation lookups. The `transfer_status_history` audit table uses one index for audit and investigation workflows: * `(transfer_id, changed_at DESC)` for transfer-level history. ### Incoming TED deduplication Before it processes a TED IN transfer, the plugin stores the raw JD message in the `JDIncomingMessage` table. This lets the plugin recover incoming transfers if the service fails during processing. To prevent duplicate processing, the table enforces a unique constraint on `sequenceNumber` (JD's `NumCabSeq`). If JD re-delivers the same message, the plugin automatically identifies it as a duplicate and ignores it. ### What the plugin sends to Midaz The plugin posts every settled movement to Midaz as a ledger transaction, but Midaz records the accounting entry — not the transfer's banking details. Counterparty identity (bank, branch, account, holder name and document) and BACEN references (`controlNumber`, `clearingControlNumber`) live only in the plugin's `Transfer` record. For a TED IN, the credit enters the ledger from the `@external/BRL` account; the sender's identity is not encoded into the Midaz transaction. What the Midaz transaction carries is correlation metadata: | Metadata key | Value | | ------------------------------------------ | --------------------------------------------------------------------------------------- | | `transferId` | The plugin's transfer identifier — the pivot back to the full record | | `transferType` | `TED_OUT`, `TED_IN`, or `P2P` | | `initiationId` | The `PaymentInitiation` identifier (TED OUT and P2P only — TED IN has no initiate step) | | `jdMessageId`, `jdSequence`, `messageCode` | The JD message that produced a TED IN credit | A devolution (return) credit additionally carries `refundKind`, `originalTransferId`, `devolutionCode`, and the original control numbers. Correlation works in both directions: * The transfer record stores `midazTransactionId`, so [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) returns the full banking detail for any ledger entry. * The Midaz transaction stores `transferId` in its metadata, so you can list Midaz transactions filtered by `metadata.transferId` to find the ledger entry for a transfer. Custom metadata you pass when initiating a TED OUT or P2P transfer is merged into the Midaz transaction as-is. The keys `transferId`, `transferType`, and `initiationId` are reserved — the plugin's values always win. ### Entity relationships The TED domain model follows these relationships: * Each `Transfer` belongs to a single organization and can have multiple `TransferStatusHistory` records. * TED OUT transfers can originate from a `PaymentInitiation` in the two-step transfer flow. * Each `JDIncomingMessage` can create at most one `Transfer` of type `TED_IN`. Entity relationships diagram # Developer guide Source: https://docs.lerian.studio/en/rails/ted/jd/ted-developer-guide Implement Bank Transfer integrations correctly — idempotency, retry strategy, state handling, and webhook validation patterns for reliable transfers. This guide is for developers who implement the Bank Transfer plugin integration. It covers the patterns and decisions that go beyond individual endpoint calls: idempotency, retry strategy, state handling, and webhook validation. For endpoint parameters and response schemas, see the [API Reference](/en/reference/midaz/plugins/ted/initiate-transfer). ## Idempotency *** Every mutating request (initiate, process, cancel) requires an `X-Idempotency` header. If you send the same key twice, the plugin returns the original response without creating a duplicate operation. **Rules:** * Use a UUID v4 or a unique business identifier (e.g. your internal order ID) * Maximum length: 255 characters * The plugin scopes each key to the effective organization. The same key from two organizations counts as two distinct requests. * The plugin returns a cached response for the configured idempotency window (`IDEMPOTENCY_RETRY_WINDOW_SEC`, default 300 seconds) * A replayed response is byte-identical to the original: same status code, same body. The response has no header to mark a replay, so design your client to stay safe in either case. ```http theme={null} POST /v1/transfers/initiate X-Organization-Id: 019c9ac2-3f5d-7df9-9215-bdccc1451def X-Idempotency: 7f3d9a1b-4e2c-4f8a-b3d1-9e6f2a4c8b7e ``` Do not reuse idempotency keys across different operations. Do not reuse an initiate key to process or cancel the same transfer. ### Duplicate detection Beyond idempotency keys, the plugin detects content-based duplicates. It builds a fingerprint from: * `senderAccountId` * recipient details (ISPB, branch, account, holder document) * amount * purpose The plugin stores the fingerprint in Redis for 5 minutes. The default is 300 seconds. Operators tune it per tenant through the systemplane setting `idempotency.duplicate_guard_ttl_seconds`. The organization is not part of the fingerprint. Tenant isolation comes from the Redis key prefix. The plugin rejects the request with `409 BTF-0012` if the client already submitted a matching transfer inside the window. This catches cases where the client sends the same transfer with a different idempotency key. One example is a retry after a timeout, when the client did not receive the original response. ## Retry strategy *** Use exponential backoff for transient errors. Do not retry every error. | HTTP status | Retry? | Notes | | ----------- | ------ | ------------------------------------------------------------------------- | | `400` | No | Validation error — fix the request before retrying | | `404` | No | Not found — the resource does not exist | | `409` | No | Duplicate — idempotent; use the original response | | `410` | No | Expired — create a new initiation | | `422` | No | Business rule (operating hours, limits) — the condition must change first | | `429` | Yes | Rate limit — wait for the `Retry-After` header value (seconds) | | `500` | Yes | Internal error — retry with backoff | | `503` | Yes | Unavailable — retry with backoff | **Recommended backoff schedule for 5xx/503:** 0s, 5s, 25s, 60s, 120s (5 attempts total). When JD SPB is unavailable, the response is `HTTP 503`. The `error.code` field then carries the raw JD vendor code — for example, `TRANSPORT` for transport failures or `ACE95` for timeouts. The plugin does not wrap JD-chain failures in a `BTF-` code. Flag the transfer for manual reconciliation after the retries run out. Do not retry without limit. The JD SPB network has defined operating hours. ## State handling *** ### TED OUT state machine Transfers follow a strict progression. You cannot cancel a transfer after it leaves `CREATED` or `PENDING`. TED OUT state machine **What to do in each state:** | State | Meaning | Recommended action | | ------------ | ------------------------------------------ | ---------------------------------------------------------- | | `CREATED` | Confirmed by user, queued for submission | Show "Processing" in UI; poll or wait for webhook | | `PENDING` | Submitted to JD, awaiting acknowledgment | Show "Processing"; do not allow cancellation | | `PROCESSING` | JD accepted and is routing the transfer | Show "Processing"; typical SLA under 10 minutes | | `COMPLETED` | Settled | Show confirmation with `confirmationNumber` | | `REJECTED` | JD rejected (invalid data, rule violation) | Show error to user; funds already released | | `FAILED` | JD unreachable or timed out | Show error; funds already released; allow retry if desired | | `CANCELLED` | Cancelled before submission | Show cancellation confirmation | ### Initiation state machine The initiate endpoint creates a `PaymentInitiation` entity. This entity has its own lifecycle before the plugin creates a `Transfer`. Initiation state machine ### TED IN state machine TED IN state machine ### P2P state machine P2P does not have a `PENDING` state. Settlement is atomic and instant. P2P state machine ### Polling vs. webhooks Prefer webhooks for real-time status. If you have not configured webhooks yet, poll `GET /v1/transfers/{transferId}`. Use a maximum of 10 attempts with the same backoff schedule as retries. Flag the transfer for manual review after 10 minutes with no terminal state (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`). See [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) and [Webhooks](/en/rails/ted/jd/ted-webhooks). ## Webhook integration *** For event payload schemas and the full list of events, see [Webhooks](/en/rails/ted/jd/ted-webhooks). ### Signature validation Every webhook request includes headers your endpoint uses to verify authenticity: * `X-Webhook-Signature` — versioned HMAC-SHA256 signature in the form `v1,sha256=` * `X-Webhook-Timestamp` — Unix timestamp in seconds (UTC) when the plugin built the request * `X-Webhook-Event` — the event type (for example, `transfer.completed`). This header is not part of the signature. The plugin computes the signature as: ``` X-Webhook-Signature: v1,sha256=hex(HMAC_SHA256(WEBHOOK_SIGNING_SECRET, "v1:" + + "." + )) ``` The signed string has four parts in order: the prefix `v1:`, the timestamp value from `X-Webhook-Timestamp`, one ASCII dot (`.`), then the **raw request body bytes**. Use the body bytes exactly as they arrive on the wire. Do not parse or re-encode them first. To validate: 1. Read `X-Webhook-Signature` and `X-Webhook-Timestamp` from the request headers. 2. Build the signed string: `"v1:" + timestamp + "." + rawBody`. 3. Compute `HMAC-SHA256` over the signed string with your `WEBHOOK_SIGNING_SECRET`, then hex-encode the result. 4. Prepend `v1,sha256=`, then compare against `X-Webhook-Signature` with a constant-time equality function. 5. Reject the request if the timestamp is outside an acceptable freshness window (a 5-minute tolerance is typical) to prevent replay. Aside from `X-Webhook-Signature` and `X-Webhook-Timestamp`, the plugin sets only `X-Webhook-Event` (the event type). It does not send `X-Webhook-Event-Type`, `X-Webhook-Routing-Key`, or `X-Webhook-Delivery-Attempt`. ```javascript theme={null} const crypto = require('crypto'); const express = require('express'); const TOLERANCE_SECONDS = 300; // 5 minutes function validateWebhook(rawBody, timestamp, signature, secret) { if (!timestamp || !signature) return false; const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)); if (Number.isNaN(ageSeconds) || ageSeconds > TOLERANCE_SECONDS) return false; const signedPayload = Buffer.concat([ Buffer.from('v1:', 'utf8'), Buffer.from(timestamp, 'utf8'), Buffer.from('.', 'utf8'), rawBody, ]); const expected = 'v1,sha256=' + crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); const expectedBuf = Buffer.from(expected); const receivedBuf = Buffer.from(signature); if (expectedBuf.length !== receivedBuf.length) return false; return crypto.timingSafeEqual(expectedBuf, receivedBuf); } // Use raw body — not req.body (parsed JSON) app.post('/webhooks/ted', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.headers['x-webhook-timestamp']; const signature = req.headers['x-webhook-signature']; if (!validateWebhook(req.body, timestamp, signature, process.env.WEBHOOK_SIGNING_SECRET)) { return res.status(401).send('Invalid signature'); } const payload = JSON.parse(req.body.toString()); // process payload... res.status(200).send('OK'); } ); ``` ```python theme={null} import hmac import hashlib import time TOLERANCE_SECONDS = 300 # 5 minutes def validate_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool: if not timestamp or not signature: return False try: age = abs(int(time.time()) - int(timestamp)) except ValueError: return False if age > TOLERANCE_SECONDS: return False signed_payload = b"v1:" + timestamp.encode() + b"." + raw_body expected = "v1,sha256=" + hmac.new( secret.encode(), signed_payload, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature) ``` ```go theme={null} import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strconv" "time" ) const toleranceSeconds = 300 // 5 minutes func validateWebhook(rawBody []byte, timestamp, signature, secret string) bool { if timestamp == "" || signature == "" { return false } ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false } if diff := time.Now().Unix() - ts; diff < -toleranceSeconds || diff > toleranceSeconds { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte("v1:")) mac.Write([]byte(timestamp)) mac.Write([]byte(".")) mac.Write(rawBody) expected := "v1,sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) } ``` ### Idempotent webhook processing Your endpoint may receive the same event more than once (at-least-once delivery). Use `transferId` + `event` as a composite key to deduplicate. ```javascript theme={null} const alreadyProcessed = await db.webhookEvents.exists({ transferId: payload.transferId, event: payload.type, }); if (alreadyProcessed) { return res.status(200).send('OK'); // acknowledge without reprocessing } ``` ## Error handling patterns *** Map API error codes to user-facing actions. See the [full error list](/en/reference/midaz/plugins/ted/ted-error-list) for all codes. | Scenario | User-facing message | Action | | ------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Outside operating hours** (`BTF-0010`) | "Transfers available Mon–Fri, 06:30–17:00 (Brasília). Next window: " | Show next available time | | **Daily limit exceeded** (`BTF-0011`) | "Daily transfer limit reached. Try again tomorrow." | Show remaining limit | | **Duplicate transfer** (`BTF-0012`) | "This transfer was already submitted." | Return original `transferId` | | **Invalid recipient data** (`BTF-0001`) | "Check recipient details and try again." | Highlight invalid fields | | **Initiation expired** (`BTF-0202`) | "Session expired. Please start a new transfer." | Restart initiation flow | | **JD SPB unavailable** (`TRANSPORT`, HTTP `503`) | "Transfer service temporarily unavailable. Try again in a few minutes." | Retry with backoff; detect via `503` + raw JD vendor code (`TRANSPORT`, `ACE95`, …), not a `BTF-` prefix | | **Midaz unavailable** (`BTF-2000`) | "Service temporarily unavailable. Try again in a few minutes." | Retry with backoff | Error responses follow this structure: ```json theme={null} { "error": { "code": "BTF-0010", "service": "plugin", "category": "deterministic", "message": "Transfers can only be initiated Monday-Friday between 06:30 and 17:00 Brasília time", "requestId": "6d3e2a68-1f2b-4c3d-9e4f-5a6b7c8d9e0f", "fields": { "currentTime": "2026-01-21T18:30:00-03:00", "nextAvailableTime": "2026-01-22T06:30:00-03:00" } } } ``` ## Go-live checklist *** Before enabling the integration in production: * [ ] Send `X-Idempotency` on every initiate, process, and cancel request * [ ] Retry logic implemented with exponential backoff for 5xx/503 errors * [ ] Webhook endpoint deployed and returning `200` within 5 seconds * [ ] Signature validation active on the webhook endpoint * [ ] Webhook event deduplication implemented using `transferId + event` * [ ] Operating hours validated client-side before calling initiate (reduces unnecessary 422s) * [ ] Both `transferId` and `confirmationNumber` stored for reconciliation * [ ] Terminal states (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`) handled in UI * [ ] Initiation expiry (24h) handled — prompt the user to restart when the window passes * [ ] Service readiness monitored in your alerting system for BYOC deployments * [ ] Redis reachable and monitored — the service rejects requests when Redis is down * [ ] `PLUGIN_AUTH_ENABLED=true` configured in production, with a valid `PLUGIN_AUTH_ADDRESS` (HTTPS) # Environment variables Source: https://docs.lerian.studio/en/rails/ted/jd/ted-environment-variables Configure the Bank Transfer plugin via JD at deploy time — application, TLS and proxy headers, persistence, JD SPB integration, and security variables. DevOps sets these variables at deploy time. A change requires a service restart. Runtime and business settings live in [Configuration](/en/rails/ted/jd/ted-configuration). In the tables below, the **Default / Required** column shows the default value. A bold qualifier (for example **Required**, **Required in production**, **Required if enabled**) marks a variable you must set. `—` means no default. ## Infrastructure configuration *** This section is for DevOps teams. DevOps sets these variables at deploy time. A change takes effect only after a service restart. ### Application | Variable | Default / Required | Description | | ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ENV_NAME` | `development` | Environment (`development`, `staging`, `production`) | | `DEPLOYMENT_MODE` | `byoc` | Deployment flavor. `byoc` = Bring Your Own Cloud (single-tenant, operator-managed). Toggles internal SaaS vs. BYOC behaviors. | | `SERVER_ADDRESS` | `:8080` | HTTP server address and port | | `HTTP_BODY_LIMIT_BYTES` | `1048576` | Maximum HTTP request body size in bytes | | `ALLOW_PRIVATE_UPSTREAMS` | `false` | ⚠️ Allow outbound adapters (CRM, Fees, JD, Midaz) to resolve to RFC1918/loopback IPs. Default `false` is fail-closed: production blocks DNS pivoting into private space. Enable in dev or in-cluster BYOC where upstreams legitimately live on private IPs. Cloud metadata endpoints stay blocked regardless of this flag. | ### TLS | Variable | Default / Required | Description | | ------------------------- | ------------------ | ------------------------------------------------------------------------------- | | `SERVER_TLS_CERT_FILE` | — | Path to TLS certificate file. Must be set together with `SERVER_TLS_KEY_FILE`. | | `SERVER_TLS_KEY_FILE` | — | Path to TLS private key file. Must be set together with `SERVER_TLS_CERT_FILE`. | | `TLS_TERMINATED_UPSTREAM` | `false` | Set to `true` when TLS is terminated by a load balancer or reverse proxy. | ### Proxy headers Set these when the service runs behind a load balancer or reverse proxy. They let the service read the real client IP for rate limiting and audit logs. | Variable | Default / Required | Description | | ------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `SERVER_PROXY_HEADER` | — | HTTP header carrying the real client IP (e.g. `X-Forwarded-For`, `X-Real-IP`). Empty disables proxy header parsing. | | `SERVER_TRUSTED_PROXIES` | **Required if proxy header set** | Comma-separated list of trusted proxy IPs/CIDRs. Required when `SERVER_PROXY_HEADER` is set to prevent IP spoofing. | ### Authentication This plugin delegates authorization to `plugin-auth`. Configure the connection with the variables below. | Variable | Default / Required | Description | | --------------------- | ----------------------- | ------------------------------------------------------------------ | | `PLUGIN_AUTH_ENABLED` | `false` | Enable authorization via plugin-auth. Set to `true` in production. | | `PLUGIN_AUTH_ADDRESS` | **Required if enabled** | URL of the plugin-auth service. Must use HTTPS in production. | When `PLUGIN_AUTH_ENABLED=true`, `PLUGIN_AUTH_ADDRESS` must use HTTPS in production environments. The service rejects an HTTP address at startup. ### Test admin (non-production only) `BTF_TEST_ADMIN_ENABLED` must remain `false` in production. It exposes test-only admin endpoints (for example `POST /admin/test/circuit-breakers/reset`) that carry no tenant and bypass production authentication. Only the docker-compose mock-lane enables it, for E2E tests. Any deployment with this flag `true` outside a sealed test network is a misconfiguration. | Variable | Default / Required | Description | | ------------------------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BTF_TEST_ADMIN_ENABLED` | `false` | ⚠️ Expose test-only admin endpoints. Must remain `false` in production. | | `BTF_TEST_ADMIN_TOKEN` | **Required if admin enabled** | Token required when `BTF_TEST_ADMIN_ENABLED=true`. Sent in the `X-Test-Admin-Token` header. Intentionally separate from production auth (test-only surface, no tenant). | ### Idempotency | Variable | Default / Required | Description | | ------------------------------ | ------------------ | ----------------------------------------------------------------------------- | | `IDEMPOTENCY_RETRY_WINDOW_SEC` | `300` | Time window (in seconds) during which an idempotency key is considered valid. | ### Multi-tenancy | Variable | Default / Required | Description | | --------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `MULTI_TENANT_ENABLED` | `false` | Enable infrastructure-level multi-tenancy with tenant-isolated databases. | | `ORGANIZATION_ID` | **Required (single-tenant + JD polling)** | Midaz organization UUID injected into background-worker context (TED IN poller, reconciliation) in single-tenant mode. Required when `MULTI_TENANT_ENABLED=false` and `JD_POLLING_ENABLED=true`. Ignored in multi-tenant mode, where per-tenant organization bindings are resolved by the tenant manager. HTTP requests always carry the organization in the `X-Organization-Id` header instead. | | `AWS_REGION` | **Required if AWS secrets backend** | AWS region for per-tenant secret reads from Secrets Manager. Required when `MULTI_TENANT_ENABLED=true` and the secrets backend is AWS. | | `ORGANIZATION_IDS` | **Required in production** | Comma-separated list of Midaz organization UUIDs in the licensing scope. The license gateway validates `LICENSE_KEY` against these at startup. Also referenced under [License](#license). | | `MULTI_TENANT_URL` | **Required when enabled** | Multi-tenancy platform service URL. | | `MULTI_TENANT_REDIS_HOST` | — | Redis host for Pub/Sub event-driven tenant discovery. | | `MULTI_TENANT_REDIS_PORT` | `6379` | Redis port for Pub/Sub. | | `MULTI_TENANT_REDIS_PASSWORD` | — | Redis password for Pub/Sub. | | `MULTI_TENANT_REDIS_TLS` | `false` | Enable TLS for Redis Pub/Sub connection. | | `MULTI_TENANT_REDIS_CA_CERT` | — | CA certificate for the multi-tenant Redis Pub/Sub TLS connection. | | `MULTI_TENANT_TIMEOUT` | `30` | HTTP timeout in seconds for multi-tenancy service calls. | | `MULTI_TENANT_MAX_TENANT_POOLS` | `100` | Maximum concurrent tenant database connection pools. | | `MULTI_TENANT_IDLE_TIMEOUT_SEC` | `300` | Idle timeout in seconds before evicting a tenant pool. | | `MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD` | `5` | Number of failures before the circuit breaker opens. | | `MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC` | `30` | Recovery timeout in seconds for the circuit breaker. | | `MULTI_TENANT_SERVICE_API_KEY` | **Required when enabled** | API key for the multi-tenancy service `/settings` endpoint. | | `MULTI_TENANT_CACHE_TTL_SEC` | `120` | In-memory tenant config cache TTL in seconds. Hot-reloadable via systemplane API. | | `MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC` | `30` | Async interval in seconds for revalidating pool settings. Bootstrap-only (not hot-reloadable). | **BYOC single-tenant:** ```bash theme={null} # Organization injected into background workers (required when JD polling is enabled) ORGANIZATION_ID= # Licensing scope validated against LICENSE_KEY in production ORGANIZATION_IDS= ``` **SaaS multi-tenant:** ```bash theme={null} MULTI_TENANT_ENABLED=true MULTI_TENANT_URL=http://tenant-manager:4003 MULTI_TENANT_SERVICE_API_KEY=your-api-key MULTI_TENANT_REDIS_HOST=redis.example.com ``` ### PostgreSQL | Variable | Default / Required | Description | | ---------------------------------- | ---------------------------------------- | ---------------------------------------------- | | `POSTGRES_HOST` | `localhost` · **Required** | Primary PostgreSQL host. | | `POSTGRES_PORT` | `5432` | Primary PostgreSQL port. | | `POSTGRES_USER` | `plugin-br-bank-transfer` · **Required** | Database user. | | `POSTGRES_PASSWORD` | **Required** | Database password. Required in production. | | `POSTGRES_DB` | `plugin-br-bank-transfer` · **Required** | Database name. | | `POSTGRES_SSLMODE` | `require` | SSL mode. `disable` is rejected in production. | | `POSTGRES_MAX_OPEN_CONNS` | `25` | Maximum open connections. | | `POSTGRES_MAX_IDLE_CONNS` | `5` | Maximum idle connections. | | `POSTGRES_CONN_MAX_LIFETIME_MINS` | `30` | Connection max lifetime in minutes. | | `POSTGRES_CONN_MAX_IDLE_TIME_MINS` | `5` | Connection max idle time in minutes. | | `POSTGRES_CONNECT_TIMEOUT_SEC` | `10` | Connection timeout in seconds. | ### PostgreSQL replica Configure a read replica for query offloading. All fields fall back to primary values when unset. | Variable | Default / Required | Description | | --------------------------- | ------------------ | --------------------------------- | | `POSTGRES_REPLICA_HOST` | — | Replica host. Unset = no replica. | | `POSTGRES_REPLICA_PORT` | — | Replica port. | | `POSTGRES_REPLICA_USER` | — | Replica user. | | `POSTGRES_REPLICA_PASSWORD` | — | Replica password. | | `POSTGRES_REPLICA_DB` | — | Replica database name. | | `POSTGRES_REPLICA_SSLMODE` | — | Replica SSL mode. | ### MongoDB The service requires MongoDB to persist transfer audit events. It does not start without a valid MongoDB connection. | Variable | Default / Required | Description | | ----------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `MONGO_ENABLED` | `true` · **Required** | Enable MongoDB connection. Must be `true` in all environments. | | `MONGO_URI` | **Required** | MongoDB connection string (e.g. `mongodb://user:pass@host:27017`). Must include credentials and TLS in production. | | `MONGO_DATABASE` | **Required** | MongoDB database name. | | `MONGO_MAX_POOL_SIZE` | `25` | Maximum connection pool size. | | `MONGO_SERVER_SELECTION_TIMEOUT_MS` | `3000` | Server selection timeout in milliseconds. | | `MONGO_HEARTBEAT_INTERVAL_MS` | `10000` | Heartbeat interval in milliseconds. | | `MONGO_TLS_CA_CERT` | — | Base64-encoded CA certificate for MongoDB TLS. Use when MongoDB requires TLS with a custom CA (e.g. Atlas, private instances with self-signed certs). | ### Redis | Variable | Default / Required | Description | | ------------------------ | ------------------------------- | ----------------------------------------------- | | `REDIS_HOST` | `localhost:6379` · **Required** | Redis host and port. | | `REDIS_MASTER_NAME` | — | Redis Sentinel master name (if using Sentinel). | | `REDIS_PASSWORD` | — | Redis password (if auth enabled). | | `REDIS_DB` | `0` | Redis database number. | | `REDIS_PROTOCOL` | `3` | Redis protocol version (2 or 3). | | `REDIS_TLS` | `false` | Enable TLS for Redis connections. | | `REDIS_CA_CERT` | — | CA certificate for Redis TLS. | | `REDIS_POOL_SIZE` | `10` | Connection pool size. | | `REDIS_MIN_IDLE_CONNS` | `2` | Minimum idle connections. | | `REDIS_READ_TIMEOUT_MS` | `3000` | Read timeout in milliseconds. | | `REDIS_WRITE_TIMEOUT_MS` | `3000` | Write timeout in milliseconds. | | `REDIS_DIAL_TIMEOUT_MS` | `5000` | Dial timeout in milliseconds. | Redis is a mandatory dependency. It caches idempotency keys and detects duplicates. If Redis is unavailable at startup, or becomes unreachable at runtime, the service reports as DOWN to the readiness probe. It then stops accepting requests. ### JD SPB connection You must set these variables for BYOC deployments. In SaaS mode, Lerian manages the JD connection. | Variable | Default / Required | Description | | ------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `JD_BASE_URL` | **Required for BYOC** | JD SPB API base URL. | | `JD_SOAP_PATH` | `/soap` | JD SOAP endpoint path. | | `JD_LEGACY_CODE` | **Required for BYOC** | JD legacy system code (max 10 chars). | | `JD_USER_CODE` | **Required for BYOC** | JD user code (max 10 chars). | | `JD_PASSWORD` | **Required for BYOC** | JD password (encrypted at rest). | | `JD_PRIVATE_KEY_PEM` | **Required for BYOC** | RSA private key PEM content for XML signature. | | `JD_PRIVATE_KEY_PEM_FILE` | — | Path to a file containing the RSA private key PEM. Alternative to inlining the key in `JD_PRIVATE_KEY_PEM`; read at startup. | | `JD_PUBLIC_KEY_PEM` | — | Public key PEM for validating signatures on JD responses. | | `JD_PRIVATE_KEY_KEYINFO` | — | XML `` block embedded in the SOAP signature (e.g. base64-encoded X509 cert). Required for the WS-Security envelope when JD demands certificate identification. | | `JD_CERT_PEM` | — | Optional PEM-encoded X.509 certificate paired with the JD signing key. Used only by the cert-expiry metrics gauge, not the signing path; a malformed PEM is a startup error. | | `JD_SIGNING_MODE` | `local_pem` | SOAP signing mode. `local_pem` signs locally with `JD_PRIVATE_KEY_PEM`; `external_signer` delegates to a remote service via `JD_EXTERNAL_SIGNER_URL`. | | `JD_EXTERNAL_SIGNER_URL` | **Required if `JD_SIGNING_MODE=external_signer`** | Base URL of the external signing service. | | `JD_EXTERNAL_SIGNER_AUTH_TOKEN` | — | Bearer token sent in the `Authorization` header for external-signer calls. | | `JD_EXTERNAL_SIGNER_TIMEOUT_MS` | `5000` | Timeout (in milliseconds) for external-signer calls. | | `JD_SANDBOX_MODE` | `false` | Enable JD sandbox mode. Rejected in production. | ### JD polling | Variable | Default / Required | Description | | -------------------------- | ------------------ | ---------------------------------------------------------- | | `JD_POLLING_ENABLED` | `false` | Enable TED IN polling worker. | | `JD_POLL_INTERVAL_SECONDS` | `60` | How often (in seconds) the plugin polls for incoming TEDs. | `JD_POLLING_ENABLED` defaults to `false` for safer deployments. In single-tenant mode, set `ORGANIZATION_ID` before you enable it. Background workers inject that value into the context for downstream CRM and Midaz calls. In multi-tenant mode, the TED IN poller manager finds each active tenant through the multi-tenancy platform service. It then starts one poller per tenant and resolves each tenant's JD configuration. ### External services (Midaz) | Variable | Default / Required | Description | | ----------------------- | ----------------------- | -------------------------------------- | | `MIDAZ_BASE_URL` | **Required** | Midaz base service URL. | | `MIDAZ_TRANSACTION_URL` | **Required** | Midaz transaction service URL. | | `MIDAZ_TIMEOUT_MS` | `3000` | Midaz request timeout in milliseconds. | | `MIDAZ_MAX_RETRIES` | `3` | Retry attempts on Midaz failure. | | `MIDAZ_AUTH_ENABLED` | `false` | Enable M2M authentication for Midaz. | | `MIDAZ_AUTH_ADDRESS` | **Required if enabled** | Auth service URL for Midaz M2M tokens. | | `MIDAZ_CLIENT_ID` | **Required if enabled** | OAuth client ID for Midaz M2M. | | `MIDAZ_CLIENT_SECRET` | **Required if enabled** | OAuth client secret for Midaz M2M. | ### External services (CRM) | Variable | Default / Required | Description | | ------------------- | ----------------------- | ------------------------------------ | | `CRM_BASE_URL` | **Required** | CRM service URL. | | `CRM_TIMEOUT_MS` | `2000` | CRM request timeout in milliseconds. | | `CRM_MAX_RETRIES` | `2` | Retry attempts on CRM failure. | | `CRM_AUTH_ENABLED` | `false` | Enable M2M authentication for CRM. | | `CRM_CLIENT_ID` | **Required if enabled** | OAuth client ID for CRM M2M. | | `CRM_CLIENT_SECRET` | **Required if enabled** | OAuth client secret for CRM M2M. | ### External services (Fees) `BTF_FEE_ENABLED` is the master switch. When `false` (default), the plugin builds no Fees adapter and every transfer proceeds with fee=0 and no HTTP call. The other `FEES_*` variables take effect only when `BTF_FEE_ENABLED=true`. | Variable | Default / Required | Description | | -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BTF_FEE_ENABLED` | `false` | Master switch for plugin-fees integration. When `false`, no Fees adapter is built and all transfers run with fee=0 without HTTP calls. Operators running plugin-fees must set this to `true` explicitly. | | `FEES_BASE_URL` | **Required if enabled** | Fees Engine service URL. | | `FEES_TIMEOUT_MS` | `2000` | Fee request timeout in milliseconds. | | `FEES_MAX_RETRIES` | `2` | Retry attempts on fee service failure. | | `FEES_AUTH_ENABLED` | `false` | Enable M2M authentication for Fees. | | `FEES_CLIENT_ID` | **Required if enabled** | OAuth client ID for Fees M2M. | | `FEES_CLIENT_SECRET` | **Required if enabled** | OAuth client secret for Fees M2M. | ### RabbitMQ The plugin can publish transfer lifecycle events to RabbitMQ for downstream consumers. | Variable | Default / Required | Description | | ------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `RABBITMQ_ENABLED` | `false` | Enable transfer lifecycle event publishing. | | `RABBITMQ_URL` | **Required if enabled** | AMQP connection URL. Must use `amqps://` outside development. | | `RABBITMQ_HEALTH_CHECK_URL` | — | HTTP(S) URL of the RabbitMQ management health endpoint. Hostname must match `RABBITMQ_URL`; must use `https://` in production. | | `RABBITMQ_EXCHANGE` | `bank_transfer.lifecycle` | Exchange name for lifecycle events. | | `RABBITMQ_EVENT_SIGNING_SECRET` | **Required if enabled** | HMAC secret for signing published events. Minimum 32 characters. | ### Streaming outbox The streaming subsystem publishes transfer events to a Redpanda/Kafka broker through a transactional outbox. You must enable it for outbound webhook delivery (`WEBHOOK_ENABLED=true` requires `STREAMING_ENABLED=true`). | Variable | Default / Required | Description | | -------------------------------------------- | ----------------------- | --------------------------------------------------------------------------- | | `STREAMING_ENABLED` | `false` | Enable the streaming outbox subsystem. Required for webhook delivery. | | `STREAMING_BROKERS` | **Required if enabled** | Comma-separated list of Kafka/Redpanda broker addresses. | | `STREAMING_CLIENT_ID` | — | Client ID presented to the broker. | | `STREAMING_CLOUDEVENTS_SOURCE` | **Required if enabled** | CloudEvents `source` attribute set on published events. | | `STREAMING_OUTBOX_DISPATCH_INTERVAL_SECONDS` | `30` | Interval (in seconds) between outbox dispatch cycles. Must be > 0. | | `STREAMING_CB_FAILURE_RATIO` | `0.5` | Circuit breaker failure ratio that trips the breaker (must be > 0 and ≤ 1). | | `STREAMING_CB_MIN_REQUESTS` | `10` | Minimum requests in a window before the circuit breaker can trip. | | `STREAMING_CB_TIMEOUT_S` | `30` | Circuit breaker recovery timeout in seconds. | | `STREAMING_CLOSE_TIMEOUT_S` | `30` | Graceful shutdown timeout (in seconds) for the streaming producer. | ### Webhook delivery Outbound webhook delivery requires both RabbitMQ and the streaming outbox. The webhook worker consumes events from a RabbitMQ queue and delivers them to subscriber endpoints. | Variable | Default / Required | Description | | ------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `WEBHOOK_ENABLED` | `false` | Enable outbound webhook delivery. Requires `RABBITMQ_ENABLED=true` and `STREAMING_ENABLED=true`. | | `WEBHOOK_SIGNING_SECRET` | **Required if enabled** | HMAC secret for signing webhook payloads. Minimum 32 characters. | | `WEBHOOK_BROKER_EVENT_SIGNING_SECRET` | — | Separate HMAC secret for verifying broker events. Falls back to `WEBHOOK_SIGNING_SECRET`. | | `WEBHOOK_QUEUE_NAME` | `transfer.webhook.delivery` | RabbitMQ queue name for webhook events. | | `WEBHOOK_DLQ_NAME` | `transfer.webhook.dlq` | Dead-letter queue for failed webhook deliveries. | | `WEBHOOK_DLX_EXCHANGE_NAME` | — | Dead-letter exchange name. When empty, the worker derives `.exchange`. Override to centralize the DLX across plugins. | | `WEBHOOK_DLQ_MESSAGE_TTL_MS` | `0` | `x-message-ttl` on the DLQ in milliseconds. `0` defers to the library default (7 days). Topology changes require deleting the existing DLQ before redeploy. | | `WEBHOOK_DLQ_MAX_LENGTH` | `0` | Maximum DLQ message count. `0` defers to the library default (10000). | | `WEBHOOK_PREFETCH_COUNT` | `20` | RabbitMQ prefetch count. | | `WEBHOOK_DELIVERY_CONCURRENCY` | `8` | Maximum concurrent webhook deliveries per worker. | ### Telemetry (OpenTelemetry) | Variable | Default / Required | Description | | -------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ENABLE_TELEMETRY` | `false` | Enable OpenTelemetry tracing and metrics. | | `OTEL_EXPORTER_OTLP_ENDPOINT` | `localhost:4317` | OTel collector gRPC endpoint. | | `OTEL_TRACES_SAMPLER_ARG` | — | Trace sampling ratio (0.0–1.0). Unset uses the default sampler resolved at telemetry init: `0.1` in production, `1.0` elsewhere. Values outside (0,1] are clamped to 1.0 so misconfiguration cannot silently disable production tracing. | | `BTF_METRICS_PROMETHEUS_ENABLED` | `false` | Expose a dedicated Prometheus scrape endpoint for `btf.*` metrics. When `true`, the listener at `BTF_METRICS_PROMETHEUS_ADDRESS` is started. | | `BTF_METRICS_PROMETHEUS_ADDRESS` | `127.0.0.1:9090` | Listen address for the Prometheus `/metrics` endpoint. Default binds to loopback so a misconfigured pod does not expose unauthenticated metrics to the cluster network. Override (e.g. `0.0.0.0:9090`) only behind a NetworkPolicy or sidecar. | ### License | Variable | Default / Required | Description | | ------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `LICENSE_KEY` | **Required in production** | License key. Required in production environments. | | `LICENSE_SERVICE_ADDRESS` | — | License validation service URL. | | `ORGANIZATION_IDS` | **Required in production** | Same variable as in [Multi-tenancy](#multi-tenancy). Comma-separated list of Midaz organization UUIDs in the licensing scope; the license gateway validates `LICENSE_KEY` against these at startup. | ### Encryption Field-level encryption for sensitive data at rest. Each key must be a hex-encoded 32-byte AES-256 key (64 hex characters). Leave a key empty to disable encryption for that field. | Variable | Default / Required | Description | | ------------------------------------ | ------------------ | ----------------------------------------------------- | | `JD_INCOMING_RAW_XML_ENCRYPTION_KEY` | — | AES-256 key for encrypting incoming JD XML payloads. | | `RECIPIENT_DETAILS_ENCRYPTION_KEY` | — | AES-256 key for encrypting recipient details at rest. | # Webhooks Source: https://docs.lerian.studio/en/rails/ted/jd/ted-webhooks React to Bank Transfer events in real time with webhooks — completed, failed, chargeback, and reconciliation notifications without polling. Webhooks let your system react to transfer events in real time, without polling. The plugin sends a notification to your endpoint when a transfer completes, fails, or needs attention. ## Available events *** Each event lists the transfer types it applies to (in parentheses), when it fires, and the recommended action. ### Transfer lifecycle (TED OUT, P2P) #### `transfer.initiated` (TED OUT) * **Trigger**: the plugin created the TED OUT transfer record after it confirmed the initiation. * **Action**: update the transfer status in your system. Show "transfer in progress" to the customer. #### `transfer.processing_started` (TED OUT) * **Trigger**: the TED OUT transfer entered processing (status path CREATED to PENDING to PROCESSING). * **Action**: show the customer that the transfer is in progress. #### `transfer.rejected` (TED OUT) * **Trigger**: JD SPB rejected the transfer request before acceptance (invalid data or rule violation). * **Action**: notify the customer of the rejection. The plugin already cancelled the fund hold. #### `transfer.completed` (P2P) * **Trigger**: the P2P transfer settled successfully. * **Action**: notify the customer. Generate a receipt. Update the balance display. ### Reconciliation (TED OUT, TED IN) #### `transfer.reconciliation_required` * **Trigger**: a transfer with an unknown outcome moved to reconciliation. * **Action**: track the transfer as pending. Do not assume success or failure. #### `transfer.reconciliation_resolved` * **Trigger**: reconciliation finished and the transfer reached a final outcome. * **Action**: update the transfer to its final status. #### `transfer.reconciliation_exhausted` * **Trigger**: reconciliation stopped after the maximum number of attempts. * **Action**: escalate the transfer for manual operator review. #### `transfer.reconciliation_failed` * **Trigger**: a reconciliation attempt hit a deterministic error, which failed the transfer. * **Action**: treat the transfer as failed and investigate. ### Incoming transfers (TED IN) #### `transfer_incoming.completed` * **Trigger**: the plugin received an inbound TED, found the recipient, and applied the credit. * **Action**: notify the recipient that the funds arrived. Update the balance display. #### `transfer_incoming.chargeback` * **Trigger**: a chargeback message arrived for a completed TED IN (STR0010R2). * **Action**: freeze the credited amount. Start a review with your compliance team. #### `transfer_incoming.undeliverable` * **Trigger**: the plugin could not credit an inbound TED (for example, it did not find the recipient account). * **Action**: investigate the transfer. The plugin may return it to the origin bank. ### Returns and initiation #### `transfer_outgoing.devolution_notified` (TED OUT) * **Trigger**: a return (devolução) arrived for an outgoing transfer. * **Action**: reconcile the returned funds against the original transfer. #### `payment_initiation.created` (TED OUT, P2P) * **Trigger**: the plugin created a payment initiation (the pre-transfer step). * **Action**: optional. Track initiations that await confirmation. For TED OUT, the plugin does not emit `transfer.completed` yet. SPB confirms TED OUT completion asynchronously, and a future release will add this event. Until then, check TED OUT status with the [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) endpoint or the reconciliation endpoint. ## Configuring webhooks *** Webhooks work per tenant. You register a destination in one of two ways. **Self-service API (recommended).** Register one or more HTTPS endpoints through the webhook registration API. The server generates a `signingSecret` on creation and returns it **once**. Store it securely. Use it to verify the signature on every delivered event. You can also list, update, disable, and delete registrations, rotate the signing secret, and look up the accepted event types. The plugin derives the owning tenant from the bearer token, never from a request header. * [Create a webhook registration](/en/reference/midaz/plugins/ted/create-webhook) — `POST /v1/webhooks` * [List webhook registrations](/en/reference/midaz/plugins/ted/list-webhooks) — `GET /v1/webhooks` * [Get](/en/reference/midaz/plugins/ted/get-webhook), [update](/en/reference/midaz/plugins/ted/update-webhook), and [delete](/en/reference/midaz/plugins/ted/delete-webhook) a registration * [Rotate the signing secret](/en/reference/midaz/plugins/ted/rotate-webhook-signing-secret) — `POST /v1/webhooks/{webhookId}/signing-secret/rotate` * [List supported event types](/en/reference/midaz/plugins/ted/list-webhook-event-types) — `GET /v1/webhooks/event-types` **Enabling delivery (operator/env).** Set `WEBHOOK_ENABLED=true` to turn on outbound delivery. Delivery also requires RabbitMQ and the streaming outbox (`STREAMING_ENABLED=true`). Destinations come from the registrations above. There is no single static endpoint environment variable. You tune per-delivery behavior — timeout and max retries — at runtime through systemplane, not through env vars. See [Bank Transfer configuration](/en/rails/ted/jd/ted-configuration). ## Payload structure *** The plugin delivers each event as an HTTPS POST. The request body is the event payload as JSON. The event type and the signature travel in HTTP headers, not in the body. | Header | Value | | --------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `Content-Type` | `application/json`. | | `X-Webhook-Event` | The event type, for example `transfer.completed`. | | `X-Webhook-Timestamp` | Delivery time as a Unix timestamp (seconds). The signature covers this value. | | `X-Webhook-Signature` | HMAC-SHA256 signature over the timestamp and body, keyed with the registration `signingSecret`. Format: `v1,sha256=`. | The body fields depend on the event type. Every payload carries `tenantId`, and transfer-scoped events also carry `transferId`. Amounts are decimal strings in the account currency, not centavos (for example, `100.00`). Here is an example body for `transfer.completed` on a P2P transfer: ```json theme={null} { "transferId": "019c96a0-ab10-7cde-f1a2-0e1f2a3b4c5d", "initiationId": "019c96a0-9a01-7bcd-e0f1-2a3b4c5d6e7f", "tenantId": "019c96a0-0a98-7287-9a31-786e0809c769", "ledgerId": "019c96a0-1b20-7def-a1b2-c3d4e5f60718", "senderAccountId": "019c96a0-2c30-7ef0-b2c3-d4e5f6071829", "recipientAccountId": "019c96a0-3d40-7f01-c3d4-e5f60718293a", "midazTransactionId": "019c96a0-cd10-7eee-bbbb-3333bbbb4444", "confirmationNumber": "20260121001", "status": "COMPLETED", "transferType": "P2P", "amount": "100.00", "feeAmount": "0.00", "totalAmount": "100.00", "completedAt": "2026-01-21T17:35:00Z" } ``` The `transfer.completed` payload carries the amounts, the accounts, and the `midazTransactionId`. For events with a smaller payload, or to read the full transfer record, fetch the transfer from [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) with its `transferId`. Payload fields differ per event type. To read every field of a transfer, use the [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) endpoint. ## Handling delivery failures *** Your endpoint must respond with a 2xx status within 5 seconds (the `webhook.timeout_ms` default). If it does not, the plugin retries the delivery with exponential backoff and full jitter. After the first attempt, the plugin makes up to 3 more attempts (the `webhook.max_retries` default), which is 4 delivery attempts in total. The backoff base is 1 second and doubles per attempt. Full jitter applies to each delay: | Attempt | Delay before this attempt | | ----------- | ------------------------- | | 1 (initial) | Immediate | | 2 | Random in `[0, 1000 ms]` | | 3 | Random in `[0, 2000 ms]` | | 4 | Random in `[0, 4000 ms]` | After all attempts fail (4 by default), the event moves to a dead-letter queue (DLQ). Set alerts on the DLQ to catch persistent delivery failures early. Tune `webhook.max_retries` through systemplane if your endpoint needs a longer or shorter retry budget. The `webhook.retry_backoff_ms` knob controls the broker reconnect backoff, not the per-delivery HTTP retry schedule above. For reliable delivery, follow these rules: * Respond within 5 seconds. * Use HTTPS with a valid certificate. * Return 200 even for events you ignore. * Move heavy processing to a background queue. Keep the webhook handler fast. ## Idempotency *** Your endpoint may receive the same event more than once. Use the `transferId` from the body and the `X-Webhook-Event` header to deduplicate. If you already processed that combination, return 200 and take no further action. ## For developers *** For signature validation code (JavaScript, Python, Go), retry implementation, and the full integration checklist, see the [Bank Transfer developer guide](/en/rails/ted/jd/ted-developer-guide). # Best practices Source: https://docs.lerian.studio/en/rails/ted/ted-best-practices Apply proven patterns when integrating Bank Transfer — show fees before confirmation, handle settlement windows, and reduce customer complaints. This guide covers the key decisions your team makes when you integrate Bank Transfer. It also gives the best practices for a reliable and compliant customer experience. ## Product decisions *** These are choices your product team makes in the customer-facing experience. They directly affect customer satisfaction and support volume. ### Show the fee before the customer confirms The `initiate` step returns the fee amount before any funds move. Use this window to show a clear confirmation screen: ``` Confirm transfer Recipient: Maria Silva — Bradesco (237) Amount: R$ 1,000.00 Fee: R$ 1.50 ───────────────────── Total: R$ 1,001.50 [ Cancel ] [ Confirm ] ``` This reduces complaints and cancellations from customers surprised by fees after the fact. ### Handle operating hours gracefully TED OUT is available Monday to Friday, 06:30–17:00 (Brasília time). When a customer starts a transfer outside these hours, do not show only an error. Tell them when they can try again: ``` TED transfers are available Monday to Friday, 06:30 to 17:00. Next available time: Monday at 06:30. ``` To avoid unnecessary round trips, validate operating hours on the client side before you call the API. You do not need to maintain your own holiday list. The plugin blocks weekends and BACEN holidays automatically. The runtime source of truth is the `bacen_holidays` table, which the plugin seeds for 2026–2028. The daily refresher runs by default and re-applies the built-in seed. It does not fetch live from ANBIMA, because ANBIMA publishes only a legacy spreadsheet that machines cannot read. The seed stays the authoritative source until that changes. When a holiday rejects a transfer, show that reason to the customer. Do not replicate the calendar on the client. Trust the plugin as the source of truth to avoid inconsistencies over time. ### Communicate transfer limits before customers hit them Show the customer's remaining daily limit in your transfer UI. Show it before they attempt a transfer that the plugin rejects. For example: ``` Daily limit: R$ 50,000.00 Used today: R$ 45,000.00 Available: R$ 5,000.00 ``` ### Show confirmation receipts after completion After a TED OUT or P2P transfer completes, display — or offer to download — a receipt with: * Transfer date and time * Sender and recipient details * Amount, fee, and total * `confirmationNumber` (customer-facing reference) * `controlNumber` (JD SPB reference, for TED OUT only) When you provide this information early, you reduce "did my transfer go through?" support contacts. ### Keep customers informed in real time Use webhooks to push transfer status updates to your UI as they happen. Do not make customers refresh or wonder if their transfer went through. See [TED webhooks](/en/rails/ted/jd/ted-webhooks) for setup. ## Compliance decisions *** These are requirements that apply to your integration regardless of your product choices. ### LGPD and personal data Transfer records contain personal data — customer names, CPF/CNPJ, and bank details. Make sure your privacy policy explicitly covers financial transaction data. Do not log CPF/CNPJ in plain text. Mask it in interfaces as `***.***.***-00`. A dedicated anonymization endpoint for LGPD right-to-erasure requests will arrive in a future release. Until then, coordinate anonymization requests with your database administration team. ### Data retention The plugin never deletes or expires transfer records, so you own their retention. Retain transfer and audit data for at least **5 years**, per BACEN record-keeping requirements for financial institutions. | Data type | Retention period | | ------------------- | ---------------------------------- | | Transaction records | 5 years (BACEN requirement) | | Application logs | 90 days | | Audit data | 5 years (anonymized after 2 years) | ### Audit trail and reconciliation Every transfer generates two reference numbers you must store: | Field | What it is | When to use | | -------------------- | ------------------------------- | --------------------------------------- | | `transferId` | Internal Lerian identifier | API lookups, support cases | | `confirmationNumber` | User-readable reference | Receipts, customer communication | | `controlNumber` | JD SPB reference (TED OUT only) | BACEN audit trail, regulatory reporting | Keep both the `transferId` and `confirmationNumber` in your own records for reconciliation. For TED OUT, also store the `controlNumber`. ### Operating hours BACEN mandates that TED operates Monday to Friday, 06:30–17:00 (Brasília time, UTC-3). The plugin enforces this window by default. An operator can adjust the open and close times at runtime through systemplane, within BACEN limits. Treat 06:30–17:00 as the norm and build your UX around it. See [Handle operating hours gracefully](#handle-operating-hours-gracefully) above. P2P transfers are not subject to operating hour restrictions and work 24/7. ## Integration checklist *** Before you go live, verify the following: * [ ] **Idempotency keys on all write operations** — Send a UUID v4 `X-Idempotency` header on every call to `initiate`, `process`, and `cancel`. This prevents duplicate transfers from retries or double-clicks. * [ ] **Webhook endpoint live before launch** — Deploy your webhook endpoint and make it reachable before you go live. Transfer events start firing immediately on the first real transaction. * [ ] **24-hour expiry handled** — An initiated transfer expires if the customer does not confirm it within 24 hours. If your flow allows a customer to start a transfer and return later, handle the expiry case explicitly. * [ ] **Exponential backoff on 5xx errors** — Implement retry with backoff (e.g., 2s, 4s, 8s) when the response is `503` or `500`. JD SPB unavailability surfaces as `503` with a raw JD vendor code (`TRANSPORT`, `ACE95`, …). Midaz ledger unavailability surfaces as `BTF-2000`. Do not retry immediately in a loop. * [ ] **Operating hours validated client-side** — Check hours in the UI before you call the API. This reduces failed API calls and gives a better customer experience. * [ ] **Both `transferId` and `confirmationNumber` stored** — Required for reconciliation and audit. For TED OUT, also store `controlNumber`. ## Error handling *** Use these error scenarios to map API errors to customer-friendly messages and define the correct recovery path. | Scenario | Customer-facing message | Recovery | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | | **Outside operating hours** (`BTF-0010`) | "TED transfers are available Monday–Friday, 06:30–17:00. Next available time: \[date/time]." | Recoverable — wait for the next window | | **Insufficient balance** (`BTF-2003`, HTTP `422`) | "Your account doesn't have enough balance for this transfer." | Recoverable — customer adds funds or reduces the amount | | **Daily limit reached** (`BTF-0011`) | "You've reached your daily transfer limit of R\$ \[X]. Limit resets at midnight." | Recoverable — wait for reset | | **Invalid recipient** (`BTF-0500`) | "Destination account not found. Please check the account details and try again." | Recoverable — customer corrects details | | **Service unavailable** (`TRANSPORT`, HTTP `503`, raw JD code) | "Transfer service is temporarily unavailable. Please try again in a few minutes." | Recoverable — retry with backoff | | **Duplicate transfer** (`BTF-0012`) | "An identical transfer was sent recently. If this was intentional, wait a moment and try again." | Conditional — wait for the deduplication window to clear | For the complete list of error codes and their meanings, see the [TED error list](/en/reference/midaz/plugins/ted/ted-error-list). # Glossary Source: https://docs.lerian.studio/en/rails/ted/ted-glossary Reference glossary for the Bank Transfer plugin — BACEN, SPB, STR, ISPB, PSTI, ISO 20022 account codes, and JD SPB message types like STR0008. Quick reference for terms and acronyms used in the Bank Transfer plugin documentation. ## Regulatory and infrastructure terms *** | Term | Definition | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **ANBIMA** | Brazilian Financial and Capital Markets Association. Upstream source of the national bank-holiday table consumed by the plugin's holiday refresher to populate the `bacen_holidays` calendar. | | **BACEN** | Banco Central do Brasil — the Central Bank of Brazil. Regulates the financial system and operates SPB. | | **SPB** | Sistema de Pagamentos Brasileiro — Brazil's Payment System, the infrastructure for all interbank settlements. | | **STR** | Sistema de Transferência de Reservas — Reserve Transfer System, the real-time settlement component of SPB. | | **RSFN** | Rede do Sistema Financeiro Nacional — the secure national financial network connecting BACEN and financial institutions. | | **PSTI** | Provedor de Serviço de Tecnologia da Informação — an IT service provider authorized by BACEN to operate RSFN connectivity infrastructure. JD Consultores is a PSTI. | | **ISPB** | Identificador de Sistema de Pagamentos Brasileiro — 8-digit code that uniquely identifies each financial institution in SPB. | | **STR0008** | The BACEN message format for initiating a TED transfer. Sent from the originating institution to SPB. | | **STR0008R2** | The response/confirmation message for STR0008. Contains the settlement result. | | **STR0010R2** | The chargeback message format. Sent when a TED must be returned to the origin bank (e.g., recipient not found). | | **Devolução / `devolutionCode`** | Return of a TED. When a previously-sent TED OUT is returned (STR0010R2), the plugin records a devolution as a return TED IN that carries the BACEN reason code (`devolutionCode`) and an `originalTransferId` linking back to the original TED OUT. (An inbound TED that cannot be credited is separately returned to the origin bank.) | | **JD / JD SPB** | JD Consultores — the PSTI used by Lerian to route TED messages to SPB. | | **JD parse failure** | Inbound JD message that could not be parsed (malformed XML, unknown message type, schema mismatch). Persisted in `jd_incoming_parse_failures` for manual triage. Distinct from undeliverable transfers and from the webhook DLQ. | | **NumCabSeq** | Sequential message identifier assigned by JD SPB to each STR0008 submission. Used to track and query transfer status. | | **NumCtrlSTR** | BACEN clearing control number assigned by STR (the Reserve Transfer System) once a message is accepted into the SPB clearing flow. Persisted on the transfer as `clearing_control_number`. Distinct from `NumCabSeq`, which is the JD-assigned message sequence on submission. | ## Account type codes (ISO 20022) *** | Code | Account Type | | -------- | --------------------------------------------------- | | **CACC** | Current account (conta corrente) — most common type | | **CPAY** | Payment account (conta de pagamento) | | **SLRY** | Salary account (conta salário) | | **SVGS** | Savings account (conta poupança) | ## Transfer types *** | Term | Definition | | ----------- | ---------------------------------------------------------------------------------------------------------- | | **TED OUT** | Outbound transfer — your client sends funds to an account at another bank. | | **TED IN** | Inbound transfer — your institution receives funds from another bank, automatically detected and credited. | | **P2P** | Internal transfer — between two accounts at the same institution (same ISPB). Faster and typically free. | | **D+0** | Same-day settlement — TED is a D+0 instrument when submitted before 17:00 Brasília time. | ## Plugin-specific terms *** | Term | Definition | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **PaymentInitiation** | The entity created by `POST /v1/transfers/initiate`. Holds fee calculation and expires after 24 hours. | | **initiationId** | UUID returned by the initiate endpoint. Required to confirm a transfer via `POST /v1/transfers/process`. | | **X-Idempotency** | Required header for safe retries. The plugin caches the response under the key and replays it for 24 hours. The plugin rejects a repeat request that reuses the key with a different body (fingerprint mismatch). | | **Cashin fee** | Fee deducted from a received TED IN amount. Configured per organization via Fees Engine. | | **Cashout fee** | Fee added to a TED OUT amount. The sender's account is debited for amount + fee. | | **Reconciliation** | Process to match transfers in unknown state (PROCESSING with no confirmation) against JD SPB records. | | **DLQ** | Webhook dead-letter queue — outbound webhook notifications that cannot be delivered after all retries are moved here for manual review. | | **Signing artifact** | Canonical TED OUT payload frozen during `POST /v1/transfers/signing/prepare` and persisted in `jd_outbound_signing_artifacts`. Consumed by external HSM/signer integrations to produce a detached signature without the plugin holding the private key. | | **Systemplane** | Runtime-config admin plane exposed via `/system/:namespace` and `/system/:namespace/:key`. Holds hot-reloadable settings that take effect without a redeploy. Distinct from bootstrap environment variables, which are read only at startup. | | **Undeliverable transfer** | Inbound TED that was parsed successfully but could not be credited (e.g., recipient account not found in the CRM). Persisted in `undeliverable_incoming_transfers` and may trigger an automatic devolução. Distinct from a JD parse failure and from the webhook DLQ. | # Receive (TED IN) Source: https://docs.lerian.studio/en/rails/ted/ted-in Receive TED transfers automatically — the plugin polls SPB, validates recipient Accounts, and credits funds with no manual intervention. TED IN lets your institution receive transfers from any Brazilian bank automatically. Your team takes no action — the plugin detects, validates, and credits each transfer. When a customer at another bank sends a TED to your institution, the funds reach the recipient's account within minutes. ## How it works *** 1. A customer at another bank starts a TED transfer to one of your institution's accounts 2. Every 60 seconds (default `JD_POLL_INTERVAL_SECONDS`), the plugin polls the JD SPB network for new incoming transfers 3. The plugin looks up the recipient account in your CRM by the document number in the transfer message 4. The plugin credits the recipient account automatically, minus the cashin fee if you configured one TED IN flow diagram ## Detection and processing timeline *** The stages below show what happens after the origin bank sends the transfer: | Stage | What happens | | ------------ | --------------------------------------------------------------------------------- | | Submission | The origin bank submits the transfer to the SPB network | | Detection | The plugin fetches the transfer on its next poll cycle. Status becomes `RECEIVED` | | Validation | The plugin confirms the recipient account. Status becomes `PROCESSING` | | Credit | The plugin credits the recipient account. Status becomes `COMPLETED` | | Notification | The plugin sends the webhook to your system | **Typical time:** The credit completes within one poll cycle. With the default 60-second poll interval, the funds land in about a minute. ## Transfer states *** | State | What it means for the recipient | | ------------ | ------------------------------------------------------------------------------------------------------------------- | | `RECEIVED` | The plugin detected the transfer on the network and started processing | | `PROCESSING` | The plugin confirmed the recipient account and applies the credit | | `COMPLETED` | The funds reached the recipient's account | | `FAILED` | The plugin could not apply the credit (for example, a Midaz rejection), or a chargeback reversed a completed credit | ## Receiving fee (cashin) *** Your organization can charge a fee on incoming transfers. When you enable it, the plugin deducts the fee from the amount before it credits the recipient. The recipient receives the net amount. You set the fee amount and configuration per organization through the Fees Engine. Formula: `credited amount = transfer amount − fee` Example: a R$1,000.00 transfer with a R$2.50 fee credits R\$997.50 to the recipient's account. This is the opposite of TED OUT, where the plugin adds the fee on top and the sender pays more. ## What happens when a recipient isn't found *** If the plugin cannot match the document number in the incoming transfer to an account in your CRM, it returns the transfer to the origin bank automatically. The sending customer gets their money back. Your team takes no action, and no funds go unaccounted for. The plugin records the inbound message as an undeliverable incoming transfer in the `undeliverable_incoming_transfers` store. It then dispatches a devolução (STR0010 return) to the origin bank. This path does not create a credited transfer record set to `FAILED`. ## Querying received transfers *** Use the [List Transfers](/en/reference/midaz/plugins/ted/list-transfers) endpoint to retrieve all incoming transfers. Filter by `type=TED_IN` to view only received transfers. **Endpoint:** GET /v1/transfers **Response (key fields):** ```json theme={null} { "items": [ { "transferId": "019c96a0-ab20-7def-a1b2-1f2a3b4c5d6e", "type": "TED_IN", "status": "COMPLETED", "amount": 5000.00, "feeAmount": 0.00, "totalAmount": 5000.00, "createdAt": "2026-01-21T10:15:00-03:00", "updatedAt": "2026-01-21T10:15:30-03:00" } ], "pagination": { "limit": 50, "offset": 0, "returned": 1, "totalCount": 150, "hasNextPage": true } } ``` For full query parameter options, see the [List Transfers](/en/reference/midaz/plugins/ted/list-transfers) reference. ## Operational endpoints *** Three operator endpoints control the TED IN polling loop. They target scripts and runbooks, not end-user traffic. | Endpoint | Purpose | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /v1/transfers/ted-in/poll` | Manually trigger the JD poller that normally runs on a 60s cron. Use it after an incident window or to validate JD connectivity. The route is tenant-scoped but does not require `X-Organization-Id`, because it resolves the tenant from the authenticated context. It requires `X-Idempotency` for safe retries. A same-key retry replays the cached response instead of reading JD's destructive queue again. | | `POST /v1/transfers/ted-in/replay` | Replay persisted, unprocessed TED IN backlog rows that the plugin already fetched from JD. This route does not read JD again. It requires `X-Organization-Id` for Midaz organization scope and `X-Idempotency` for safe retries. It still resolves the tenant from the authenticated context. | | `POST /v1/transfers/ted-in/resume` | Clear the fail-closed receive latch and re-arm a poller that auto-recovery cannot revive — a panicked multi-tenant child or a single-tenant poller past the panic hard-cap. Higher-privilege than `/poll` because it re-opens JD destructive-read consumption. Resume never bypasses the money-path gate: if the current latch has no durable reconciliation gap yet, the request is refused with `409`. Verify pending gaps with [List TED IN Reconciliation Gaps](/en/reference/midaz/plugins/ted/list-ted-in-reconciliation-gaps) first, then optionally pass `{ "acknowledge": true, "note": "..." }` to mark the gap resolved in the same call. A healthy, not-latched poller returns `resumed: false` — the route is a safe idempotent no-op. | For request body, response, status codes, and error codes, see the [TED OpenAPI specification](/en/openapi/v3-current/ted.yaml) (operations `triggerTEDInPoller`, `replayTEDInPoller`, and `resumeTEDInPoller`). ## Three distinct dead-letter paths *** The plugin uses three separate failure stores. They are not interchangeable, and you must monitor each one independently: * **JD parse failures** — the plugin stores these in `jd_incoming_parse_failures`. The message arrived from JD, but the plugin could not interpret it (malformed XML, unknown message type). This store needs manual triage. * **Undeliverable inbound transfers** — the plugin stores these in `undeliverable_incoming_transfers`. Parsing succeeded, but the plugin could not apply the credit (for example, it did not find the recipient account). This path can trigger an automatic devolução to the origin bank. * **Webhook DLQ** — the retry queue for failed outbound webhook deliveries, at `/v1/webhooks/dlq`. It does not relate to TED IN ingestion. This is the outbound event channel to integrating clients. ## Webhooks *** Configure a webhook to get real-time notifications when transfers arrive. The `transfer_incoming.completed` event fires as soon as the plugin credits a transfer. See [Webhooks](/en/rails/ted/jd/ted-webhooks) for setup and event payload details. ## Reconciliation *** For accounting and finance reconciliation, each transfer record includes these fields: | Field | Usage | | --------------- | ------------------------------------------------------------------------------ | | `controlNumber` | JD SPB control number — unique per transfer, used for interbank reconciliation | | `transferId` | Lerian internal identifier | | `createdAt` | Timestamp when the plugin detected the transfer | | `completedAt` | Timestamp when the plugin credited the funds | The plugin persists transfer records for reconciliation and audit. ## Processing guarantees *** The plugin makes sure that it never loses a transfer and never credits one twice: * **No duplicate credits** — each transfer message carries a unique sequence number. The plugin rejects any attempt to process the same message twice. * **Automatic retry on failure** — the plugin retries transient errors (such as a momentary service interruption) with exponential backoff before it records any failure state. * **Dead-letter queue for unresolvable issues** — if the plugin cannot process a transfer after all retries, it moves the transfer to a dead-letter queue for manual review. The plugin never drops a transfer silently. # How the plugin works Source: https://docs.lerian.studio/en/rails/ted/ted-in-lerian Understand how the Bank Transfer plugin handles SPB communication, fee calculation, settlement, and webhook notifications across the full transfer lifecycle. The Bank Transfer plugin runs Brazil's TED rail through JD Consultores. JD provides the regulated SPB connectivity. The plugin drives each transfer from fee calculation to settlement confirmation, so your team does not call JD directly. ## What the plugin handles for you *** * Sends outgoing TEDs to any Brazilian bank (TED OUT) * Receives and credits incoming TEDs (TED IN) * Processes instant internal transfers between accounts (P2P) * Calculates and applies fees before the customer confirms * Detects and blocks duplicate transfers within a configurable window * Validates BACEN business days against the `bacen_holidays` calendar (static seed for 2026–2028, live ANBIMA refresher pending) * Signs messages with your institution's digital certificate, as BACEN requires * Retries failed operations automatically * Notifies your system through webhooks when a transfer changes status ## How TED OUT works *** TED OUT is a confirm-first flow. The customer reviews the fee before the plugin sends the transfer. ### Step 1 — Initiate Endpoint: [POST /v1/transfers/initiate](/en/reference/midaz/plugins/ted/initiate-transfer) Your system calls the plugin with the transfer details: amount, recipient, and sender account. The plugin validates the sender account, checks operating hours, and runs duplicate detection. The plugin calculates the fee and returns an `initiationId` with the calculated amounts. Your system shows the fee to the customer for confirmation. The initiation is valid for 24 hours. If the customer does not confirm within that window, it expires. ### Step 2 — Prepare signing (optional) Endpoint: [POST /v1/transfers/signing/prepare](/en/reference/midaz/plugins/ted/prepare-transfer-signing) Use this step only when your tenant signs outside the plugin (external signing mode). The plugin freezes the canonical STR0008 payload and returns the exact bytes and hash to sign. Your system signs the payload and passes the signature to the process step. When the plugin signs with your local key (the default), skip this step. ### Step 3 — Process Endpoint: [POST /v1/transfers/process](/en/reference/midaz/plugins/ted/process-transfer) Your system calls the plugin with the `initiationId` to confirm. The plugin checks the daily and monthly limits and the available balance. The plugin reserves funds in Midaz (a hold) and sends the signed message to JD Consultores. JD routes the transfer to the destination bank over the SPB network. The plugin receives the settlement confirmation from JD and finalizes the records. Your system receives a webhook with the final status. ## How TED IN works *** 1. An external bank sends a TED to your institution through JD Consultores. 2. The plugin polls JD every 60 seconds (default) to detect new incoming transfers. 3. The plugin validates the recipient against the CRM to find the correct account. 4. The plugin credits the account in Midaz and creates a completed transfer record. 5. Your system receives a webhook that confirms the credit. TED IN polling is off by default. To turn it on, set `JD_POLLING_ENABLED` after you configure the JD credentials and the polling worker. ## How P2P works *** P2P transfers move funds between two accounts in the same organization. They do not use the SPB network, and settlement is instant. 1. Your system calls the plugin with the sender account, the recipient account, and the amount. 2. The plugin calculates the fee, if configured, and presents it for confirmation. 3. After confirmation, the plugin runs the transfer in Midaz. 4. Both accounts update immediately, and your system receives a webhook. ## Deployment models *** The TED plugin supports two deployment models. The `DEPLOYMENT_MODE` environment variable selects the model. ### SaaS (managed by Lerian) In SaaS deployments, Lerian manages the integration with JD Consultores, including credential and certificate maintenance. Your team configures only business-level settings through the Admin API, such as transaction limits, fees, and webhooks. You do not manage infrastructure or connections. In `saas` mode, the plugin runs as a multi-tenant service. The multi-tenancy platform service resolves the tenant identity, JD credentials, webhook secrets, and selected settings at runtime. This mode requires the `MULTI_TENANT_*` variables and `AWS_REGION`, and the plugin uses them actively. ### BYOC (bring your own credentials) In BYOC deployments, your institution provides the JD Consultores credentials and the RSA private key that signs messages. Your DevOps team sets these values through environment variables. You keep full control of the JD connection, and the plugin runs entirely in your own infrastructure. BYOC is the default deployment mode (`byoc`). The plugin loads all configuration from environment variables at startup, including JD credentials, webhook secrets, and fee settings. ### Organization resolution The plugin identifies the Midaz organization from the required `X-Organization-Id` header on organization-scoped API routes. Some background processes do not receive request headers, such as the TED IN poller and the reconciliation workers. For these, the plugin falls back to the `ORGANIZATION_ID` environment variable. In `byoc` mode, the plugin ignores the multi-tenancy variables (`MULTI_TENANT_*`) and `AWS_REGION`. See [TED configuration](/en/rails/ted/jd/ted-configuration) for the complete list of supported environment variables. ## Integration with Midaz *** All financial movements go through the Midaz ledger. The plugin creates a Midaz transaction for every transfer: * **TED OUT** — the plugin holds the funds at the process step (via `pending: true`), then debits them when JD confirms settlement. * **TED IN** — the plugin credits the funds after JD validates and confirms the transfer. * **P2P** — a single Midaz transaction debits the sender and credits the recipient atomically. Every transfer maps to one transaction record in your Midaz ledger. See [TED data and reporting](/en/rails/ted/jd/ted-data-model) for the fields available for reconciliation. ## For developers *** ### Architecture The plugin uses a Hexagonal (Ports and Adapters) architecture with CQRS. This design keeps business logic separate from infrastructure. You can add a new adapter, such as a different SPB provider, without a change to the core behavior. Ted Architectural Pattern ### Duplicate detection The plugin builds a duplicate-detection fingerprint for each transfer. The fingerprint covers `senderAccountId`, the recipient details (ISPB, branch, account, holder document), the amount, and the purpose. The plugin stores the fingerprint in Redis with a configurable TTL (default 300 seconds, set by `DUPLICATE_GUARD_TTL_SEC`). The organization is not part of the fingerprint. Tenant isolation comes from the Redis key prefix. The plugin rejects a duplicate request within the window with `409 Conflict` and error code `BTF-0012`. ### Multi-tenant data isolation Tenant isolation comes from per-tenant database resolution in the multi-tenancy platform. The plugin reads the `tenantId` from the JWT claim or the authenticated context, never from `X-Organization-Id`. The Redis cache uses per-tenant key prefixes (`tenant:{tenantId}:{key}`). Business tables use the Midaz organization fields only for business-scope authorization inside the resolved tenant. ### Observability The plugin exposes Prometheus metrics, structured JSON logs, and OpenTelemetry traces. It also exposes unauthenticated liveness and readiness probes for Kubernetes orchestration. These probes matter mainly for BYOC deployments. The liveness and readiness probes are unauthenticated by design, for K8s probe compatibility. In BYOC deployments, restrict access to these probes at the network level, for example with ingress rules or security groups. This keeps internal dependency status off the public internet. # Send (TED OUT) Source: https://docs.lerian.studio/en/rails/ted/ted-out Send TED transfers to any Brazilian bank with a two-step initiate-and-confirm flow — fee preview, same-day settlement, and BACEN compliance. TED OUT lets your customers send money to any account at any Brazilian bank. The two-step flow, initiate then confirm, lets the customer review the fee before the system commits the transfer. ## How it works for your customer *** **Step 1 — Enter details and review fee** The customer provides the recipient's bank details and the amount. The system calculates the fee and returns the total cost before it debits anything. The customer sees exactly what they will pay. **Step 2 — Confirm and send** After the customer confirms, the system debits the funds (amount + fee) from their account. It submits the transfer through JD's SPB gateway to the BACEN network and issues a confirmation number at once. ## Prerequisites *** Before you initiate a transfer: * The sender must have a registered account in CRM. * The sender's balance must cover the transfer amount plus the applicable fee. * You must request the transfer on a business day, between 06:30 and 17:00 (Brasília time). ## Step 1: Initiate the transfer *** The customer submits the recipient's details and the amount. The system validates the request, calculates the fee, and creates a transfer intent valid for 24 hours. The system moves no funds at this stage. See the full request specification in the [Initiate Transfer](/en/reference/midaz/plugins/ted/initiate-transfer) reference. **Endpoint:** POST /v1/transfers/initiate **Response (key fields):** ```json theme={null} { "initiationId": "019c96a0-aa10-7abc-d1e2-8c9d0e1f2a3b", "feeAmount": 1.50, "totalAmount": 1001.50, "status": "PENDING_CONFIRMATION", "expiresAt": "2026-02-06T15:30:00-03:00" } ``` **Fee direction (cash-out):** For TED OUT and P2P transfers, the plugin adds the fee on top of the transfer amount, so `totalAmount = amount + feeAmount`. The plugin debits the sender's account for the full total. TED IN works the other way and deducts the fee from the received amount. The transfer intent expires 24 hours after initiation. If the customer does not confirm within that window, create a new initiation. ## Step 2: Confirm the transfer *** After the customer reviews the fee and confirms, submit the `initiationId` to process the transfer. The system provisions the funds and sends the payment to the BACEN network. Most integrations process the transfer with only the `initiationId`. Some tenants sign TED OUT payloads outside the plugin. These tenants first call `POST /v1/transfers/signing/prepare`. They then send `signingArtifactId`, `payloadHash`, and `signature` with the same `initiationId`. See the full request specification in the [Process Transfer](/en/reference/midaz/plugins/ted/process-transfer) reference. **Endpoint:** POST /v1/transfers/process **Response (key fields):** ```json theme={null} { "transferId": "019c96a0-ab10-7cde-f1a2-0e1f2a3b4c5d", "confirmationNumber": "20260205001", "status": "PROCESSING", "feeAmount": 1.50, "totalAmount": 1001.50 } ``` ## Settlement timeline *** The system provisions the funds and sends the transfer to the BACEN network. Status: `PROCESSING`. The destination bank confirms settlement. Status: `COMPLETED`. The plugin dispatches a `transfer.completed` webhook to your system. TED settles same-day (D+0) for transfers submitted before 17:00 Brasília time. ## Operating hours *** Request TED transfers only on business days, 06:30–17:00 (Brasília, UTC-3, by default). The plugin rejects any transfer outside this window at once and moves no funds. It also rejects BACEN bank holidays. The error response gives the next available time. ## Error handling *** The plugin always accounts for your customer's money when something goes wrong: The receiving institution declines the transfer. The plugin releases the provisioned funds at once and returns the full amount, including the fee, to the sender's balance. Status: `REJECTED`. The plugin sends a `transfer.rejected` webhook. The plugin retries automatically, up to three attempts by default. If the outcome stays unknown after retries — a 5xx or a timeout from JD SPB — the plugin does not auto-revert the transfer. It preserves the hold and hands the transfer to the reconciliation worker, which resolves it against the ledger. If reconciliation runs out of attempts, the plugin flags the transfer for manual operator review (`MANUAL_REVIEW`). The plugin never loses the funds, but resolution may take time. A clear 4xx rejection differs: it reverts the hold at once (see the rejected case above). The destination bank can return the funds after settlement, for example on account closure or a regulatory hold. The plugin reverses the amount in your ledger as a separate transaction. This return sits outside the transfer lifecycle. The original transfer record stays `COMPLETED`, and the plugin creates a new reversal record. Every mutating transfer request must include an `X-Idempotency` header (max 255 characters). Reuse the same key when you retry a request to avoid duplicate submissions. See [Retries and idempotency](/en/reference/retries-idempotency) for details. For a full list of error codes, see the [TED error reference](/en/reference/midaz/plugins/ted/ted-error-list). ## Check transfer status *** Track the progress of a transfer at any time. **Endpoint:** [GET /v1/transfers/](/en/reference/midaz/plugins/ted/retrieve-transfer) **Response (key fields):** ```json theme={null} { "transferId": "019c96a0-ab10-7cde-f1a2-0e1f2a3b4c5d", "confirmationNumber": "20260205001", "senderAccountId": "019c96a0-a910-7abc-d1e2-8c9d0e1f2a3b", "type": "TED_OUT", "status": "COMPLETED", "amount": 1000.00, "feeAmount": 1.50, "totalAmount": 1001.50, "createdAt": "2026-02-05T15:30:00-03:00", "completedAt": "2026-02-05T15:35:12-03:00" } ``` ## Cancel a transfer *** You can cancel a transfer while it is in `CREATED` or `PENDING` status, before the plugin submits it to the network. **Endpoint:** [POST /v1/transfers//cancel](/en/reference/midaz/plugins/ted/cancel-transfer) You cannot cancel a transfer in `PROCESSING` or a later state. After the plugin submits it to the BACEN network, the transfer must complete or fail before any reversal. ## Common ISPB codes *** *Last verified: 2026-02-06. Values are subject to change.* | Bank | ISPB | | --------------- | -------- | | Banco do Brasil | 00000000 | | Bradesco | 60746948 | | Itaú | 60701190 | | Santander | 90400888 | | Caixa Econômica | 00360305 | | Nubank | 18236120 | | Inter | 00416968 | For a complete list, consult the [ISPB directory](https://www.bcb.gov.br/pom/spb/estatistica/port/AODO.asp) on the Central Bank website. # What is Bank Transfer? Source: https://docs.lerian.studio/en/rails/ted/ted-overview Move money between Brazilian banks the same business day with Lerian's Bank Transfer plugin: outbound, inbound, and P2P transfers with BACEN compliance built in. TED (Transferência Eletrônica Disponível) is Brazil's interbank transfer system. Money moves between any two banks on the same business day. Lerian reaches TED in two ways. **Lerian SPB** is Lerian's own messaging with the Brazilian Payment System — a direct connection with no connectivity partner in the path, and it covers the full SPB messaging contract, not only TED. The **Bank Transfer plugin** connects your institution through the connectivity partner JD. Both paths give your institution a production-ready TED integration: outbound and inbound transfers, internal P2P, fee handling, and BACEN compliance. **Regulatory reference** This page presents a practical overview of how TED works. For complete technical, legal, and regulatory details, consult the [official documentation](https://www.bcb.gov.br/estabilidadefinanceira/spb) published by the **Central Bank of Brazil (BACEN)**. ## What you can build *** * **Payroll disbursements** — send salaries or contractor payments directly to employees' accounts at any Brazilian bank * **Supplier and vendor payments** — pay invoices and settle B2B obligations without manual bank portal access * **Customer withdrawals** — let customers move funds out of your platform to their external bank accounts on demand * **Internal fund movement** — move money between accounts in your own institution, with no external routing (P2P) ## Transfer types *** | Type | What it means for your business | | ----------- | ---------------------------------------------------------------------------------------- | | **TED OUT** | Your customer sends money to an account at another bank | | **TED IN** | Your institution receives money from another bank automatically | | **P2P** | Money moves between two accounts within your institution — same-day, no external routing | ## How it works *** 1. **Initiate** — you enter the recipient's bank details and the amount. The plugin shows the fee first 2. **Confirm** — you authorize the transfer, and the plugin reserves the funds in Midaz 3. **Send** — the plugin signs the payment and dispatches it over the BACEN STR network to the destination bank 4. **Settle** — the recipient's bank credits the funds, and you receive the JD SPB control number as confirmation ## Operating hours and limits *** What your customers can expect: | Rule | Details | | ------------------- | ------------------------------------------------------------------- | | Operating window | Monday to Friday, 06:30–17:00 Brasília time (default, configurable) | | Settlement SLA | Same day (D+0) for transfers confirmed within the window | | Weekends / holidays | TED is unavailable; schedule transfers for the next business day | Outside the operating window, the plugin rejects the transfer and moves no funds. The error names the next available time. ## What the plugin handles for you *** Your team does not need to build or maintain: * **Fee calculation** — the plugin computes fees through Fees Engine at initiation, before you confirm * **Idempotency** — a retried request carries an idempotency key, so it returns the original result and never creates a second transfer * **BACEN compliance** — the plugin enforces the operating window and records a full audit trail * **Holiday calendar** — BACEN business-day validation, backed by the `bacen_holidays` table (seeded for 2026–2099) * **Webhook notifications** — webhooks notify your system when a transfer completes, arrives, or fails * **Reconciliation** — every transfer records the JD SPB control number, timestamps, and status history for audit * **Automatic returns (devolução)** — if the plugin cannot credit an incoming transfer, it returns the funds to the origin bank automatically. This differs from a chargeback, which reverses a completed credit To configure fee pricing for TED transfers in your organization, see [Fees Engine](/en/midaz/fees/fees-engine-overview). [Explore the TED API](/en/reference/midaz/plugins/ted/initiate-transfer). Browse the endpoints for initiating and managing transfers. # Internal transfers (P2P) Source: https://docs.lerian.studio/en/rails/ted/ted-p2p Move funds between accounts within your institution instantly, 24/7, with no BACEN routing. The plugin detects internal transfers automatically. P2P transfers move funds between accounts within your institution instantly, at no cost by default, 24 hours a day. You can configure fees per organization — see [Fee configuration](#fee-configuration) below. The transfer never leaves your system, so it settles synchronously and needs no BACEN network. ## When to use P2P *** P2P is the right transfer type whenever both the sender and recipient hold accounts at your institution. Common scenarios include: * **Moving money between a customer's own accounts** — a customer transferring from their checking account to their savings account, or between two accounts they manage. * **Paying another customer at the same institution** — a customer sending money to a friend, family member, or business that also banks with you. * **Internal treasury movements** — splitting balances between corporate accounts held within the same organization. * **Payment splitting** — distributing amounts across multiple accounts for payroll, expense sharing, or cashback. The plugin detects P2P automatically. When the recipient's ISPB matches your institution's ISPB, the plugin routes the transfer internally. Your API integration is identical to TED OUT, so you need no separate endpoint or logic. ## Key advantages *** | Aspect | P2P | TED OUT | | -------------------- | ------------------------------------- | ------------------------------ | | **Settlement time** | Real time (synchronous) | Via JD SPB | | **Operating hours** | 24/7, including weekends and holidays | Mon–Fri, 06:30–17:00 (default) | | **Fee (default)** | None | Configurable | | **BACEN dependency** | None | Required (JD SPB) | ## How it works *** P2P reuses the same API as TED OUT, so your integration stays consistent across transfer types. The process is two steps from the customer's perspective: P2P flow diagram 1. **Initiate** — The customer submits the transfer details. The plugin validates the recipient, checks the sender balance, and confirms the fee (typically R\$ 0.00). The customer sees the transfer summary before committing. 2. **Confirm** — The customer confirms. The plugin creates an atomic debit + credit in Midaz. The transfer completes immediately. The two-step flow gives you a natural moment to show the customer a confirmation screen with the full transfer summary before any funds move. ## Transfer states *** P2P state machine diagram | State | What it means | | ------------ | -------------------------------------------------- | | `CREATED` | Transfer initiated, awaiting customer confirmation | | `PROCESSING` | Midaz atomic transaction underway | | `COMPLETED` | Transfer settled — funds have moved | | `FAILED` | Internal error; no funds were moved | | `CANCELLED` | Cancelled by the customer before confirmation | The `PENDING` state does not apply to P2P transfers. TED OUT uses `PENDING` to mark funds it reserves while it awaits SPB submission. P2P settles entirely within Midaz, so the transfer moves directly from `PROCESSING` to `COMPLETED`. ## Fee configuration *** The plugin disables fees by default. Most institutions offer internal transfers at no charge. The fee configuration is available if your pricing model needs it. To enable P2P fees, see [TED configuration](/en/rails/ted/jd/ted-configuration). ## Recipient validation *** The recipient must have an account at the same institution. When you submit a transfer, the plugin looks up the recipient in CRM. It matches the bank details you provide: ISPB, branch, account, and holder document. If the plugin does not find the recipient, it rejects the transfer and moves no funds. The customer must check the account details and try again. ## Technical integration *** P2P uses the same endpoints as TED OUT. You need no extra integration work beyond the outbound transfer flow you already built. * [Initiate Transfer](/en/reference/midaz/plugins/ted/initiate-transfer) — POST /v1/transfers/initiate * [Process Transfer](/en/reference/midaz/plugins/ted/process-transfer) — POST /v1/transfers/process * [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) — GET /v1/transfers/ The `type` field in the transfer response is `P2P`. P2P transfers have no `controlNumber`, because they do not go through JD SPB. ## Error handling *** P2P transfers are atomic. The plugin creates the debit and credit as a single Midaz transaction. If any part fails, Midaz applies neither leg. No intermediate state moves funds partially or leaves an account short. If a transfer fails, the customer keeps the same balance and can retry. See the [full error list](/en/reference/midaz/plugins/ted/ted-error-list) for error codes and recovery guidance. # Transfer types Source: https://docs.lerian.studio/en/rails/ted/ted-transfer-types Overview of the three Bank Transfer types supported by the plugin — TED OUT, TED IN, and P2P — with links to each flow's guide and best practices. Each transfer type serves a distinct role in your payment infrastructure. ## Content *** Here, you'll find: Send funds to any Brazilian bank account. TED OUT uses a two-step flow: initiate, then process. Tenants that sign externally add an optional signing step. The guide covers settlement and error handling. Receive and credit incoming transfers from other banks automatically. TED IN needs no manual step. Move funds between accounts in the same institution. P2P settles internally and skips external bank routing. # What are Brazil Rails? Source: https://docs.lerian.studio/en/rails/what-are-brazil-rails How Lerian connects your institution to Brazil's payment rails — Pix, TED, and boletos — through partner providers and Lerian-owned BACEN connections. **Brazil Rails** is where the Lerian platform meets Brazil's payment system. Each rail — Pix, TED, Boletos & Bill Payment — is a regulated way to move money. The Central Bank of Brazil (BACEN) operates these rails, or market infrastructures do under its supervision. Lerian gives your institution production-ready access to them. You keep ownership of your ledger, your data, and your business logic. ## How this section is organized *** The documentation here follows a **rail → connection** structure. Each rail has its own section — Pix, TED, Boletos & Bill Payment, and the rails that Lerian reaches only through its own messaging. Inside a rail section, you see every way to connect to it. * **Rail-level pages** cover concepts, domains, and best practices that hold no matter how you connect. * **Connection sections** cover the integration specifics of one path: Lerian native messaging, or a partner interface such as *JD* or *BTG*. ## Two ways to connect *** Lerian connects institutions to Brazilian rails through two models: 1. **Native messaging** — Lerian-owned, non-intermediated connections to BACEN's networks and the market infrastructures under its supervision. Lerian SPI carries Pix and covers the full SPI messaging contract. Lerian SPB carries TED and covers the full SPB messaging contract. Five more native rails handle deferred settlement, card settlement, file exchange, judicial asset orders, and payroll-deductible credit. 2. **Partner interfaces** — adapters that integrate the platform with third-party connectivity providers. Today these cover Pix through **JD** and **BTG**, TED through **JD**, and boletos and bill payment through **BTG**. Your integration with the Lerian platform stays the same. The adapter absorbs the provider's specifics. Both connection models preserve your ledger, accounts, and business logic. They do not require a particular ledger; the rail emits settlement facts for your ledger consumer. See [Native messaging and partner interfaces](/en/rails/native/native-messaging) for how the two compare. ### 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**. The native Lerian rails are Lerian-owned software for a direct, non-intermediated connection to BACEN. They do not replace the JD and BTG paths. ## Rails and connections *** | Rail | Lerian native messaging | Partner interface | | ------------------------------------------------------------------------------------- | ----------------------- | ----------------------------- | | [Pix](/en/rails/pix/pix-overview) | Lerian SPI | JD (direct) · BTG (indirect) | | [TED](/en/rails/ted/ted-overview) | Lerian SPB | JD | | [Boletos & Bill Payment](/en/rails/boleto/boleto-and-bill-payment) | — | BTG (Console Payments module) | | [SILOC](/en/rails/native/siloc/what-is-lerian-siloc) — deferred net settlement | Lerian SILOC | — | | [SLC](/en/rails/native/slc/what-is-lerian-slc) — card settlement | Lerian SLC | — | | [STA](/en/rails/native/sta/what-is-lerian-sta) — file exchange with BACEN | Lerian STA | — | | [SISBAJUD](/en/rails/native/sisbajud/what-is-lerian-sisbajud) — judicial asset orders | Lerian SISBAJUD | — | | [Consignado](/en/rails/native/consignado/what-is-lerian-consignado) — Dataprev | Lerian Consignado | — | How the two connection models compare, and the seven Lerian native rails. Brazil's instant payment system: keys, QR Codes, MED, and the Pix Switch orchestration layer. Same-business-day interbank transfers with sending, receiving, and P2P flows. Issue boletos and pay bills through the Console Payments module. # Operating Streaming Hub Source: https://docs.lerian.studio/en/streaming-hub/operating-streaming-hub Deploy Streaming Hub with Helm in BYOC, run out-of-band migrations, wire liveness and readiness probes, set essential config, and observe the hub via OTLP. This page is for operators running Streaming Hub in their own infrastructure (BYOC). It covers deployment, the configuration that matters, the health and shutdown contract, and how to observe the service. ## Deploying with Helm *** Streaming Hub ships as a dedicated Helm chart, `streaming-hub-helm`, separate from any other Lerian product chart. The chart runs the hub in one of two shapes: * **`all`** — a single deployment that runs every background worker. This is the default and the simplest to operate. * **`split`** — separate **ingest** and **delivery** deployments that scale independently: ingest replicas share one Kafka consumer group, while delivery replicas work delivery jobs from Postgres. The split shape is driven per process by `STREAMING_HUB_ROLE` (`all` | `ingest` | `delivery`). The role gates **which background workers run and which Kafka clients dial** — it does **not** gate which HTTP routes mount. Every role serves the full control-plane API and, crucially, the `/readyz` endpoint your orchestrator and metrics scrape depend on. There is one image and one binary; the role is a deployment input, not a build. ## Running database migrations *** Streaming Hub is backed by a single hub-owned PostgreSQL database, and it **never migrates itself**. Schema migrations run **out of band** — as a separate migration step (for example, an ArgoCD PreSync hook) that applies the versioned migrations before the hub starts. At boot the hub only *verifies* that the schema version it expects is present; it never runs a migration as a side effect of starting. The hub does provision its own weekly table partitions ahead of time as a routine background task — that is internal housekeeping, not a schema migration, and needs no operator action beyond leaving the partition cron running. ## Health and graceful shutdown *** Streaming Hub exposes two distinct probe endpoints. Wire each to the matching Kubernetes probe: | Endpoint | Probe | Behavior | | ---------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/healthz` | Liveness | Returns `200` unconditionally once the process is serving, independent of Postgres, Kafka, or roster health. A failing liveness restarts the pod, so it must not depend on dependencies. | | `/readyz` | Readiness | Folds the full probe set for the role. `Healthy` or `Degraded` → `200` (kept in rotation); `Down` → `503` (pulled from rotation). A `Degraded` replica keeps serving. | `/readyz` distinguishes two failure classes. A **runtime-probe** failure — Postgres unreachable, consumer dead — takes the replica **Down** and out of rotation. A **degrader** — elevated latency, consumer lag, a thin partition buffer — clamps the replica to **Degraded** but keeps it serving, because an impaired replica should not refuse traffic. The probe set is role-aware: a delivery-role pod is not marked unready for having no ingest consumer. On `SIGTERM` the hub drains gracefully. It flips `/readyz` to `NotReady` **first** — before it stops serving — and waits a bounded pre-stop window so the orchestrator can pull the pod from the service before connections are cut. `/healthz` stays `200` throughout, so the pod is not killed mid-drain. It then tears down in dependency-safe order (HTTP, then consumer, then dispatcher, then the background apps, then Kafka clients, then the pool, then telemetry). Set the deployment's `terminationGracePeriodSeconds` at or above the hub's derived drain ceiling for your `STREAMING_HUB_SHUTDOWN_TIMEOUT`, not a fixed magic number. At the default 30-second shutdown timeout the ceiling is about **80 seconds**: the 5-second pre-stop window, plus the shutdown timeout itself, plus a worst-case dispatcher drain leg of `min(timeout, 55s)`, plus a fixed teardown margin for the remaining components. A grace period below the ceiling risks a `SIGKILL` of a still-draining replica — safe for correctness (in-flight jobs are reclaimed and redelivered, deduplicated at the consumer), but it forfeits the clean drain. `/version` (build identity) and `/runtime` (a cheap Go-runtime snapshot) round out the unauthenticated operational surface for incident triage. ## Essential configuration *** Streaming Hub reads its configuration from `STREAMING_HUB_*` environment variables (plus a few shared `PLUGIN_AUTH_*` and `OTEL_*` variables). The full inventory, with every default, lives in the service's environment reference. The variables you set most often: | Variable | Default | Purpose | | -------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `STREAMING_HUB_ENV` | `local` | Deployment environment (`local` \| `staging` \| `production`). Drives the production security gate that rejects the dev bypass flags. | | `STREAMING_HUB_ROLE` | `all` | Deployable slice: `all` \| `ingest` \| `delivery`. Gates workers and Kafka clients, never routes. | | `STREAMING_HUB_HTTP_LISTEN_ADDR` | `:8080` | The single control-plane bind address. | | `STREAMING_HUB_POSTGRES_DSN` | *(required)* | The hub-owned PostgreSQL DSN. Boot fails if empty. | | `STREAMING_HUB_KAFKA_BROKERS` | *(empty)* | Bootstrap broker list for the internal stream. | | `STREAMING_HUB_TENANT_ID` | `default` | The BYOC tenant id. See the warning below before changing it. | | `STREAMING_HUB_KEK_SOURCE` | `env` | Key-encryption-key provider (`env` \| `secretsmanager`). | | `STREAMING_HUB_KEK_REF` | *(empty)* | The **name** of the env var holding the KEK material — never the material itself. | | `STREAMING_HUB_MANIFEST_SOURCES` | *(empty)* | Producer manifest base URLs the event catalog is built from. | | `PLUGIN_AUTH_ADDRESS` | *(plugin-auth default)* | The plugin-auth decision-point base URL for control-plane authorization. | | `PLUGIN_AUTH_ENABLED` | `true` | Master auth switch. `false` is a local bypass, **rejected in production**. | | `STREAMING_HUB_AUTODISABLE_ENABLED` | `true` | Kill switch for auto-disabling broken destinations. | | `STREAMING_HUB_SHUTDOWN_TIMEOUT` | `30s` | Graceful-drain window. The pre-stop window plus `min(value, 55s)` must stay strictly below the 60-second dispatcher lease, or boot fails closed. | | `STREAMING_HUB_MULTI_TENANT_ENABLED` | `false` | `false` is BYOC single-tenant; `true` wires the SaaS multi-tenant roster. | | `STREAMING_HUB_AWS_HUB_PRINCIPAL_ARN` | *(empty)* | The hub's public IAM principal embedded in AWS setup artifacts. Required for AWS sinks. | | `STREAMING_HUB_AWS_SETUP_TEMPLATE_URL` | *(empty)* | The public CloudFormation template URL for the AWS quick-create link. | | `OTEL_EXPORTER_OTLP_ENDPOINT` | *(empty)* | The OTLP collector endpoint telemetry exports to (no `STREAMING_HUB_` prefix). | Secret **values** never belong in these variables in production. The KEK is referenced by the *name* of the env var that the deploy layer injects it into (`STREAMING_HUB_KEK_REF`); the hub reads the material from that named variable and never logs it. SASL, TLS CA, and tenant-manager credentials follow the same rule — the variable holds the value at runtime, but the value comes from your secret store, not a committed config file. **`STREAMING_HUB_TENANT_ID` is a zero-delivery trap in BYOC.** The hub only accepts events whose `ce-tenantid` matches this value exactly; every other event is silently dropped (`unknown_or_inactive_tenant`), advancing the offset with no poison row. If you set it to anything other than `default`, you **must** confirm the producer emits that same `ce-tenantid` — otherwise the hub drops 100% of the stream and delivers nothing, with no error. A non-default value emits one startup warning; heed it. ## DLQ forensics *** `GET /admin/dlq` is the operator forensics surface for dead-letter observations. It is **cross-tenant by design**: it is gated by the lib-auth admin scope, carries **no** tenant shim, and returns records across all tenants, so it is not part of the customer-facing `/v1` API. The dead-letter observations it reads are **observability only** — they are captured from upstream producers' dead-letter topics and are never re-delivered by the hub. Use it to investigate why records failed upstream; it does not replay them. ## Topic reconciler *** The topic reconciler is a **read-only** drift detector, enabled by default (`STREAMING_HUB_RECONCILER_ENABLED`). On each pass it compares the live broker topics, the event catalog, and the distinct subscription targets, and flags three kinds of drift: **ghost topics** (a followed topic with no catalog entry), **dead subscriptions** (a subscribed event type and major with no live catalog entry), and **lag-versus-retention** breaches. It **detects, never corrects** — it emits count-only gauges and structured logs, and writes no broker or database state. When disabled, it spawns no goroutine and dials no admin client, so the disabled path costs nothing; disabling loses an operational alarm but never affects delivery. ## Observability *** Streaming Hub exports its metrics, traces, and logs through **OTLP** (lib-observability), pointed at the collector in `OTEL_EXPORTER_OTLP_ENDPOINT`. That is where the real `streaming_hub_*` metrics live. The `/metrics` endpoint is **almost empty by design**: it serves only the static `streaming_hub_build_info` gauge. Scrape the hub's real metrics from your OTLP collector, not from `/metrics`. Tenant identity is never a metric label — it lives on span attributes and log fields — so the metric cardinality stays bounded no matter how many tenants a deployment serves. ## Next steps *** The delivery internals behind the operational surfaces above. The control-plane operations your tenants use. # Multi-tenancy Source: https://docs.lerian.studio/en/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 customer contexts — **tenants** — while keeping each request and its data within the authenticated tenant's scope. Lerian Cloud is operated 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. ## Product support *** Tenant scoping and configuration are product-specific. The product documentation currently describes multi-tenant operation for: | Product | Scope documented in its product docs | | :----------- | :------------------------------------------------------------ | | **Midaz** | Organizations, ledgers, accounts, transactions, and balances. | | **Tracer** | Rules, limits, validation decisions, and audit events. | | **Reporter** | Configured report data sources and generated reports. | | **Matcher** | Reconciliation resources. | Use the authentication and configuration guide for the product you are deploying. 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. In a multi-tenant deployment, use the supported Access Manager flow and include the Bearer token required by the product. 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. Multi-tenancy is an operational capability, not a promise that every Lerian product has the same authentication middleware or configuration surface. ## Storage isolation *** Tenant Manager service registrations use the values `dedicated` and `shared`. This documentation uses the corresponding deployment concepts below: * **`DATABASE` (`dedicated`)** — a tenant receives a dedicated PostgreSQL database. * **`SCHEMA` (`shared`)** — tenants share a PostgreSQL database and each receives a dedicated schema. The `DATABASE` / `SCHEMA` distinction 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 | `DATABASE` (`dedicated`) | `SCHEMA` (`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, not a generic automatic platform workflow. Validate the target product's migration path, data-consistency requirements, maintenance window, backup/restore plan, and rollback procedure before changing a tenant's service registration. 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 automatically enabled. Authentication requirements depend on the product and environment; for Midaz, production and multi-tenant deployments require authentication, while a permitted non-production single-tenant deployment can disable it. ## Configuration *** There is no cross-product environment-variable contract for multi-tenancy. Do not copy a generic list of `MULTI_TENANT_*` variables between Midaz, Tracer, Reporter, and Matcher. For Midaz, multi-tenant operation requires its product-specific configuration, including `MULTI_TENANT_ENABLED`, `PLUGIN_AUTH_ENABLED`, and `APPLICATION_NAME`. Confirm the current values, defaults, and dependencies in the Midaz configuration reference before deploying. Other products have their own configuration contracts. ## Related pages *** How tenant services and their backing resources are provisioned. How to choose dedicated or shared PostgreSQL isolation. Compare Lerian Cloud, BYOC Single-Tenant, and supported BYOC Multi-Tenant configurations. Understand the supported authentication flows for the products you deploy. # Automatic provisioning Source: https://docs.lerian.studio/en/multi-tenancy/auto-provisioning How Tenant Manager provisions supported tenant services and validates the service endpoints used for machine-to-machine credentials. Automatic provisioning is driven by a tenant's service registration. It can create the backing resources declared for that service, record their credentials, and run the product-specific initialization needed before the service accepts traffic. The exact resources and lifecycle are service-specific. Do not assume that every multi-tenant product provisions PostgreSQL, MongoDB, RabbitMQ, or migrations in the same way. ## Lifecycle *** 1. **Create a tenant identity.** The tenant exists before a product service is attached to it. 2. **Register a tenant service.** The registration identifies the product service, its environment, isolation mode, and connection/provisioning information. Tenant Manager uses `dedicated` and `shared` for the storage modes described elsewhere as `DATABASE` and `SCHEMA`. 3. **Provision supported backing resources.** Tenant Manager invokes the adapters required by the registered service and mode. 4. **Initialize the product.** Schema migrations and readiness work follow that product's own provisioning contract. 5. **Operate the service.** The product uses its own authentication and routing contract to serve the tenant. A successful tenant-service registration is not a blanket guarantee that every product's database, broker, and migration lifecycle is identical. Verify the product-specific provisioning requirements before onboarding production tenants. ## Storage resources *** | Resource | Tenant Manager behavior | | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | | **PostgreSQL** | Uses a dedicated database in `dedicated` mode or a tenant schema in a shared database in `shared` mode. | | **MongoDB** | Uses a tenant database in `dedicated` mode or a tenant collection prefix in `shared` mode. | | **RabbitMQ** | Provisions mode-specific messaging resources; do not rely on a universal vhost or queue naming convention. | | **Credentials** | Service credentials are stored and resolved through the registered service's credential path. Treat rotation and access controls as service-specific. | ## Base URLs for machine-to-machine credentials *** A tenant service can declare an optional `baseUrls` map for the `staging` and `production` environments. Tenant Manager selects the URL for the tenant's environment as `targetBaseUrl` when it creates, rotates, or recreates a machine-to-machine credential. Each value must be an absolute HTTP or HTTPS URL with a host. It cannot include a path other than `/`, query string, fragment, or user credentials. HTTPS is required outside Kubernetes cluster-local services (`*.svc.cluster.local`) and local development (`localhost` or `127.0.0.1`). Updating `baseUrls` replaces the service's map. It does not rewrite existing machine-to-machine credentials; rotate or recreate the credential when the endpoint must change. ## Operator checks *** Before registering a production tenant service: * Confirm that the product and entitlement support multi-tenancy. * Select `dedicated` or `shared` according to the product's supported storage configuration. * Validate the backing resources, credentials, network path, and product migration process. * Register `baseUrls` only for endpoint URLs that are valid for the tenant environment. * Test provisioning, authentication, and rollback using the product's own operational guide. ## Related pages *** How products scope requests and how PostgreSQL isolation modes differ. Choosing dedicated or shared PostgreSQL isolation for a service. # Multi-tenancy use cases Source: https://docs.lerian.studio/en/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. In Tenant Manager registrations, the values are `dedicated` and `shared`; this page uses the corresponding PostgreSQL concepts `DATABASE` and `SCHEMA`. ## Choose `DATABASE` (`dedicated`) when isolation dominates *** Use a dedicated PostgreSQL database per tenant when the tenant needs a strong infrastructure boundary, 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 `SCHEMA` (`shared`) when density dominates *** Use a dedicated schema per tenant in a shared PostgreSQL database when the supported product can operate in `shared` mode and your operating model values density and lower per-tenant infrastructure cost. 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 `SCHEMA` to `DATABASE`. 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; and 4. post-migration checks for authentication, routing, and tenant data. ## Decision checklist *** | Question | Favors `DATABASE` | Favors `SCHEMA` | | :--------------------------------------------------------------------- | :---------------- | :-------------- | | 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 *** Product support, request scoping, and storage concepts. How tenant-service registrations provision supported resources. # API reference template Source: https://docs.lerian.studio/en/partners-hub/api-reference-template Create API operation pages that Mintlify renders from the matching service specification. ## Endpoint pages *** API operation pages are rendered from the matching OpenAPI or Swagger source. Each operation page contains only frontmatter: an `openapi` key that points to the rendered specification, plus optional `title` and `description` keys that override the rendered values: ```mdx theme={null} --- openapi: "/en/openapi/v3-current/.yaml " --- ``` Use the matching `/es/` or `/pt/` specification prefix in the translated page. The method and path stay identical in every locale. Mintlify renders the operation details from the specification. Do not manually duplicate parameters, request and response examples, error bodies, or endpoint descriptions in an operation page. ## Updating an operation *** Update the service's canonical API contract first. Then sync the rendered specifications and keep the operation stubs aligned with the resulting method and path. This keeps the reference accurate across all supported languages. ## Before publishing *** * Verify that the rendered specification contains the operation. * Use the same method and path in each locale. * Keep behavioral descriptions, schemas, and examples in the service contract rather than in a hand-written operation page. # API standards Source: https://docs.lerian.studio/en/partners-hub/api-standards Document each partner API from its implemented contract and service-specific behavior. ## Contract first *** The API contract implemented by each service is the source of truth for its reference documentation. Document the API using the OpenAPI or Swagger version that the service implements. Do not describe an API as OpenAPI 3.1 unless that service has adopted it. ## Service-specific behavior *** Do not assume shared defaults across Lerian APIs. For every operation, document the behavior implemented by that service, including: * Authentication requirements and headers. * Parameter and JSON field names. * Response status codes and date formats. * PATCH media type and omitted or `null` semantics. * DELETE behavior and metadata support. Use the operation schema and handler contract to establish these details. ## Errors *** Error envelopes and error-code formats vary by product. Use the response schema and error reference for the specific service before handling an error programmatically. Do not publish a global error body, prefix, numeric range, or field set unless it is implemented across the services in scope. ## Reference pages *** Render API operation pages from the matching specification rather than duplicating request, response, or error prose by hand. See the [API reference template](/en/partners-hub/api-reference-template) for the operation-stub format. ## Before publishing *** * Verify the method, path, schemas, and media types against the service contract. * Preserve the same method and path in every rendered locale. * Update the canonical service contract before synchronizing rendered API specifications. # Architecture and operations Source: https://docs.lerian.studio/en/partners-hub/architecture-operations Guidance for partner plugins on interoperability, deployment, and operational requirements. Partner plugins should use the documented interfaces and operational requirements of the Lerian products they integrate with. ## Programming language and architecture *** Choose the programming language and architecture that fit your plugin. Design the integration around the published interfaces of the Lerian product it connects to. ## Deployment models *** Lerian supports two deployment models: * **BYOC**: the customer operates the infrastructure in its own cloud or on-premises environment. * **Lerian Cloud**: Lerian operates the managed cloud environment. Make plugin deployment requirements explicit and validate them with the integration owner for the selected model. ## Observability *** Document the logs, metrics, tracing, and operational signals that an integration needs. Agree on the required telemetry format and destination with the product owner before deployment. ## Auditability and traceability *** For a plugin that handles financial data, document its audit behavior, data-retention requirements, and any integrity mechanism. Confirm the applicable requirements with the product owner before integration. ## Authentication and authorization *** Access Manager is an optional Enterprise feature that products and plugins can use for access control. Document the authentication behavior implemented by the product and plugin that are being integrated. # Capitalization Source: https://docs.lerian.studio/en/partners-hub/capitalization Reference the capitalization rules used across Lerian docs — sentence case for headings, exact casing for product names, and common edge cases. We use **sentence case** for all headings and titles. That means only the **first letter** of the sentence and proper nouns are capitalized. | Correct | Avoid | | ------------------------------------ | ----------------------------- | | Getting started with the API | Getting Started With The API | | Using the transaction builder | Using The Transaction Builder | This applies to: * Page titles * Section headings * Card titles * Navigation labels ## Product names *** Product names are proper nouns. Always capitalize them exactly as shown below. | Correct | Wrong | | ------------------------------------ | ---------------------------------------------- | | Midaz | midaz, MIDAZ, MiDaz | | Lerian Console | lerian console, Lerian console, LERIAN CONSOLE | | Fees Engine | fees engine, Fees engine, FEES ENGINE | | Access Manager | access manager, Access manager, ACCESS MANAGER | | Pix | pix, PIX (when referring to the Lerian plugin) | | Flowker | flowker, FLOWKER | | Tracer | tracer, TRACER | | Reporter | reporter, REPORTER | | Matcher | matcher, MATCHER | # Our coding standards Source: https://docs.lerian.studio/en/partners-hub/coding-standards Review the technical, security, and compliance coding standards every plugin must meet to be approved and published on the Lerian Marketplace and ecosystem. Every plugin built for the Lerian ecosystem must meet strict technical, security, and compliance standards. These guidelines define the expectations for partners developing solutions that will be published on the Lerian Marketplace. Our goal is to ensure that every plugin is reliable, interoperable, and secure by design, while giving partners clear rules to follow. The standards are updated regularly to reflect new regulations, feedback, and technology improvements. # Error model Source: https://docs.lerian.studio/en/partners-hub/error-model Document and handle API errors from the contract implemented by each service. ## Use the service contract *** Lerian products do not share one universal error envelope or error-code format. For each operation, use the response schema and error reference implemented by that service as the source of truth. Before handling an error programmatically, verify: * The response media type and schema. * Whether the service exposes a stable error code. * The fields that are present for that operation and status code. * The documented retry or remediation guidance. ## Field validation *** Some services return field-level validation details and others use a different error shape. Document those details only when they are present in the operation's implemented contract. ## Error codes *** Error-code prefixes, numeric formats, and meanings vary by product. Do not infer an error's meaning from a code used by another service. Consult the product-specific error reference before acting on an error code. ## Authoring API reference *** Keep request, response, and error schemas in the service specification. Render operation pages from that specification rather than copying an error model into hand-written endpoint documentation. # Authentication & security Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-auth-security Reference the auth and security fields used across Lerian APIs — OAuth2 tokens, webhook signatures, scopes, and component health checks. This page covers every field related to authentication flows, security checks, webhook signatures, and component health. ## 1. OAuth2 and authorization fields *** | Field | Description | Used in | | ------------ | ------------------------- | ------------------ | | accessToken | JWT access token | Auth, Pix | | refreshToken | Refresh token | Auth | | idToken | OpenID Connect ID token | Auth | | tokenType | Token type (Bearer) | Auth, Pix | | expiresIn | Expiration time (seconds) | Auth, Pix | | grantType | OAuth2 grant type | Auth | | clientId | OAuth2 client ID | Auth, Pix, Flowker | | clientSecret | OAuth2 client secret | Auth, Pix | | username | Username | Auth, Identity | | password | Password | Auth, Identity | | scope | Requested scope | Auth, Pix | | scopes | Scopes list | Flowker | | sub | Subject identifier | Auth | | aud | Audience | Auth | | iss | Issuer | Auth | | iat | Issued at | Auth | | exp | Expiration | Auth | | nbf | Not before | Auth | | jti | JWT ID | Auth | | active | Token active flag | Auth | | authorized | Authorization result | Auth | | resource | Target resource | Auth | | action | Action on the resource | Auth | | redirectUri | OAuth redirect URI | Flowker | | state | OAuth opaque state | Flowker | ## 2. Webhooks and notifications *** | Field | Description | Used in | | --------------------- | ----------------------- | ------- | | X-Webhook-ID | Webhook identifier | Flowker | | X-Webhook-Signature | Webhook HMAC signature | Flowker | | idReqJdPi | JD request identifier | Pix | | pagador | Pix payer | Pix | | recebedor | Pix receiver | Pix | | valor | Pix value | Pix | | dtHrOp | Pix operation timestamp | Pix | | descricao | Operation description | Pix | | endToEndIdOriginal | Original E2E ID | Pix | | endToEndIdDevolucao | Return E2E ID | Pix | | codigoDevolucao | Return reason code | Pix | | motivoDevolucao | Return reason | Pix | | refundUrlNotification | Refund notification URL | Pix | ## 3. Health and monitoring *** | Field | Description | Used in | | -------- | ------------------------- | --------------------- | | status | General status | Pix, JD Mock, Flowker | | services | Dependent services status | Pix | | database | Database status | Pix | | redis | Redis status | Pix | | jd | JD integration status | Pix | # Core fields Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-core-fields Reference the foundational fields shared across Lerian APIs — identifiers, lifecycle timestamps, pagination, and traceability metadata. This page consolidates the foundational fields that appear across multiple Lerian APIs. These fields represent the backbone of how we identify data, track lifecycle events, paginate results, and guarantee traceability. ## 1. Unique identifiers *** | Field | Description | Used in | | --------------- | ----------------------------------------- | --------------------------------------------- | | id | Universal unique identifier (UUID v4) | Midaz, CRM, Fee, Auth, Flowker, Pix, Identity | | accountId | Unique account identifier in the ledger | Midaz, CRM, Pix | | organizationId | Identifier of the owning organization | Midaz, Fee, Flowker | | org\_id | Organization ID (workflow context) | Flowker | | ledgerId | Unique ledger identifier | Midaz, CRM, Fee | | holderId | Identifier of the account holder | CRM | | transactionId | Unique financial transaction identifier | Midaz, Pix | | externalId | Correlation ID for external systems | CRM, Pix, Flowker | | endToEndId | Pix end-to-end identifier | Pix | | protocolId | DICT Pix protocol identifier | Pix | | segmentId | Organizational segment identifier | Midaz, Fee | | portfolioId | Portfolio identifier | Midaz | | assetId | Financial asset identifier | Midaz | | routeId | Operation route identifier | Midaz | | templateId | Template identifier | Reporter, Flowker | | workflowId | Workflow identifier | Flowker | | runId | Workflow execution identifier | Flowker | | jobId | Processing job identifier | Flowker, Reporter | | tenantId | Tenant identifier in multi-tenant systems | Flowker | | clientId | OAuth2 client identifier | Auth, Flowker | | userId | User identifier | Auth, Identity | | groupId | Group identifier | Identity | | roleId | Role identifier | Identity | | bankId | ISPB bank code (8 digits) | Pix | | providerId | Provider identifier (OAuth2) | Flowker | | batchId | Processing batch identifier | Flowker | | keyId | API key identifier | Flowker | | versionId | Version identifier | Flowker | | targetVersionId | Target version identifier (comparison) | Flowker | ## 2. Temporal fields (timestamps) *** | Field | Description | Used in | | ------------------ | ------------------------------ | --------------------------------------- | | createdAt | Creation timestamp (ISO 8601) | Midaz, CRM, Fee, Pix, Flowker, Identity | | updatedAt | Last update timestamp | Midaz, CRM, Fee, Pix, Flowker, Identity | | deletedAt | Logical deletion timestamp | Midaz, CRM, Fee, Identity | | executedAt | Effective execution timestamp | Pix, Flowker | | scheduledAt | Scheduled execution time | Pix, Flowker | | expiresAt | Expiration timestamp | Pix, Auth | | settledAt | Financial settlement timestamp | Pix | | transactionAt | Transaction timestamp | Pix | | dueDate | Due date | Pix | | birthDate | Date of birth (YYYY-MM-DD) | CRM | | foundingDate | Company founding date | CRM | | accountOpeningDate | Account opening date | Pix | | timestamp | Generic event timestamp | Auth, JD Mock, Flowker | | uptime | System uptime | Pix, JD Mock | | startDate | Start date (RFC3339) | Flowker | | endDate | End date (RFC3339) | Flowker | | startTimeFrom | Workflow search start time | Flowker | | startTimeTo | Workflow search end time | Flowker | | lastLoginAt | Last login timestamp | Identity | | lastActivityAt | Last activity timestamp | Identity | ## 3. Pagination and navigation *** *** | Field | Description | Used in | | -------------- | ----------------------------------------------------------- | ----------------------------- | | limit | Items per page (default: 10) | Midaz, CRM, Fee, Pix, Flowker | | page | Current page | Midaz, CRM, Fee | | page\_size | Page size (workflow search) | Flowker | | page\_token | Pagination token | Flowker | | offset | Pagination offset | Flowker | | total | Total number of items | Midaz, CRM, Fee | | items | Items in the current page | Midaz, CRM, Fee | | sortOrder | Sort order (asc/desc) | CRM | | sort\_by | Sort field | Flowker | | sort\_order | Sort order (asc/desc) | Flowker | | includeDeleted | Include logically deleted items | CRM | | hardDelete | Requests removal subject to retention and compliance policy | CRM | | search | Search term | Flowker | ## 4. Control and audit metadata *** | Field | Description | Used in | | ------------- | ----------------------------- | --------------------- | | version | Resource or API version | Pix, JD Mock, Flowker | | environment | Environment (prod/dev/stage) | Pix, JD Mock | | traceId | Log trace identifier | Pix, Flowker | | correlationId | Cross-service correlation ID | Flowker | | retryCount | Retry counter | Flowker | | maxRetries | Maximum number of retries | Flowker | | backoffDelay | Exponential retry delay | Flowker | | createdBy | Creator user | Identity | | updatedBy | Last updater user | Identity | | deletedBy | User who deleted the resource | Identity | # Financial data Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-financial-data Reference the financial data fields used across Midaz, CRM, Pix, and Fees Engine — banking details, Pix keys, amounts, and currency codes. This page brings together all financial information fields used across Midaz, CRM, Pix, and Fees Engine. ## 1. Banking data *** | Field | Description | Used in | | --------------- | ------------------------------------- | -------- | | branch | Bank branch code | CRM, Pix | | account | Account number | CRM | | bankAccount | Bank account number (up to 20 digits) | Pix | | bankAccountType | Account type | Pix | | accountNumber | Generic account number | Pix | | accountDigit | Check digit | Pix | | agency | Bank agency | Pix | | accountType | Account type | CRM, Pix | | iban | International Bank Account Number | CRM | | bankId | Bank identifier | CRM, Pix | | openingDate | Account opening date | CRM | ## 2. Pix keys *** | Field | Description | Used in | | -------------------- | --------------------------------------- | ------- | | key | Pix key value | Pix | | keyType | Key type (email, phone, tax ID, random) | Pix | | requiresConfirmation | Requires token confirmation | Pix | | confirmationToken | Token for key confirmation | Pix | | claimId | Claim identifier | Pix | | claimReason | Claim reason | Pix | | claimType | Claim type | Pix | | authorizationCode | Authorization code | Pix | | checked | Key verification flag | Pix | ## 3. Financial transactions *** | Field | Description | Used in | | --------------- | ----------------------------------- | ------------------- | | amount | Monetary amount | Midaz, Pix, Fee | | value | Numeric value | Fee, Pix | | totalAmount | Total amount of the operation | Pix | | minimumAmount | Minimum allowed amount | Fee | | maximumAmount | Maximum allowed amount | Fee | | originalAmount | Amount before fees | Fee | | afterFeesAmount | Amount after fee application | Fee | | description | Transaction description | Midaz, Pix, Flowker | | asset | Asset code (BRL, USD) | Midaz, Fee | | code | Transaction or authorization code | Midaz, Flowker | | route | Operation route | Midaz | | pending | Pending status | Midaz | | priority | Priority (high/medium/low) | Pix, Flowker | | flow | Transaction flow (cash-in/cash-out) | Pix | | isPixSaque | Pix withdrawal flag | Pix | | isPixTroco | Pix cashback flag | Pix | | isReversal | Reversal flag | Pix | | isInternal | Internal transaction flag | Pix | | reversalType | Type of reversal | Pix | | refundId | Refund identifier | Pix | | returnId | Return identifier | Pix | | returnReason | Reason for return | Pix | | returnedAt | Return timestamp | Pix | | failureReason | Failure reason | Pix | ## 4. Pix QR codes *** | Field | Description | Used in | | ----------------- | ------------------------------- | ------- | | qrCode | Pix QR Code string | Pix | | qrCodeImage | Base64 QR image | Pix | | qrCodeType | QR type (static/dynamic) | Pix | | txId | Transaction ID (up to 25 chars) | Pix | | recipientName | Recipient name | Pix | | expirationTime | Expiration time in seconds | Pix | | isValid | Whether QR is valid | Pix | | validationMessage | Validation message | Pix | | additionalData | Additional QR data | Pix | ## 5. Limits and controls *** | Field | Description | Used in | | -------------- | ------------------------- | ------- | | limitType | Limit type | Pix | | periodType | Period type | Pix | | period | Period (metrics context) | Flowker | | used | Amount used in the period | Pix | | remaining | Remaining amount | Pix | | resetDate | Period reset date | Pix | | waivedAccounts | Accounts exempt from fees | Fee | # Ledger operations Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-ledger-operations Reference the Midaz Ledger operation fields — balances, transaction routes, operation routes, and metadata indexes used in postings. This page covers fields specific to Midaz ledger operations, including balances, transaction routes, operation routes, and metadata indexes. ## 1. Balances *** | Field | Description | Used in | | -------------- | --------------------------------------------------------------------------- | ------- | | key | Unique identifier for the balance within the account context | Midaz | | available | Amount available for transactions | Midaz | | onHold | Amount currently reserved or on hold | Midaz | | version | Balance version, updated with each transaction (for optimistic concurrency) | Midaz | | accountType | Type of account that holds this balance | Midaz | | allowSending | Whether the balance can be used to send transactions | Midaz | | allowReceiving | Whether the balance can be used to receive transactions | Midaz | | assetCode | Asset or currency code (e.g., BRL, USD) | Midaz | | alias | Account alias for quick identification | Midaz | ## 2. Transaction routes *** | Field | Description | Used in | | --------------- | --------------------------------------------------------------------- | ------- | | title | Short text summarizing the route's purpose (max 255 characters) | Midaz | | description | Detailed description of the transaction route (max 250 characters) | Midaz | | operationRoutes | Array of operation route IDs that define source and destination logic | Midaz | ## 3. Operation routes *** | Field | Description | Used in | | ------------- | ------------------------------------------------------ | ------- | | operationType | Route direction: source or destination | Midaz | | code | External reference code for the operation route | Midaz | | account | Account selection rule for the operation | Midaz | | ruleType | Account selection method: alias or account\_type | Midaz | | validIf | Condition that must be satisfied for the rule to match | Midaz | ## 4. Metadata indexes *** | Field | Description | Used in | | ----------- | --------------------------------------------------------------------------------- | ------- | | metadataKey | Metadata key to index (max 100 characters, must match the field name in entities) | Midaz | | unique | Whether the index enforces uniqueness (duplicate values rejected when true) | Midaz | | sparse | Whether the index is sparse (only indexes entities containing the field) | Midaz | | indexName | Name of the created index | Midaz | | entityName | Name of the entity collection the index applies to | Midaz | # Lerian field dictionary Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-main Explore the centralized dictionary of every field used across Lerian products — Midaz, Flowker, Pix, CRM, Fee, Auth, Reporter, and more. This guide organizes every field used across the Lerian ecosystem into a single, reliable, and well-structured reference. It supports partners and development teams who need clarity and consistency across Midaz, Flowker, Identity, CRM, Fee, Pix, Auth, Reporter, and related plugins. This dictionary helps you validate payloads, improve interoperability, streamline QA, and accelerate integration development. ## Why this dictionary matters *** APIs evolve fast. New features, plugins, and versions bring new fields and behavior. This dictionary centralizes all definitions, making it easier to understand how our products communicate with each other. Use it to: * Build integrations with confidence * Reduce ambiguity across workflows * Validate payload structures * Support QA, observability, and debugging * Accelerate onboarding for new developers and partners ## How to use this page *** Each section groups fields by semantic categories. Every field includes: * Field name * Description * Where it is used across Lerian products If you're working with a specific product, scroll down to find dedicated subsections for Flowker, Pix, Identity, Fees Engine, and Reporter. For strict schema validation, always check the official OpenAPI specifications. Those files reflect the exact structure required by each endpoint. ## Before you explore *** This dictionary follows consistent conventions across all products. * Dates use ISO 8601 or RFC3339 formats * UUIDs follow v4 formatting * Pix fields follow BACEN standards * Workflow fields follow Flowker’s runtime and authoring semantics * Fees Engine fields reflect the latest v2 model * Metadata is always a flexible key-value If your integration uses sensitive operations like Pix, Identity, or Fees Engine, verify behavior and accepted formats directly in the product APIs. Some fields behave differently depending on the execution context. ## Structure of the dictionary *** The dictionary is divided into the following categories: Essential fields shared across all Lerian APIs, including identifiers, timestamps, pagination, and audit metadata. Fields that describe individuals, companies, users, groups, and organizational structures. Fields related to banking details, Pix operations, financial transactions, QR Codes, and operational limits. Fields used for authentication, authorization, OAuth2 flows, webhook validation, and system health monitoring. Fields related to workflow orchestration (Flowker), reporting (Reporter), fee calculations, and error handling. Fields for transaction validation, rules, spending limits, and decision responses. Fields for Pix dispute resolution — claims, infraction reports, refund requests, and fraud markers. Fields for balances, transaction routes, operation routes, and metadata indexes. # People & Organizations Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-people-organizations Reference the people and organization fields used across Lerian APIs — personal data, corporate data, contacts, addresses, and identity. This page groups all fields related to personal data, corporate data, contact details, addresses, and identity management. ## 1. Personal and business information *** | Field | Description | Used in | | ------------ | ------------------------------ | --------------------------------------- | | name | Full name or legal entity name | Midaz, CRM, Pix, Flowker, Identity | | document | Tax ID (CPF/CNPJ) | CRM, Pix | | type | Person type or resource type | CRM, Pix, Flowker | | favoriteName | Preferred or nickname | CRM | | socialName | Social name | CRM | | tradeName | Trade name | CRM, Pix | | motherName | Mother's name | CRM, Pix | | fatherName | Father's name | CRM | | gender | Gender | CRM, Identity | | civilStatus | Civil status | CRM | | nationality | Nationality | CRM | | activity | Business activity | CRM | | size | Company size | CRM | | status | Status (active, inactive, etc) | Midaz, CRM, Pix, Fee, Flowker, Identity | ## 2. Contact information *** | Field | Description | Used in | | -------------- | ----------------------- | ------------------- | | email | Primary email | Auth, CRM, Identity | | primaryEmail | Primary email | CRM, Identity | | secondaryEmail | Secondary email | CRM, Identity | | emailVerified | Email verification flag | Auth, Identity | | mobilePhone | Mobile phone | CRM, Identity | | otherPhone | Additional phone | CRM | | phone | Main phone | Midaz, Identity | ## 3. Addresses *** | Field | Description | Used in | | ----------- | ------------------------------- | --------------- | | address | Full address or nested object | Midaz, Identity | | line1 | Address line 1 | CRM | | line2 | Address line 2 | CRM | | zipCode | ZIP code | CRM | | city | City | CRM | | state | State | CRM | | country | ISO 3166-1 alpha-2 country code | CRM | | countryCode | Country bank code | CRM | | description | Address description | CRM | ## 4. Organizational structures (Midaz) *** | Field | Description | Used in | | -------------------- | ------------------------------ | ------- | | legalName | Legal company name | Midaz | | doingBusinessAs | Trade name | Midaz | | legalDocument | Official legal document | Midaz | | address | Organization address | Midaz | | phone | Main phone | Midaz | | website | Website | Midaz | | parentOrganizationId | Parent organization identifier | Midaz | | metadata | Flexible key-value metadata | Midaz | | scale | Scale factor for calculations | Midaz | | precision | Decimal precision | Midaz | | symbol | Currency or asset symbol | Midaz | | balance | Account balance | Midaz | | available | Available balance | Midaz | | onHold | Held balance | Midaz | ## 5. User profile *** | Field | Description | Used in | | ----------------- | -------------------- | -------------- | | profile | User profile URL | Auth, Identity | | preferredUsername | Preferred username | Auth, Identity | | picture | Profile picture URL | Auth, Identity | | avatar | Avatar image URL | Identity | | bio | User biography | Identity | | locale | Preferred locale | Identity | | timezone | User timezone | Identity | | representative | Legal representative | CRM | | role | User role | CRM, Identity | ## 6. Identity — users and groups *** | Field | Description | Used in | | -------------------- | ----------------------- | -------- | | userId | Unique user identifier | Identity | | user\_id | User ID (admin context) | Identity | | username | Unique username | Identity | | email | Primary email | Identity | | emailVerified | Email verified flag | Identity | | phone | User phone | Identity | | phoneVerified | Phone verified flag | Identity | | password | Hashed password | Identity | | passwordHash | bcrypt hash | Identity | | passwordChangedAt | Last password change | Identity | | passwordResetToken | Reset token | Identity | | passwordResetExpires | Reset expiration | Identity | | firstName | First name | Identity | | lastName | Last name | Identity | | fullName | Full name | Identity | | displayName | Display name | Identity | | avatar | Avatar | Identity | | bio | User biography | Identity | | locale | User locale | Identity | | timezone | Timezone | Identity | | isActive | User active flag | Identity | | isEmailVerified | Email verified flag | Identity | | isMfaEnabled | MFA enabled | Identity | | mfaSecret | MFA secret | Identity | | lastLoginAt | Last login | Identity | | lastActivityAt | Last activity | Identity | | loginCount | Login counter | Identity | | failedLoginAttempts | Failed login attempts | Identity | | lockedUntil | Lock expiration | Identity | | groupId | Group ID | Identity | | groupName | Group name | Identity | | groupDescription | Group description | Identity | | permissions | Array of permissions | Identity | | roles | Array of roles | Identity | | metadata | Custom metadata | Identity | | createdBy | Created by user | Identity | | updatedBy | Updated by user | Identity | | deletedBy | Deleted by user | Identity | | createdAt | Creation timestamp | Identity | | updatedAt | Update timestamp | Identity | | deletedAt | Deletion timestamp | Identity | # Pix operations Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-pix-operations Reference the Pix dispute and anti-fraud fields aligned with BACEN — key claims, infraction reports, refund requests, and fraud markers. This page covers fields specific to Pix dispute resolution and anti-fraud operations, including key claims, infraction reports, refund requests, and fraud markers. These fields follow BACEN's regulatory standards. ## 1. Claims *** | Field | Description | Used in | | ------------------- | ----------------------------------------------------------------------------- | ------- | | claimId | Unique identifier for the key claim | Pix | | claimType | Type of claim: OWNERSHIP or PORTABILITY | Pix | | claimStatus | Current status: OPEN, WAITING\_RESOLUTION, CONFIRMED, CANCELLED, or COMPLETED | Pix | | claimer | Information about the claiming party (document, name, type) | Pix | | claimerAccount | Account details of the claiming party | Pix | | donorParticipant | ISPB of the donor participant (8 digits) | Pix | | confirmReason | Reason for confirming the claim | Pix | | cancelReason | Reason for cancelling the claim | Pix | | cancelledBy | Party that cancelled the claim: DONOR or CLAIMER | Pix | | resolutionPeriodEnd | Deadline for the counterparty to respond (D+7) | Pix | | completionPeriodEnd | Deadline for claim completion (D+14) | Pix | ## 2. Infraction reports *** | Field | Description | Used in | | ----------------------- | ----------------------------------------------------------------- | ------- | | infractionReportId | Unique identifier for the infraction report | Pix | | transactionId | SPI transaction identifier (End-to-End ID) | Pix | | reason | Infraction reason: REFUND\_REQUEST or REFUND\_CANCELLED | Pix | | situationType | Type of fraud situation (e.g., SCAM, ACCOUNT\_TAKEOVER, COERCION) | Pix | | reportDetails | Detailed description of the infraction (max 2000 characters) | Pix | | reporterParticipant | ISPB of the reporting participant (8 digits) | Pix | | counterpartyParticipant | ISPB of the counterparty (8 digits) | Pix | | analysisResult | Analysis result: AGREED or DISAGREED | Pix | | analysisDetails | Detailed analysis comments | Pix | | status | Current status: OPEN, ACKNOWLEDGED, CLOSED, or CANCELLED | Pix | ## 3. Refund requests *** | Field | Description | Used in | | --------------------- | --------------------------------------------------------------------------- | ------- | | refundId | Unique identifier for the refund request | Pix | | transactionId | Original Pix transaction identifier (End-to-End ID) | Pix | | amount | Refund amount in BRL | Pix | | infractionId | Related infraction report ID (required for FRAUD reason) | Pix | | refundStatus | Current status: OPEN, CLOSED, or CANCELLED | Pix | | refundAnalysisResult | Analysis result: TOTALLY\_ACCEPTED, PARTIALLY\_ACCEPTED, or REJECTED | Pix | | refundAnalysisDetails | Detailed analysis comments | Pix | | refundRejectionReason | Rejection reason: NO\_BALANCE, ACCOUNT\_CLOSURE, INVALID\_REQUEST, or OTHER | Pix | | refundTransactionId | End-to-End ID of the refund transaction | Pix | ## 4. Fraud markers *** | Field | Description | Used in | | ----------------- | ----------------------------------------------------------------------------- | ------- | | fraudMarkerId | Unique identifier for the fraud marker | Pix | | taxIdNumber | CPF (11 digits) or CNPJ (14 digits) of the flagged individual or entity | Pix | | fraudType | Classification: APPLICATION\_FRAUD, MULE\_ACCOUNT, SCAMMER\_ACCOUNT, or OTHER | Pix | | key | Optional Pix key associated with the fraud | Pix | | document | CPF or CNPJ of the flagged party | Pix | | fraudMarkerStatus | Current status: REGISTERED or CANCELLED | Pix | # Tracer Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-tracer Reference the Tracer fields used in real-time transaction validation — request payloads, rules, spending limits, and decision responses returned to callers. This page covers fields specific to Tracer — Lerian's real-time transaction validation engine. These fields are used across validation requests, rules, spending limits, and decision responses. ## 1. Validation requests *** | Field | Description | Used in | | -------------------- | --------------------------------------------------------------------------------- | ------- | | requestId | Client-generated unique ID for idempotency and audit correlation | Tracer | | transactionType | Type of transaction: CARD, WIRE, PIX, or CRYPTO | Tracer | | subType | Transaction subtype for additional context (e.g., debit, credit) | Tracer | | amount | Transaction amount as a decimal string, for example `"100.00"` (must be positive) | Tracer | | currency | ISO 4217 currency code (3 uppercase letters) | Tracer | | transactionTimestamp | When the transaction occurred (RFC 3339 format with timezone) | Tracer | ## 2. Validation responses *** | Field | Description | Used in | | ---------------- | ------------------------------------------------------------ | ------- | | validationId | Server-generated unique identifier for the validation record | Tracer | | decision | Validation decision: ALLOW, DENY, or REVIEW | Tracer | | reason | Human-readable reason for the decision | Tracer | | matchedRuleIds | IDs of rules that matched and triggered the decision | Tracer | | evaluatedRuleIds | IDs of all rules that were evaluated | Tracer | | processingTimeMs | Processing time in milliseconds | Tracer | | totalRulesLoaded | Total number of rules loaded for evaluation | Tracer | | truncated | Whether the response was truncated due to size limits | Tracer | ## 3. Rules *** | Field | Description | Used in | | ------------- | -------------------------------------------------------------------------- | ------- | | ruleId | Unique identifier for the rule | Tracer | | name | Human-readable rule name (unique within its context, max 255 characters) | Tracer | | description | Rule purpose and logic explanation (max 1000 characters) | Tracer | | expression | CEL expression that must evaluate to boolean (max 5000 characters) | Tracer | | action | Action taken when the expression evaluates to true: ALLOW, DENY, or REVIEW | Tracer | | scopes | Array of scopes that determine which transactions this rule applies to | Tracer | | status | Rule lifecycle status: DRAFT, ACTIVE, INACTIVE, or DELETED | Tracer | | activatedAt | When the rule was last activated (null if never activated) | Tracer | | deactivatedAt | When the rule was last deactivated (null if never deactivated) | Tracer | ## 4. Spending limits *** | Field | Description | Used in | | --------------- | --------------------------------------------------------------------------------------------- | ------- | | limitId | Unique identifier for the limit | Tracer | | name | Human-readable limit name (globally unique, max 255 characters) | Tracer | | description | Limit purpose and usage explanation (max 1000 characters) | Tracer | | limitType | Type of limit: DAILY, WEEKLY, MONTHLY, CUSTOM, or PER\_TRANSACTION (immutable after creation) | Tracer | | maxAmount | Maximum decimal amount as a string, for example `"1000.00"` | Tracer | | currency | ISO 4217 currency code (immutable after creation) | Tracer | | scopes | Array of scopes that determine which transactions this limit applies to | Tracer | | status | Limit lifecycle status: DRAFT, ACTIVE, INACTIVE, or DELETED | Tracer | | activeTimeStart | Start of the daily active window in `HH:mm` format | Tracer | | activeTimeEnd | End of the daily active window in `HH:mm` format | Tracer | | customStartDate | Start date-time for CUSTOM limits | Tracer | | customEndDate | End date-time for CUSTOM limits | Tracer | ## 5. Limit usage *** | Field | Description | Used in | | --------------- | ----------------------------------------------------------------------- | ------- | | limitAmount | Total limit amount as a decimal string | Tracer | | currentUsage | Projected usage after applying the transaction, as a decimal string | Tracer | | exceeded | Whether the limit was exceeded during validation | Tracer | | period | Limit period type: DAILY, WEEKLY, MONTHLY, CUSTOM, or PER\_TRANSACTION | Tracer | | scope | Human-readable scope description (e.g., "account:uuid", "segment:uuid") | Tracer | | attemptedAmount | Transaction amount being validated, as a decimal string | Tracer | ## 6. Scopes and targeting *** | Field | Description | Used in | | --------------- | ---------------------------------------------------------- | ------- | | accountId | Account identifier for scope targeting | Tracer | | segmentId | Segment identifier for scope targeting | Tracer | | portfolioId | Portfolio identifier for scope targeting | Tracer | | merchantId | Merchant identifier for scope targeting | Tracer | | transactionType | Transaction type filter: CARD, WIRE, PIX, or CRYPTO | Tracer | | category | Merchant Category Code (4-digit MCC per ISO 18245) | Tracer | | country | Merchant country (ISO 3166-1 alpha-2, 2 uppercase letters) | Tracer | # Workflows & Reporting Source: https://docs.lerian.studio/en/partners-hub/field-dictionary/fd-workflows-reporting Reference workflow and reporting fields used by Flowker, Reporter, and Fees Engine, plus the shared error-handling structures returned across these plugins. This page brings together all fields used by Flowker, Reporter, Fees Engine, and error-handling structures. ## 1. Reporting (Reporter) *** | Field | Description | Used in | | ------------ | -------------------------------------------------------------------- | -------- | | templateName | Name of the associated template | Reporter | | templateId | Identifier of the template used to generate the report | Reporter | | outputFormat | Output format generated from the template (HTML, PDF, CSV, XML, TXT) | Reporter | | dataSourceId | Data source identifier (e.g., midaz\_onboarding) | Reporter | | filters | Filter conditions grouped by data source, table, and field | Reporter | | reportId | Generated report ID | Reporter | ## 2. Workflow orchestration (Flowker general) *** | Field | Description | Used in | | -------------- | -------------------- | ------- | | workflowName | Workflow name | Flowker | | workflow | Workflow filter | Flowker | | workflow\_type | Workflow type | Flowker | | stepId | Step identifier | Flowker | | stepType | Step type | Flowker | | conditions | Execution conditions | Flowker | | actions | Actions to execute | Flowker | | variables | Context variables | Flowker | | dependencies | Step dependencies | Flowker | ## 3. Fees Engine *** | Field | Description | Used in | | ---------------- | ------------------------- | ------- | | feeGroupLabel | Fee group label | Fee | | feeLabel | Specific fee label | Fee | | calculationModel | Fee calculation model | Fee | | applicationRule | Application rule | Fee | | calculations | Fee calculation array | Fee | | referenceAmount | Reference amount | Fee | | priority | Fee priority | Fee | | isDeductibleFrom | Whether fee is deductible | Fee | | creditAccount | Fee credit account | Fee | | routeFrom | Origin route | Fee | | routeTo | Destination route | Fee | | transactionRoute | Transaction route | Fee | | enable | Fee enabled flag | Fee | ## 4. Error handling *** | Field | Description | Used in | | ---------- | --------------------------- | ---------------------- | | error | Error code | Pix, Fee, CRM | | message | Error message | Pix, Fee, CRM, Flowker | | details | Specific error details | Pix, Fee | | field | Field that caused the error | Pix, Fee | | path | Endpoint path | Pix | | code | Standardized error code | Fee, CRM, Flowker | | entityType | Entity type | Fee | | title | Error title | Fee | | timestamp | Error timestamp | Pix, Fee, Flowker | ## 5. Flowker *** Expanded fields specific to Flowker | Field | Description | Context | | ------------------- | ----------------------------- | ------------------------ | | workflowId | Unique workflow ID | Runtime | | workflow\_id | Workflow ID | Execution search | | runId | Execution run ID | Runtime | | workflow\_type | Type of workflow | Runtime | | workflow | Workflow filter | Event Router | | workflowName | Descriptive workflow name | Authoring | | providerId | Provider ID (OAuth flow) | OAuth2 | | provider\_id | Provider ID | Providers | | provider\_ids | List of provider IDs | Providers | | batch\_id | Batch ID | Providers | | key\_id | API key ID | Admin | | org\_id | Organization ID | Admin | | template\_id | Template ID | Authoring/Providers | | versionId | Source version ID | Transformations | | targetVersionId | Target version ID | Transformations | | ver | Version number | Authoring | | version | Resource version | Providers/Authoring | | stepId | Step ID | Authoring | | stepType | Step type | Authoring | | name | Step or resource name | Authoring | | rule | Routing rule filter | Event Router | | status | Workflow or provider status | Multiple Flowker modules | | state | OAuth2 state | OAuth2 | | category | Provider or template category | Providers | | tags | Comma-separated tags | Providers | | type | Multipurpose type | Providers/Runtime | | start\_date | Start date (RFC3339) | Analytics | | end\_date | End date (RFC3339) | Analytics | | start\_time\_from | Start time (from) | Runtime | | start\_time\_to | Start time (to) | Runtime | | period | Period (metrics) | Transformations | | limit | Pagination limit | Multiple endpoints | | offset | Pagination offset | Multiple endpoints | | page\_size | Page size | Runtime | | page\_token | Pagination token | Runtime | | sort\_by | Sort field | Providers/Runtime | | sort\_order | Sort order | Providers/Runtime | | search | Search term | Providers | | from | Source version (diff) | Authoring | | to | Target version (diff) | Authoring | | code | OAuth2 code | OAuth2 | | redirectUri | OAuth2 redirect URI | OAuth2 | | scopes | OAuth2 scopes list | OAuth2 | | X-Webhook-ID | Webhook ID | Providers | | X-Webhook-Signature | Webhook signature | Providers | # Guides templates Source: https://docs.lerian.studio/en/partners-hub/guides-template Adopt the official Lerian templates for narrative docs — product overviews, architecture, getting started, entity references, and how-to guides. This page provides the official templates for all narrative and task-oriented documentation in the Lerian ecosystem: product overviews, architecture pages, getting started guides, entity references, how-to guides, and plugin overviews. These templates reflect the structure and patterns established in the Midaz documentation. Use them as a starting point for any new page. Apply [voice and tone](/en/partners-hub/voice-tone) and [capitalization](/en/partners-hub/capitalization) rules regardless of which template you use. For API endpoint documentation, see [API Reference templates](/en/partners-hub/api-reference-template). ## Product overview page *** The entry point for a product or plugin. Answers three questions: what is it, why use it, and how does it work. Use "What is \[Product]?" as the title. Add a one-line subtitle in the frontmatter `description` field that summarizes the product in plain language. ```yaml theme={null} --- title: "What is [Product]?" description: "[Product] does [X] for [audience]." --- ``` Two to three sentences. State what the product is, what it does, and where it fits in the Lerian ecosystem. Mention the licensing model (source-available for Midaz and Fetcher, or Enterprise / licensed for other products) and link to the public GitHub repo only when the product is source-available — otherwise note that its repository is maintained internally. Reference: [What is Midaz?](/en/midaz/what-is-midaz) opens with a single paragraph that covers product definition, licensing, and source availability. For non-source-available products, see [What is Reporter?](/en/reporter/what-is-reporter) for the recommended phrasing. Frame the problem the product solves. Use a bullet list for the key value propositions. Each bullet starts with a bold label followed by a colon and a one-line explanation. ```markdown theme={null} * **Own your ledger**: Source-available means full transparency and no vendor lock-in * **Move fast**: Modular architecture lets you add products without re-architecting ``` List core capabilities as bullets. Follow with a "Built for" subsection if the product serves distinct audiences (fintechs, banks, enterprises). Use an `` with 3–5 use cases. Each accordion has a short title and a 2–3 sentence description of the scenario. ```jsx theme={null} Launch checking accounts, savings products, and instant transfers. ``` Use a `` component to walk through the high-level workflow (4–6 steps). Each step has a verb-based title and a 1–2 sentence explanation. A table mapping related Lerian products to what they add. | Product | Integration | | -------------------------------------- | ----------------------------------------------------------- | | [Matcher](/en/matcher/what-is-matcher) | Reconcile ledger transactions against external data sources | A `` with 3–4 cards pointing to: architecture/concepts, quick start, use cases, and API reference. **Reference pages:** [What is Midaz?](/en/midaz/what-is-midaz) · [What is CRM?](/en/midaz/crm/crm-overview) · [Pix in Lerian](/en/rails/pix/pix-overview) ## Architecture and concepts page *** Explains the internal structure of a product — its domains, entities, and how they relate. This is the "About \[Product]" page. One paragraph that positions the product architecturally. Mention the domain-driven design if applicable. Break the product into its logical domains (e.g., Onboarding Domain, Transaction Domain). Each domain gets an H3 heading with: * A short description of its purpose * A bullet list of its components, each with a bold name and a one-line definition Use glossary tooltips for domain-specific terms on first mention: ```jsx theme={null} Ledger ``` End with an "In short" section containing a table that maps domains to their purpose and key APIs. | Domain | Purpose | Key APIs | | :------------- | :-------------------------- | :----------------------------- | | **Onboarding** | Structure and configuration | Organizations, Ledgers, Assets | **Reference page:** [About Midaz](/en/midaz/about-midaz) ## Getting started page *** A hands-on tutorial that takes the reader from zero to a working setup. Target: under 15 minutes. Use "Getting started with \[Product]" as the title. The subtitle should set expectations: what the reader will accomplish and how long it takes. A table listing required tools with minimum versions and check commands. | Tool | Minimum version | Check command | | ---- | --------------- | ------------- | | Go | 1.24+ | `go version` | Add a `` for OS compatibility. Each step is an H2 with the format "Step N — \[Verb] \[object]" (e.g., "Step 1 — Clone the repository"). Inside each step: * A 1–2 sentence explanation of what the step does and why * A code block with the exact command * A `` or `` for important details (ports, default values, gotchas) Link to the full API reference for each endpoint used: ```markdown theme={null} For the complete endpoint specification, see [Create an Organization](/en/reference/midaz/create-an-organization). ``` Always include a final step that confirms the setup works (e.g., check a balance, query a status endpoint). A `` pointing to entities, transactions, use cases, and API explorer. **Reference page:** [Getting started with Midaz](/en/midaz/midaz-getting-started) ## Entity reference page *** Documents a single core entity (Account, Ledger, Transaction, Holder, etc.). Explains what it is, how it behaves, and how to use it. One paragraph defining the entity, its role in the system, and its relationship to other entities. Explain key behaviors and rules. Use H3 subsections for distinct concepts. Include diagrams (``) when relationships are complex. Use `` to show JSON and DSL examples side by side when both are supported. Use realistic payloads. ````markdown theme={null} ```json JSON Example { "name": "Revenue Account", "assetCode": "BRL", "type": "deposit" } ``` ```go DSL Example (from @revenue :amount BRL 1000) ``` ```` Use `` for architecture, flow, or entity relationship diagrams. Link to the API reference, related entities, and relevant guides. **Reference pages:** [Transactions](/en/midaz/transactions) · [Accounts](/en/midaz/accounts) · [Holders](/en/midaz/crm/holders) ## Guide page *** Explains how to accomplish a specific task or implement a use case. Task-oriented and contextual — explains the "why" alongside the "how." One to two paragraphs explaining what the reader will accomplish, who this guide is for, and what problem it solves. Be specific — not "learn about X" but "configure X to handle Y." What must already be set up or understood. Link to relevant pages. Use H2 sections for major phases. Within each phase, use numbered steps or `` components. Start each step with a verb. Include: * Code blocks with realistic payloads * `` for best practices * `` for common pitfalls * `` for important context How the reader confirms the task was completed successfully. Cards or links to related guides, deeper concepts, or API references. Keep the main guide focused. If a section grows beyond the scope of the task (architecture deep dives, algorithm details, security design), move it to a separate child page and link to it. **Reference pages:** [Getting started with CRM](/en/midaz/crm/crm-getting-started) · [Pix plugin overview](/en/rails/pix/pix-overview) ## Plugin overview page *** Plugin overviews follow the same structure as [product overview pages](#product-overview-page), with these additions: * State clearly the plugin's licensing model (for example, source-available when shipped inside the Midaz repository, or Enterprise / licensed otherwise) and whether it requires a license. * Specify the **versioning relationship** with Midaz (e.g., "CRM version always matches the Midaz Core version"). * Include a **deployment model** section: how the plugin is deployed relative to Midaz (independently, same namespace, etc.). * Add a **requirements** section listing what the institution needs to operate the plugin (ISPB, connectivity provider, DevOps maturity, etc.). * If applicable, include a **trade-offs and challenges** section that honestly describes the operational responsibilities the plugin introduces. **Reference pages:** [What is CRM?](/en/midaz/crm/crm-overview) · [Pix in Lerian](/en/rails/pix/pix-overview) · [What is Fees Engine?](/en/midaz/fees/fees-engine-overview) ## Component patterns *** These Mintlify components appear across all guide templates. Use them consistently. | Component | When to use | | ------------------ | --------------------------------------------------------------------------- | | `` | Best practices, recommendations, cross-references to API specs | | `` | Important context that isn't a warning (OS compatibility, default values) | | `` | Common pitfalls, things that will cause errors if ignored | | `` | Security-critical information, data loss risks, compliance responsibilities | | `` | Sequential workflows (getting started, how it works) | | `` | Use cases, optional configurations, expandable details | | `` | Multiple code formats for the same operation (JSON + DSL, bash + YAML) | | `` | Next steps, related pages | | `` | Diagrams and illustrations with captions | | Glossary tooltips | First occurrence of domain-specific terms in a page | # Welcome to our Partner's Hub Source: https://docs.lerian.studio/en/partners-hub/introduction Discover the standards, processes, and resources you need to build, document, certify, and publish plugins for the Lerian platform and its partner marketplace. This portal is for **anyone building plugins** for the Lerian platform, including development partners, internal teams, and clients with custom needs. This hub covers standards and processes for building plugins. **By following the resources in this portal, you help ensure:** * A consistent and secure experience for Lerian clients. * Clear documentation that accelerates integration and support. * Faster validation and deployment of your plugin or service. For API docs and integration guides, visit the [main documentation site](/en/welcome-to-our-guides). ## What you'll find here *** * **[Coding standards](/en/partners-hub/technical-guidelines)**: Coding guidelines to build consistent, scalable, and production-ready plugins on Lerian. * **[Writing standards](/en/partners-hub/writing-standards)**: Writing standards we follow at Lerian to keep our documentation clear, consistent, and well-structured. ## The partner journey *** Building and publishing a plugin follows five stages: Read the coding standards, API conventions, error model, and writing guidelines in this hub. Develop your solution following the [technical guidelines](/en/partners-hub/technical-guidelines), including architecture, security, and testing requirements. Write guides and API references using Lerian's [writing standards](/en/partners-hub/writing-standards) and [templates](/en/partners-hub/templates). Package your plugin and documentation, then submit through the [approval process](/en/partners-hub/lifecycle-gov#approval-process). After passing [homologation requirements](/en/partners-hub/lifecycle-gov#homologation-requirements), your plugin goes live on the Lerian Marketplace. Before you start, read the [API standards](/en/partners-hub/api-standards) and [Error model](/en/partners-hub/error-model) pages. These define the contracts your plugin must follow. # Licensing basics Source: https://docs.lerian.studio/en/partners-hub/license-manager Understand how Lerian licenses enterprise features and plugins, why they exist, and how the modular monetization model is structured. Midaz, our source-available Core, is free to use under the **Elastic License 2.0 (ELv2)** — as is Fetcher, our source-available data ingestion service. But when it comes to monetizing the rest of the Lerian portfolio — enterprise products, features, and plugins, whether developed by Lerian or our partners — we rely on a licensing system to manage access. ## Why licenses matter *** Without licenses, we can’t **ensure payment** before usage, **tailor access** per customer, or **scale support** and commercial operations confidently. Licensing helps us protect revenue, simplify operations, and ensure that customers get what they paid for; nothing less, nothing more. ## How Lerian monetizes *** Lerian's business model is modular. Each product line may require a license to unlock access: | Revenue Line | Description | | :--------------------------- | :---------------------------------------------------------------------------------------- | | **Support** | Technical support + access to enterprise plugins as a bundle | | **Marketplace / Plugins** | Individual plugin licenses, billed monthly. Includes Lerian-built and third-party plugins | | **Learning & Certification** | Courses and certifications related to Midaz and its ecosystem | | **Professional Services** | Custom consulting and project delivery | Enterprise features included in the Support plan are actually plugins — but licensed as a unified bundle instead of one by one. ## Supported licensing scenarios *** Your plugin must support all the following use cases: | Scenario | Description | | :-------------------------------- | :---------------------------------------------------------- | | **Community** | Source-available. No license required | | **Support only** | One global license for Lerian enterprise features | | **Support + Lerian plugins** | One global license + individual licenses per Lerian plugin | | **Support + third-party plugins** | One global license + individual licenses per partner plugin | | **Support + mixed plugins** | Combined setup: Lerian + partner licenses | Our system must also support future SaaS models, allowing partners to license and distribute their own plugins independently. ## How License Manager works *** ### Powered by KeyGen We use [KeyGen](https://keygen.sh/) to manage license keys and their lifecycle. It supports: * License key generation * Status control (active, expired, revoked) * Periodic validation * Integration with future billing workflows Although we're currently using monthly billing cycles, our system is prepared for other intervals, such as biweekly, quarterly, or annual. ## License validation flow *** Every plugin or enterprise feature must regularly validate its license using the official SDK **once a week**. The SDK handles caching, retry logic, and graceful degradation, so you don’t need to build that logic yourself. ### Status outcomes | Status | Behavior | | :------------------------------------------------------- | :---------------------------------------------------------------- | | **Active license** | Full access and client in good standing. | | **License expired** | Continue access (2-week grace period) and show a warning. | | **License revoked** | Block access after the grace period until the license is restored | ## Architecture *** ### License validation overview License Manager 1 Jp Each plugin uses the **License SDK** to check if the license provided by the client is valid. Here's what happens behind the scenes: 1. The SDK sends the license key and client info to Lerian's server. 2. Lerian checks the license status: * Is it active? * Is it expired, but still within a grace period? * Has it been revoked? 3. The server replies with a response your plugin can use to allow, warn, or block access. 4. To improve performance, this result is stored in a local cache so it doesn’t need to check every time. **What you need to do**: Include the License SDK in your plugin. It will take care of validation and caching automatically. ### How each plugin uses licensing How a plugin uses the License SDK to validate against the licensing server and cache the result locally to allow, warn, or block access Every plugin or enterprise feature — whether developed by Lerian or a partner — uses the **License SDK** to validate its license. * Each plugin has its own license key. * The SDK checks the license by talking to Lerian's licensing system. * Lerian verifies that the license is valid using a secure backend (powered by KeyGen). * In future versions, licenses will be automatically managed based on billing. **What you need to do**: Add the License SDK to your plugin and make sure it calls `validateLicense()` before exposing any paid feature. ## Benefits for partners *** The License Manager is more than a gatekeeper; it’s a foundation for scaling responsibly and sustainably. ### Solves * Unauthorized use of paid features * Manual license tracking and revocation * Legal and compliance risks ### Enables * Transparent monetization of partner plugins * Flexible resale and bundling models * Configurable licensing policies: trials, renewals, tolerance windows Future versions will integrate with our billing system to automate license revocation and renewal. # License Manager error list Source: https://docs.lerian.studio/en/partners-hub/license-manager-error-list Reference every License Manager error code, title, and message so you can diagnose authorization failures and licensing issues on paid Lerian plugins quickly. License validation errors occur when a paid plugin is used without proper authorization. The system checks the `LICENSE_KEY` and `ORGANIZATION_IDS` variables set in the environment (for example, in your `.env` file) to ensure that only organizations with an active contract can access licensed features. This mechanism helps prevent unauthorized use of paid plugins, such as cases where there's no active agreement in place. ### Error format ```bash JSON theme={null} { "code": "", "title": "", "message": "" } ``` ### Field definitions * **`code`** – A stable, unique identifier for the error. Useful for programmatic handling and support requests. * **`title`** – A short, human-readable summary of the issue. * **`message`** – Detailed guidance to help you resolve the error. This structure ensures you always get actionable feedback when something doesn’t go as expected. ## License Manager errors *** Refer to the table below for a list of possible error codes, their meanings, and instructions on how to resolve them. | `code` | `title` | `message` | | :------- | :------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | LCS-0001 | Internal server error | An internal server error occurred. Please try again later or contact support if the problem persists. | | LCS-0002 | No organization IDs configured | The application is not configured with any organization IDs. | | LCS-0003 | No valid licenses found for any organization | No valid licenses were found for any of the configured organizations. Please check your license keys and ensure at least one organization has a valid license. | | LCS-0010 | Organization ID header is missing | The X-Organization-ID header is missing. Please ensure the header is included in the request. | | LCS-0011 | Organization ID header is unknown | The organization ID '12345' was not found in the list of configured organizations. Please verify you are using a valid organization ID. | | LCS-0012 | Failed to validate organization license | License validation failed for organization ID 12345. This could be due to network issues, invalid license keys, or server-side problems. Please verify your license key is correct and that you have network connectivity to the license server. | | LCS-0013 | Organization license is invalid | The license for organization ID '12345' is not valid and has no grace period active. Please renew your license or contact support for assistance. | | LCS-1001 | Validation error | Could not validate license key. Please try again later or contact your system administrator. | | LCS-1002 | License not found | There is no license for this license key. | | LCS-1003 | License expired | The license key has expired. | | LCS-1004 | License suspended | The license key has been suspended due to payment issues or administrative action. | | LCS-1005 | Fingerprint mismatch | The license key or organization ID are not valid for this license. | | LCS-1006 | Product mismatch | There is no license for this product. | # Plugin lifecycle Source: https://docs.lerian.studio/en/partners-hub/lifecycle-gov Follow the versioning, release, and support standards every plugin must meet to stay predictable across the Lerian ecosystem after launch. A plugin's responsibility doesn't end at release. These standards ensure plugins remain predictable, well-documented, and supported throughout their lifecycle. ## Versioning and release *** Versioning and release discipline are critical to ensuring predictability for clients and interoperability across the Lerian ecosystem. All plugins must follow the same versioning rules and align with Lerian's official release cycle. ### Versioning All plugins must use [**Semantic Versioning 2.0.0**](https://semver.org/). * **MAJOR**: incompatible API or behavior changes. * **MINOR**: backward-compatible feature additions. * **PATCH**: backward-compatible bug fixes. Check our simplified [Versioning](/en/updates-and-versioning/versions) page for practical examples. ### Release cycle All plugin releases must align with [Lerian's official release calendar](/en/updates-and-versioning/release-cycle). This ensures predictability and smooth upgrades for shared clients. * Internal development workflows remain independent * Public releases must follow the Lerian cycle * **Patch versions** for hotfixes or critical updates may be released outside the cycle Misaligned releases may be rejected from publication in the Lerian Marketplace. ## Technical documentation *** Clear, consistent documentation is mandatory for every plugin in the Lerian ecosystem. Your clients depend on it to adopt, integrate, and operate your solution effectively. All official plugin documentation must be hosted on the **Lerian Documentation Hub** to ensure a unified experience. ### Required documentation Every plugin must provide at least: * **Guides**: for business rules, setup, and usage. * **API Reference**: for all exposed endpoints, and technical documentation. You may still host your own documentation, but clients must be redirected to the official Lerian site for authoritative technical content. ### Standards and templates To guarantee consistency and usability, all documentation must follow the [Lerian Writing Standards](/en/partners-hub/writing-standards). These templates are mandatory and ensure that end users receive high-quality, accessible documentation across the ecosystem. ### Why this matters Great documentation is a compliance requirement, not a nice-to-have. It reduces onboarding friction, prevents misuse, and keeps client support efficient. Plugins without proper documentation will not be approved for publication. ## Approval process *** Before a plugin can be published on the Lerian Marketplace, it must pass a strict **homologation process**. This ensures that every solution meets Lerian's standards for quality, security, and compliance. Plugins that fail any of these checks will not be approved for publication. All identified issues must be resolved before resubmission. ### Homologation requirements Before being published, **all plugins** must pass a formal review, which includes: * **Technical checklist** – architecture, coding standards, observability, and API compliance. * **Security checklist** – pentests, audits, and data protection controls. * **Quality & performance checks** – test coverage, scalability, and stress test reports. * **Regulatory compliance review** – verification against BACEN and other governing bodies. * **End-to-end testing** – validation with **Midaz** or other **Lerian solutions** to confirm interoperability. ## Severity-based SLAs *** If your plugin is distributed under a support agreement where **you, as the development partner, are responsible for maintenance**, you must follow Lerian's severity-based SLA guidelines. These rules ensure timely resolution of issues and a consistent experience for clients across the ecosystem. ### Why this matters Meeting SLAs reinforces trust in the Lerian ecosystem. Clients can rely on predictable response times, while partners demonstrate accountability and professionalism in supporting their solutions. ### Resolution timelines These SLAs define the maximum time allowed to deliver a fix, based on the severity of the reported issue: | **Severity** | **Required resolution time** | | :----------- | :------------------------------ | | High | Fix within **5 business days** | | Medium | Fix within **10 business days** | | Low | Fix within **30 business days** | Timelines refer to **business days** and apply from the moment the issue is confirmed. These SLAs are **mandatory** for all partner-supported plugins. This ensures a consistent experience for users and reinforces trust in the Lerian ecosystem. # Quality and performance Source: https://docs.lerian.studio/en/partners-hub/quality-performance Review the testing, performance, and resilience standards every plugin must meet to be approved for the Lerian Marketplace — coverage, load, and SLO targets. Plugins in the Lerian ecosystem must demonstrate high standards of quality and resilience. Testing and performance are non-negotiable requirements to ensure reliability for shared clients in production. This section defines the minimum expectations every partner solution must meet before being approved for the Lerian Marketplace. ### Related practices For faster client adoption, we strongly recommend providing SDKs in popular languages. While not required, SDKs help reduce integration errors and improve developer experience. Refer to the [SDK Guidelines](/en/partners-hub/sdk-guideline) for best practices. ## Testing standards *** Your plugin must demonstrate: * **≥ 90% unit test coverage** * Integration and contract tests for external systems * A visible test coverage report We also recommend automated CI/CD checks, linting, and peer reviews. ## Performance testing *** Performance testing is **mandatory**. Your plugin must be: * Stateless and horizontally scalable * Resource-efficient (CPU, memory, connections) * Fault-tolerant and high availability ready A stress test report must be included in the repository. # SDKs guidelines Source: https://docs.lerian.studio/en/partners-hub/sdk-guideline Follow Lerian's SDK best practices — repository layout, error handling, examples, and version alignment — to accelerate plugin adoption. While not mandatory, SDKs are a **strong recommendation** for every plugin in the Lerian ecosystem. They accelerate adoption, improve developer experience, and reduce integration errors for clients. By offering SDKs in popular languages, you make it easier for users to interact with your plugin and build reliable solutions on top of it. **Best practices for SDKs:** * Support the most common use cases out of the box * Provide clear error handling aligned with Lerian’s [Error Model](/en/partners-hub/error-model) * Include examples and tests for key functions * Keep versions in sync with the plugin’s releases Go and TypeScript SDKs are preferred, as they align with Lerian's main stack and partner ecosystem. ## Repository structure *** Organize your SDK repository with a consistent layout: ``` your-plugin-sdk/ ├── README.md # Overview, installation, and quickstart ├── CHANGELOG.md # Version history following Keep a Changelog format ├── LICENSE ├── src/ # Source code ├── examples/ # Working code samples for common use cases ├── tests/ # Unit and integration tests └── docs/ # Additional documentation (optional) ``` Include a `README.md` with installation instructions, a minimal usage example, and a link to the full documentation. ## Naming conventions *** Align package and module names with Lerian product names: * Use lowercase for package names: `midaz`, `feesengine`, `tracer` * Prefix with your organization name when publishing to registries: `@yourorg/midaz-plugin-sdk` * Keep naming consistent across languages: if the Go module is `midaz-plugin-sdk`, the npm package follows the same pattern ## Versioning *** Follow [Semantic Versioning (semver)](https://semver.org): * **MAJOR** -- breaking changes to the SDK's public API * **MINOR** -- new features, backward-compatible * **PATCH** -- bug fixes, backward-compatible Sync SDK releases with your plugin releases. When the plugin version changes, release a matching SDK version. Document the plugin-to-SDK version mapping in your `README.md`. ## Error handling *** Wrap responses from the Lerian [Error model](/en/partners-hub/error-model) into typed errors: * Map each Lerian error code to a specific error type or class in your SDK * Include the original error `code`, `message`, and `details` fields in the typed error * Provide helper methods for checking error types (e.g., `IsNotFoundError()` in Go, `instanceof NotFoundError` in TypeScript) * Never swallow errors silently -- always propagate or wrap them with context ## Minimum languages *** Provide SDKs in at least these two languages: | Language | Rationale | | ---------- | ------------------------------------------------------ | | Go | Primary language of the Lerian backend stack | | TypeScript | Primary language for frontend and Node.js integrations | Additional language support (Python, Java, C#) is encouraged but not required. # Security and compliance Source: https://docs.lerian.studio/en/partners-hub/security-compliance Review the mandatory security and compliance baseline — OWASP ASVS, CIS, secret management, audits, and LGPD — every Lerian plugin must meet. **Security is not optional; it's a baseline.** Every plugin in the Lerian ecosystem must be designed with security, compliance, and regulatory obligations in mind. These requirements protect end-users, maintain trust, and ensure interoperability across environments. This section covers mandatory standards for secure development, audits, data protection, and regulatory alignment. Plugins that fail to meet these security and compliance standards will not be approved for publication in the Lerian Marketplace. ## Security by design *** All plugins must comply with: * **OWASP ASVS Level 3** * **CIS Benchmarks** for container, K8s, and OS hardening * Integration with secret managers like **Vault** or **AWS Secrets Manager** ## Pentests and security audits *** A **pentest is mandatory** before publication and **must** be repeated at least **every 6–12 months** or whenever major changes are introduced. Don't have a provider? We can recommend trusted security partners. ## Data protection and LGPD *** All plugins must fully comply with the **LGPD** and equivalent data protection laws. * Personal data must be **encrypted at rest and in transit**. * **No exceptions are allowed.** ## Regulatory compliance *** Your plugin must follow all applicable regulations, including: * BACEN circulars * Other relevant governing bodies # Technical guidelines Source: https://docs.lerian.studio/en/partners-hub/technical-guidelines Explore the technical expectations for partner solutions — architecture, APIs, quality, security, and SDKs — across the Lerian Marketplace. These guidelines define the expectations for partners developing solutions that will be published on the Lerian Marketplace. ## What you'll find here *** * **[Architecture and operations](/en/partners-hub/architecture-operations)**: Language requirements, MSP model, observability, auditability, and authentication standards * **[API standards](/en/partners-hub/api-standards)**: Guidelines for designing consistent, reliable, and developer-friendly APIs across Lerian. * **[Quality and adoption](/en/partners-hub/quality-performance)**: Testing coverage thresholds, performance testing requirements, and SDK recommendations * **[Security and compliance](/en/partners-hub/security-compliance)**: OWASP compliance, pentests, data protection (LGPD), and regulatory alignment * **[SDK guidelines](/en/partners-hub/sdk-guideline)**: Standards for SDK design, versioning, and error handling aligned with Lerian’s architecture. # Documentation templates Source: https://docs.lerian.studio/en/partners-hub/templates Explore the official documentation templates Lerian uses for guides and API references so your content stays consistent and clear. This section provides the official documentation templates used across the Lerian ecosystem. If you're building a product or integration for Lerian, these templates will help you write consistent, clear, and helpful documentation, the way we need it. They cover both the **structure** and **tone** we expect across all public and internal docs. ## What you'll find here *** * **[Guides templates](/en/partners-hub/guides-template)**: Template and best practices for structuring guides that are clear, actionable, and aligned with Lerian’s documentation standards. * **[API Reference templates](/en/partners-hub/api-reference-template)**: Template and standards for documenting APIs with consistent structure, clear schemas, and reliable integration patterns. # Voice and tone Source: https://docs.lerian.studio/en/partners-hub/voice-tone Learn how Lerian writes — assertive, tech-friendly, and clear by design — so every guide, reference, and changelog sounds consistent. > Builder-first. Clear by design. Serious about systems. This page defines how Lerian communicates across all documentation. Every guide, API reference, tutorial, and changelog follows these principles. They exist to keep our voice consistent, precise, and recognizable — regardless of who writes. ## Assertive *** Lerian speaks with confidence. No hedging, no vague suggestions, no unnecessary qualifiers. * Gets straight to the point. * Explains with clarity. * Avoids excess context when it doesn't add value. Don't say "this might help you…". Say "Use this endpoint to…". ## Tech-friendly *** Technical when it needs to be, but never inaccessible. * Uses correct industry terms. * Doesn't oversimplify technical concepts. * Doesn't talk down to the reader. * Assumes the reader is a builder. Our communication is designed for: * Developers * Tech leads * Partners * Product teams ## Encouraging *** Lerian empowers those who build. * Shows possibilities. * Explains impact. * Makes the reasoning behind technical decisions clear. Not arrogant. Collaborative. ## Direct *** Every sentence justifies its existence. No filler, no fluff, no detours. * Short sentences. * Explanations in logical order. * No excessively long paragraphs. * No unnecessary em dashes. ## Structured *** Information is organized, predictable, and scannable. * Bullet points when they help. * Tables for comparisons and reference data. * Headings that describe content, not decorate it. * Most important information first. ## Humble and pragmatic *** Lerian doesn't promise miracles. * Avoids hype. * Doesn't use empty buzzwords. * Explains limitations when necessary. * Focuses on solving real problems. ## Inclusive and conscious *** * Avoids ableist terms. * Avoids aggressive metaphors. * Maintains professional, respectful language. ## Tone by document type *** Different documents serve different readers. The tone adapts — the voice stays the same. | Document type | Tone | Description | | ------------- | ----------------------------- | --------------------------------------------------------------------------------------- | | Guides | Business-oriented, contextual | Explain the "why" behind decisions. Provide context for when and how to apply concepts. | | API Reference | Technical, dry, precise | State facts. Describe inputs, outputs, and behavior. No narrative. | | Tutorials | Step-by-step, imperative | Use numbered steps. Start each step with a verb. Keep instructions short. | | Changelogs | Factual | State what changed, what it affects, and what action is needed. No commentary. | ## Banned words *** Certain words dilute precision or add no meaning. They sound professional but communicate nothing. Replace or remove them. | Word/phrase | Why | Alternative | | ---------------- | --------------------------------------- | ---------------------------------------- | | leverage | Vague corporate jargon | use | | robust | Does not describe what it actually does | Describe the specific capability | | seamless | Hides integration complexity | Describe the actual integration behavior | | powerful | Empty superlative | Describe the specific capability | | simple / easy | Subjective and dismissive of effort | Remove, or describe the steps | | just | Minimizes complexity | Remove | | obviously | Assumes reader knowledge | Remove | | please note that | Unnecessary filler | State the information directly | | in order to | Wordy | to | | utilize | Unnecessarily formal | use | | facilitate | Vague | enable, allow | ## Examples *** Compare off-tone and on-tone writing: | Off-tone | On-tone | | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | This powerful feature allows you to seamlessly integrate with our robust API. | This feature connects your service to the Midaz API through webhook events. | | You might want to consider using the retry mechanism. | Use the retry mechanism to handle transient failures. | | Please note that the API utilizes OAuth 2.0 in order to authenticate requests. | The API authenticates requests with OAuth 2.0. | | It's really easy to get started with Midaz -- just install the SDK and you're good to go! | Install the SDK, then follow the setup steps in the quickstart guide. | # Our writing guidelines Source: https://docs.lerian.studio/en/partners-hub/writing-guidelines Follow the Lerian writing guidelines that keep documentation clear, scannable, and consistent — voice, tone, structure, and formatting across the ecosystem. Every piece of documentation published on Lerian Docs must follow the same writing guidelines. These rules ensure that content is clear, consistent, and aligned with how Lerian communicates. Whether you're writing a new guide or updating an existing API reference, apply these practices so your content feels cohesive and useful to developers, tech leads, and partners. By following these guidelines, you: * Keep documentation readable and scannable. * Maintain a consistent voice across the entire ecosystem. * Reduce ambiguity and support requests. ## What you'll find here *** * **[Voice and tone](/en/partners-hub/voice-tone)**: Standards for writing content aligned with Lerian’s voice. * **[Capitalization](/en/partners-hub/capitalization)**: Guidelines for consistent capitalization to improve clarity and readability across Lerian content. # Our writing standards Source: https://docs.lerian.studio/en/partners-hub/writing-standards Adopt the Lerian writing standards and page templates so every guide, tutorial, and reference shares the same voice, structure, and end-to-end usability. Clear, consistent documentation is as critical as clean code. To keep the Lerian ecosystem accessible and professional, all guides and references must follow our official writing standards and templates. These rules ensure that every piece of documentation shares the same voice, structure, and usability. Partners are expected to apply them in every deliverable that will appear on the Lerian Docs site. By following these standards, you: * Guarantee clarity and readability for clients. * Reduce onboarding time and support requests. * Keep documentation aligned with Lerian’s voice and tone. # What is Access Manager? Source: https://docs.lerian.studio/en/platform/access-manager/access-manager Access Manager controls human and machine access across Lerian products using Auth, Identity, and product-level authorization for fine-grained permissions. Every request to a Lerian product raises two questions: who is making it, and what are they allowed to do? Authentication answers the first, authorization the second. **Access Manager** handles both. **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. Set `AUTH_REQUIRED=true` in the product integration when requests must be refused if its Auth client is unavailable or misconfigured. Under the hood, 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`; set `AUTH_M2M_INVERSION_ENABLED=true` to use the application's token `sub`. **Test the Access Manager locally** Run Lerian's plugins without deploying to Kubernetes using our [plugins-docker-compose repository](https://github.com/LerianStudio/plugins-docker-compose). Keep in mind that these services require a valid license configuration. License checks run through HTTP middleware, so validate the license before enabling protected products. For license details, check our [License documentation](/en/reference/lerians-license). ## 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 multi-tenant deployments**, tenant context comes from trusted token claims, never from request payloads or arbitrary headers. * In **BYOC single-tenant 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. 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/access-manager/am-auth-apis) and [Identity](/en/reference/access-manager/am-identity-apis) operations. * Lerian Console settings for supported visual user and application management. * Product-level authorization enforcement for protected HTTP and gRPC 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, and keeping them separate saves you trouble later: | 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 is running, 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. Built-in resources, actions, roles, and permission sets are a different story. Ship changes to those through controlled platform updates like migrations or an idempotent reconciler, and don't edit bootstrap seed data to change access in an environment that's already running. ## Multi-tenant behavior *** In SaaS and BYOC multi-tenant deployments, the tenant is part of who the caller is, not something they pass in. Access Manager reads it from trusted JWT claims during user flows, and from the application's organization during machine-to-machine flows. 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. 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. [Explore the Access Manager API](/en/reference/access-manager/am-api-introduction). Browse endpoints for users, groups, applications, and permissions. # 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, which makes it one of the most sensitive pieces of your stack. 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 and must be configured 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 it is no longer required, and handle any existing tokens or credentials through your environment's incident-response procedure. ### Use user-based credentials for manual actions When human access is required (for debugging, operations, or support), issue per-user credentials instead of sharing `client_credentials`, so you can 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`; it 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're changing, and the line runs between everyday operations and platform data. ### Use the management surfaces after bootstrap After the environment is running, 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. See our [Security Recommendations](/en/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 is built from 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. Runtime tokens, MFA verification, user information, permission decisions, and cache. Management of users, groups, applications, communication providers, and MFA configuration. Route-level checks inside protected Lerian products that call Auth before business logic when their Auth client is enabled and configured. 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 Understand the difference between installing, enabling, and operating Access Manager across Lerian products, and how each stage maps to deployment tasks. Access Manager setup is divided into three lifecycle stages that are easy to mix up. Understanding the difference between them makes the rest of the setup much simpler. ## Installing *** Deploy Auth and Identity, configure their dependencies, and confirm the services are reachable and healthy. * Who does it: Platform operator * When: Once per environment, before any product enforces access. ## Enabling *** Turn on Access Manager enforcement inside each Lerian product or plugin by setting the Auth environment variables in that product's `.env`. * Who does it: Product operator * When: After installation, per product. ## Using *** Perform day-to-day access operations such as requesting tokens, managing users and groups, configuring MFA, and calling protected APIs. * Who does it: Administrators, end users, integrations * When: Continuously, after the environment is up. ## In short *** * **Installation** puts the gate in place. * **Enabling** tells each product to check the gate. * **Using** is everything that happens at the gate from then on. If a Lerian product is enabled before Access Manager is installed and healthy, every protected request will fail. Install first, validate, then enable. ## 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, giving 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 ### 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. * Validation results can be cached to reduce repeated identity-provider calls. ### Organization context For authorization, Auth uses the JWT `owner` claim when it is present and otherwise the configured organization. The product that receives an authorized request owns its own tenant data context and data-plane isolation. Do not infer a cross-product tenant-resolution flow from Auth alone. For multi-tenant deployment configuration, follow the relevant product and [multi-tenancy](/en/multi-tenancy) documentation. ### 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 domain 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. 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 or SMS MFA, the client submits the returned MFA token and the selected method to request challenge delivery, and Auth sends the challenge code through that method. * A TOTP application generates its passcode locally, without a delivery request. 3. **Challenge verification** * The user submits the MFA token and either a passcode or a recovery code, not both. * 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. * Successful authorization decisions can be cached 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 validates the token and retrieves user details from the identity provider. * 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. * Related cache entries are cleared. ## 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/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 does not invent permissions; it evaluates the data Identity manages. 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/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, attempt counters, and remember-device state when configured. ## Testing and reliability *** **Auth** undergoes continuous testing to maintain reliability and security. Tests cover: * **Authentication and token validation flows**. * **Access control enforcement**. * **Performance and caching efficiency**. Auth also runs ongoing security assessments and monitoring across these flows. # Enabling Access Manager Source: https://docs.lerian.studio/en/platform/access-manager/enabling-access-manager Activate Access Manager across Lerian products — set 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, you turn it on in each product by setting the Auth variables in the `.env` file of **any Lerian product or plugin** where you want it active. Enabling Access Manager only turns on authorization enforcement in a product or plugin. Access data such as users, groups, applications, providers, roles, and permissions is managed separately through Access Manager. 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 are configuring: ```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 Direct JD PLUGIN_AUTH_ENABLED=true PLUGIN_AUTH_HOST=http://plugin-auth:4000 ``` For a product that supports BYOC multi-tenancy, configure the complete product-specific set of variables. For Midaz Ledger, enabling multi-tenancy also requires the URL of its Tenant Manager Service and a Valkey host. Tenant Manager listens on port `4026` by default: ```text Text theme={null} # Midaz Ledger multi-tenant configuration MULTI_TENANT_ENABLED=true MULTI_TENANT_URL=https://tenant-manager. MULTI_TENANT_REDIS_HOST=valkey ``` Point `MULTI_TENANT_URL` at an HTTPS endpoint so `MULTI_TENANT_SERVICE_API_KEY` does not travel over cleartext HTTP. In-cluster HTTP (`http://tenant-manager..svc.cluster.local:4026`) is for local, non-production clusters only and requires the explicit opt-in `MULTI_TENANT_ALLOW_INSECURE_HTTP=true`. In Helm deployments, Midaz also needs `MULTI_TENANT_SERVICE_API_KEY`, normally supplied through a Secret or an existing Secret. The flag alone is not a complete multi-tenant configuration; follow the deployment documentation for the product you are configuring. If your Tenant Manager `SERVER_ADDRESS` or Kubernetes Service overrides the default port, use that deployed address instead. Once **Access Manager** is enabled, 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`. The CRM component uses `PLUGIN_AUTH_ADDRESS` in its own configuration. * `/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, Flowker, Bank Transfer, and Fetcher use `PLUGIN_AUTH_ADDRESS`. * Pix Indirect BTG uses `PLUGIN_AUTH_ADDRESS` in its `pix` service and `PLUGIN_AUTH_HOST` in its `inbound` and `outbound` workers. * Pix Direct JD uses `PLUGIN_AUTH_HOST`. 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 in that repository. The Access Manager source uses the following commands: In your terminal, go to the Access Manager repository root. If Docker is running, stop it: ```bash Bash theme={null} make down ``` Then build and start the stack: ```bash Bash theme={null} make up ``` ## Deployment lifecycle *** Access Manager setup has two phases, and bootstrap differs by deployment mode: * **Single-tenant bootstrap** seeds a new environment with the base organization, roles, groups, applications, and permission sets required by the platform. * **Multi-tenant bootstrap** prepares shared certificate material only. It does not seed tenant organizations, users, groups, applications, or permission sets; create and manage tenant access data after the tenant exists. * **Operation** starts after the environment is running. 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. Bootstrap seed data is only applied 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. # 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 *** Human access is managed through users, groups, and direct user permissions. A group represents a permission set for a product or Access Manager area. For example, a user can be assigned to a Midaz viewer group to inspect ledger data without changing it, and 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 are paginated with `page` and `limit`. In multi-tenant deployments, users and groups are scoped from the bearer token. The service reads the tenant organization from the authenticated context and returns only the users and groups that belong to that tenant. In single-tenant deployments, the same endpoints return the environment-wide set. When you create or update a user, send the group IDs returned by [List Groups](/en/reference/access-manager/list-groups). The API handles the internal organization prefixing; clients should not build Casdoor-style `organization/group` values manually. 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. | Roles are scoped 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/access-manager/list-groups) and [Retrieve Group Details](/en/reference/access-manager/retrieve-group-details) to inspect the groups available in your environment. 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. 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. 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. The current M2M permission catalog includes these application names: | Application name | Typical use | | ---------------------------- | ------------------------------------------ | | `midaz` | Core ledger automation. | | `plugin-fees` | Fee package, fee, and estimate automation. | | `plugin-crm` | CRM holder and alias automation. | | `reporter` | Report and template automation. | | `fetcher` | Fetcher ingestion automation. | | `plugin-br-pix-jd` | Pix Direct JD automation. | | `plugin-br-pix-indirect-btg` | Pix Indirect BTG automation. | | `plugin-br-bank-transfer` | Bank Transfer automation. | | `plugin-br-pix-switch-spi` | Pix Switch SPI automation. | | `plugin-br-pix-switch-dict` | Pix Switch DICT automation. | | `plugin-br-pix-switch-cob` | Pix Switch COB automation. | | `flowker` | Flowker workflow automation. | | `streaming-hub` | Streaming Hub automation. | | `br-sta` | STA file-transfer automation. | | `br-sisbajud` | Sisbajud automation. | Application names are product identifiers, not UI display labels. Identity accepts only names in this catalog when it creates or deletes applications. Some products, such as Tracer, have platform-managed M2M permission sets seeded by Access Manager but are not part of this self-service creation catalog. Identity filters internal applications from the public application list. In multi-tenant mode, it also returns only applications bound to the caller's tenant organization. ### Tenant scoping In multi-tenant deployments, 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 the email or SMS delivery services available to applications, including MFA flows. They are managed separately from applications so the same provider can be reused and controlled 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 authentication route. For SSO, Identity manages one active OAuth provider per tenant. Supported provider types are Google, Microsoft, Okta, and Custom. Configuring an SSO provider disables local password login by default; configure the SSO policy explicitly if the tenant must keep local password login. 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. ## 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 can use supported methods such as authenticator app, email, or SMS, depending on the environment configuration and the user profile data available. 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 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 unavailable 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/access-manager/am-identity-apis) documentation. # 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. It's the step that has to happen *before* any Lerian product can enforce authentication or permissions. This guide is written for operators who are setting up the environment. It explains what installation actually covers, what you need in place, and the order in which to bring things up. For the Helm chart commands and chart-level configuration, follow the [Access Manager via Helm](/en/platform/helm/access-manager/access-manager-helm) page. ## Prerequisites *** You don't need to be deep in Kubernetes to plan an installation, but you do need a clear picture of the pieces involved. Treat 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/reference/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, Casdoor configuration, and a few operational defaults later on. * **SaaS** - Lerian runs the platform; access flows are tenant-scoped from trusted JWT claims. * **BYOC multi-tenant** - You run Access Manager, and tenant context comes from trusted token claims, never from request payloads or headers. * **BYOC single-tenant** - You run Access Manager against a configured default organization and Casdoor identity backend. If you're not sure which model applies, confirm with your Lerian contact before installing. ### Auth service 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 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 Casdoor 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 Casdoor backend for identity storage. Casdoor persists its identity data in the configured PostgreSQL database; PostgreSQL backs Casdoor, it does not replace it. In SaaS and most BYOC deployments, the backend comes pre-configured. For a BYOC deployment, configure that Casdoor backend for your environment; Access Manager does not connect directly to arbitrary identity-provider APIs. ### Data and cache services * A **PostgreSQL** instance as the backing database for Casdoor-managed identity data. * A **Valkey** cache for token, permission, and MFA-related operations. Both must be provisioned and 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. * Neither service should be exposed publicly without the appropriate ingress controls. ### Environment and bootstrap data A fresh single-tenant environment seeds base access data, including its organization, built-in roles, groups, applications, and permission sets, once at startup. Multi-tenant bootstrap prepares shared certificate material only; tenant organizations and access data are created after the tenant exists. This is the **bootstrap** layer. After the environment is running, day-to-day changes happen through the Identity APIs or Lerian Console, not by editing bootstrap data. Bootstrap seed data is only applied 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, and skipping ahead is the most common cause of avoidable trouble later. Pick SaaS, BYOC multi-tenant, or BYOC single-tenant and confirm any model-specific decisions with your Lerian contact: tenant strategy, Casdoor configuration, and licensing scope. Write this down because every later step refers back to it. 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 Casdoor-managed identity data; * a Valkey cache for tokens, permissions, and MFA; * the configured Casdoor 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. Install the Access Manager services into your environment. For Kubernetes, the supported path is the Helm chart. See [Access Manager via Helm](/en/platform/helm/access-manager/access-manager-helm) for chart versions, OCI registry, and the exact `helm install` command. If you only need to evaluate Access Manager locally before a real deployment, use the source repository's `make up` command. It builds and starts the Access Manager stack. Use the lifecycle commands documented by the repository revision you run. In single-tenant mode, first startup seeds the base organization, built-in roles, groups, applications, and permission sets that the platform relies on. In multi-tenant mode, bootstrap does not seed tenant organizations or access data; create that data after tenant creation. 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. 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 (Casdoor's backing database) and Valkey are connected and responsive. * The Casdoor identity backend is configured and reachable. * A test token request succeeds against Auth, and a basic Identity call (for example, [List Groups](/en/reference/access-manager/list-groups)) succeeds for the selected deployment mode. In multi-tenant mode, create tenant access data before expecting groups to be returned. If anything in this list fails, fix it here. Don't enable products on top of an unhealthy install. Once installation is validated, turn enforcement on inside each Lerian product or plugin by setting the Auth variables in its `.env`. The full per-product configuration, including multi-tenant settings, lives in [Enabling Access Manager](/en/platform/access-manager/enabling-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. Treat installation as its own milestone. Auth and Identity should be deployed, healthy, and validated *before* you flip any product into enforcement mode. That single habit avoids most first-day incidents. ## 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 via Helm](/en/platform/helm/access-manager/access-manager-helm) - chart versions, OCI registry, and installation commands. * [How Access Manager works](/en/platform/access-manager/am-components) - a closer look at Auth, Identity, and product-level enforcement. # 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 configured inside each protected Lerian product. It is not a third Access Manager service. When the product's Auth client is enabled and has an Auth address, its route-level code calls Auth and either 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 is where those decisions are actually applied 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. Route protection is configured separately in each product. Its Auth client must be enabled and have an Auth address. Set `AUTH_REQUIRED=true` to make protected routes refuse with `503` if that client is disabled or misconfigured; without it, the middleware passes the request through by default. `PLUGIN_AUTH_ENABLED` configures Identity's Auth client only; it does not enable enforcement in every Lerian product. 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. 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, lib-auth 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 Identity stores each tenant's IP allowlist and the surfaces where it applies. A non-empty list is enforced 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 the product integration so lib-auth forwards the resolved client IP, and configure `TRUSTED_PROXIES` in Auth before enforcing a list behind a proxy. The gate runs before Auth's permission cache; tokens marked as internal bypass it. An empty list, a missing or unusable client IP, an untrusted proxy, or unavailable allowlist data does not deny the request. A token is internal when it carries the `isInternal` claim with the value `true`. Casdoor 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 Casdoor before it authorizes the request, so a forged internal marker on an invalid token does not grant access. These branches fail open. A missing or unusable client IP, an untrusted proxy, or unavailable allowlist data lets the request continue to the permission check. If you rely on the allowlist as a security control, monitor your deployment for these conditions. In deployments where platform services call Casdoor from cluster networks, configure `PLATFORM_INTERNAL_CIDRS` in Identity with those CIDRs. Identity co-stores those ranges only while a tenant list exists so Casdoor's native checks allow platform traffic; Auth subtracts them from the tenant policy before enforcement. ### Resource and action names Resource and action names are exact strings. 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 the route is not best described by an HTTP method: | 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/access-manager/retrieve-user-permissions) to inspect the effective resources and actions available to the authenticated user. 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 missing or malformed, the request is rejected before any permission check is attempted. 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 and can be denied by a tenant IP allowlist active for that request's scope. 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 runtime decision side is handled by the [Auth service](/en/platform/access-manager/auth-plugin). For the day-to-day workflow against the APIs, see [Using Access Manager](/en/platform/access-manager/using-access-manager). # 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 ``` When a product's Auth client is enabled and configured, requests without a valid bearer token are rejected on its protected routes, even if the endpoint was previously reachable without authentication. In SaaS and BYOC multi-tenant deployments, that token also carries your tenant context in trusted claims like `tenantId`, so you never pass tenant identifiers in payloads or headers yourself. Learn more about [multi-tenancy](/en/multi-tenancy). With the Identity APIs, the token is also your tenant boundary: list endpoints return only the users, groups, and applications in your tenant, and create, update, and delete operations stay inside it. ## Human access *** Follow this flow when a person needs to access Lerian products. Use [List Groups](/en/reference/access-manager/list-groups) to see the groups available in your environment. Use [Retrieve Group Details](/en/reference/access-manager/retrieve-group-details) when you need to inspect a specific group's permissions before assigning it. In multi-tenant deployments, the list is scoped to the tenant carried by the bearer token. Use the returned group IDs as-is when creating or updating users. Check each group's resources and actions before assigning it. Access Manager permissions are evaluated as exact resource-action pairs, such as `reports:get`, `users:patch`, or `transfers:read`. 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. Use [Create a User](/en/reference/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. Use [Request an Access Token](/en/reference/access-manager/request-access-token) with the `password` grant type. The returned access token is used as the bearer token when the user calls protected APIs. Use [Refresh the Access Token](/en/reference/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/access-manager/list-users) — list users. * [Retrieve User Details](/en/reference/access-manager/retrieve-user-details) — inspect a user. * [Update a User](/en/reference/access-manager/update-user) — update user information and group assignments. * [Delete a User](/en/reference/access-manager/delete-user) — remove user access. * [Reset a User Password](/en/reference/access-manager/reset-user-password) — reset a user's password through an administrative flow. * [Update a User Password](/en/reference/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. Use [Create an Application](/en/reference/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. Use [List Applications](/en/reference/access-manager/list-applications), [Retrieve Application Details](/en/reference/access-manager/retrieve-application-details), or [Delete Application](/en/reference/access-manager/delete-application) when you need to review or remove machine-to-machine access. Identity hides internal applications from the list. In multi-tenant deployments, it only returns applications bound to the caller's tenant organization. Use [Request an Access Token](/en/reference/access-manager/request-access-token) with the `client_credentials` grant type. The returned access token is used as the bearer token for the integration's API calls. ### Current M2M application catalog The current application catalog accepts these application names when you create machine-to-machine applications: | Application name | Product | | ---------------------------- | ----------------- | | `midaz` | Midaz Ledger | | `plugin-fees` | Fees Engine | | `plugin-crm` | CRM | | `reporter` | Reporter | | `fetcher` | Fetcher | | `plugin-br-pix-jd` | Pix Direct JD | | `plugin-br-pix-indirect-btg` | Pix Indirect BTG | | `plugin-br-bank-transfer` | Bank Transfer | | `plugin-br-pix-switch-spi` | Pix Switch (SPI) | | `plugin-br-pix-switch-dict` | Pix Switch (DICT) | | `plugin-br-pix-switch-cob` | Pix Switch (COB) | | `flowker` | Flowker | | `streaming-hub` | Streaming Hub | | `br-sta` | STA | | `br-sisbajud` | Sisbajud | Identity accepts only names in this catalog when it creates or deletes machine-to-machine applications. ## Provider setup *** Use providers when an application needs a configured communication provider for MFA delivery, such as email or SMS. 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/access-manager/list-providers). 2. Link the provider to the application with [Link Provider to Application](/en/reference/access-manager/link-provider-to-application). 3. If the application has multiple linked providers, use [Set Default Application Provider](/en/reference/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. Use [Initiate MFA Setup](/en/reference/access-manager/initiate-mfa-setup) on the signed-in user's own account and selected MFA method. Use [Verify MFA Passcode](/en/reference/access-manager/verify-mfa-passcode) to confirm the method. Use [Enable MFA](/en/reference/access-manager/enable-mfa) after setup verification. Use [Get MFA Status](/en/reference/access-manager/get-mfa-status), [Set Preferred MFA Method](/en/reference/access-manager/set-preferred-mfa-method), or [Disable MFA](/en/reference/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/access-manager/initiate-mfa-challenge) with the MFA token and selected method, then [Verify MFA Login](/en/reference/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/access-manager/retrieve-user-information) returns their OIDC-compatible profile, and [Retrieve User Permissions](/en/reference/access-manager/retrieve-user-permissions) shows the resources and actions they can reach. To end a session, call [End User Session](/en/reference/access-manager/end-user-session) with the required `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/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. ## Multi-tenant access rules *** The public API workflow uses the same endpoints in single-tenant and multi-tenant deployments, but multi-tenant behavior adds tenant-specific credential resolution and validation: * In multi-tenant deployments, Auth and Identity resolve the tenant from trusted token or application context. For password grants, Auth uses tenant application credentials when available and keeps the token cache tenant-aware. Identity also permits new-user creation only when the email domain can be confirmed to match the tenant's reference administrator. * In single-tenant deployments, Access Manager uses the configured default organization. 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. # 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. Not everything in Access Manager needs an API call. Lerian Console gives you a visual home for the access tasks most administrators handle day to day. From the Console's Settings area, you can create and manage users, assign permission groups, and set up machine-to-machine applications without touching the Identity APIs. If you need the full API surface, including providers, application-provider links, MFA setup, 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 Once **Access Manager** is enabled, 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. ## Accessing Access Manager settings *** Access Manager features are available through the Settings area in Lerian Console: Log into Lerian Console with your admin credentials. Click the gear icon () in the top navigation bar. Select **Users** or **Applications** from the sidebar to manage the respective features. ## What you can do *** Lerian Console Settings offers two main Access Manager areas: Create, edit, and delete user accounts. Manage passwords and assign users to permission groups for human access. 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, but not all of them. For anything that isn't in Console settings, reach for the [Identity APIs](/en/reference/access-manager/am-identity-apis) and [Auth APIs](/en/reference/access-manager/am-auth-apis): | Area | Use APIs for | | ----------------- | ----------------------------------------------------------------------------------------------- | | Providers | Create, update, delete, link, unlink, and set default communication providers for MFA delivery. | | MFA | Initiate setup, verify passcodes, enable, disable, and change the preferred MFA method. | | 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. | In multi-tenant deployments, Console follows the same tenant boundary as the APIs. The logged-in administrator only sees and manages users and applications in the tenant carried by their authenticated context. You cannot delete the user you are logged in as. The default admin user created during Access Manager installation can be deleted by another administrator, and the installer does not recreate it afterwards — make sure another administrator exists before removing it. You can update its details and password 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/access-manager/am-api-introduction) for programmatic access # What is Lerian Console? Source: https://docs.lerian.studio/en/platform/console/about-lerian-console Manage and operate every Lerian product from a single web interface with unified authentication, navigation, and product administration. The **Lerian Console** is the unified web interface for managing and operating all Lerian products. It provides a single point of access for configuration, monitoring, and administration across the entire Lerian ecosystem. Built as a cross-product workspace, the Console centralizes navigation, authentication, and product management — allowing you to work with multiple Lerian products through one consistent experience. The Console is part of the **Enterprise plan** — it isn't sold separately. See [how Lerian is packaged](/en/products-and-plugins) or [talk to our team](https://lerian.studio/contact) for details. ## Architecture *** The Console is organized into two layers: * **Platform layer** — Shared capabilities that apply across all products: centralized authentication, product catalog, global navigation, language preferences, session policies, and feature flags. * **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 seamlessly 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 | | **DevOps teams** | Install and update the platform, monitor system health, manage deployments | ## Console modules *** Each product has its own module in Lerian Console: Core ledger management — organizations, ledgers, accounts, assets, accounting, and transactions. Report generation and template management for financial and operational data. Design visual workflows, configure provider integrations, run automated processes, and monitor executions through the Flowker module. Configure real-time fraud detection rules, set spending limits, and review the audit trail of every transaction validation decision. # About settings Source: https://docs.lerian.studio/en/platform/console/about-settings Configure your Lerian environment from a single Settings area covering applications, users, products, plugins, and system-wide preferences for operators. The Settings area in Lerian Console provides centralized configuration options for managing your platform environment. Access Settings by clicking the gear icon () in the top navigation bar. ## Settings options *** The Settings area is organized into four main sections: Manage Access Manager applications for machine-to-machine integrations. Create credentials and control which systems can authenticate with the platform. Manage Access Manager user accounts. Create users, assign permission groups, and maintain credentials for your team members. Enable or disable Lerian products in your environment. Configure system-wide preferences such as language settings for the Lerian Console interface. # Changing a User's Password Source: https://docs.lerian.studio/en/platform/console/am-changing-password Update an existing user's password from Lerian Console to maintain account security, rotate credentials, and comply with your access control policies. To change a user's password, follow these steps: From the **Users** page, find the user whose password you want to change. Click the three dots () in the **Actions** column and select **Details**. In the **Edit User** panel, select the **Password** tab. Fill in the following fields: * **New Password** *(required)* – Enter the new password. It must meet the following requirements: * At least 12 characters * One uppercase letter * One lowercase letter * One number * One special character * No repeated characters in sequence (for example, `aa`, `111`, or `**`) * **Confirm Password** *(required)* – Re-enter the new password to confirm. Click **Save**. A confirmation dialog appears. Click **Yes, change password** to confirm. The user's password is updated and they must use the new password for their next login. Password validation is enforced by the Console. If a rule is not met, the password cannot be changed. # Creating an Application Source: https://docs.lerian.studio/en/platform/console/am-creating-application Create an application in Lerian Console to grant API access to external systems, issue client credentials, and control how they integrate with Lerian. To create an application, follow these steps: From the **Applications** page, click the **New Application** button. The **New Application** form opens on the right side of the screen. Fill in the following fields: * **Application Name** *(required)* – Select or type the application name (e.g., `midaz`, `plugin-fees`, `plugin-crm`). * **Description** *(required)* – Add a short description explaining what the application does. Click **Save**. The new application appears in the list with its generated **Client ID** and **Client Secret**. After creating the application, copy the **Client ID** and **Client Secret** immediately. You'll need these credentials to authenticate API requests from your application. # Creating a User Source: https://docs.lerian.studio/en/platform/console/am-creating-user Set up a new user in Lerian Console with the login credentials, group memberships, and permissions they need to access your platform and product modules. To create a user, follow these steps: From the **Users** page, click the **New User** button. The **New User** form opens on the right side of the screen. Fill in the following fields: * **Name** *(required)* – The user's first name. * **Last Name** *(required)* – The user's last name. * **Username** *(required)* – A unique login identifier. Only letters, numbers, hyphens, and underscores are allowed. * **E-mail** *(required)* – The user's email address. * **Password** *(required)* – The user's password. It must meet the following requirements: * At least 12 characters * One uppercase letter * One lowercase letter * One number * One special character * No repeated characters in sequence (for example, `aa`, `111`, or `**`) * **Confirm Password** *(required)* – Re-enter the password to confirm. * **Role** *(required)* – Select one or more permission groups for the user. Click **Save**. The new user appears in the list and can now log in with their credentials. Password validation is enforced by the Console. If a rule is not met, the user cannot be created. # Deleting an application Source: https://docs.lerian.studio/en/platform/console/am-deleting-application Revoke an application's credentials from Console to cut API access for connected external systems, subject to your data retention and compliance policies. Deleting an application **revokes access to all connected services immediately.** Any systems using these credentials will lose access to the platform. The application record may be retained for audit, regulatory, or compliance purposes, subject to your deployment's data retention policies. To delete an application, follow these steps: From the **Applications** page, find the application you want to delete. Click the three dots () in the **Actions** column and select **Delete**. A confirmation dialog appears asking you to confirm the deletion. Click **Yes, delete it** to confirm. The application's credentials are invalidated immediately and the application is removed from active listings, subject to your deployment's data retention and compliance policies. Default applications (such as the Lerian Console application) are protected and cannot be deleted. # Deleting a User Source: https://docs.lerian.studio/en/platform/console/am-deleting-user Remove a user account from the Console to revoke their access to your Lerian platform, subject to your deployment's data retention and compliance policies. The user's access is **revoked immediately.** The user account record may be retained for audit, regulatory, or compliance purposes, subject to your deployment's data retention policies. Confirm your retention policy before proceeding. To delete a user, follow these steps: From the **Users** page, find the user you want to delete. Click the three dots () in the **Actions** column and select **Delete**. A confirmation dialog appears asking you to confirm the deletion. Click **Confirm** to finalize the deletion. The user's access is revoked immediately and the account is removed from active listings, subject to your deployment's data retention and compliance policies. You cannot delete your own user account. This prevents accidentally locking yourself out of the platform. # Editing a User Source: https://docs.lerian.studio/en/platform/console/am-editing-user Update a user's profile details, group memberships, and contact info from Lerian Console while preserving their immutable username and account history. The username cannot be changed after the user is created. To change a username, you must delete the user and create a new one. To edit a user's information, follow these steps: From the **Users** page, find the user you want to edit. Click the three dots () in the **Actions** column and select **Details**. The **Edit User** panel opens on the right side of the screen with the **Personal Information** tab selected. Update the following fields as needed: * **Name** – The user's first name * **Last Name** – The user's last name * **E-mail** – The user's email address * **Role** – The permission group(s) assigned to the user Click **Save** to apply the changes. To change a user's password, use the **Password** tab in the Edit User panel. See [Changing a user's password](/en/platform/console/am-changing-password) for details. # Viewing Application details Source: https://docs.lerian.studio/en/platform/console/am-viewing-application Inspect an application's read-only configuration in Lerian Console to verify credentials, scopes, redirect URIs, and integration details during audits. Application details are read-only. To update an application's configuration, you must delete it and create a new one with the updated information. To view application details, follow these steps: From the **Applications** page, find the application you want to view. Click the three dots () in the **Actions** column and select **Details**. The **Application Details** panel opens on the right side of the screen, displaying: * **Name** – The application identifier * **Description** – The application description * **ClientId** – The unique client identifier for authentication * **ClientSecret** – The secret key for authentication Use the copy button next to the **ClientId** and **ClientSecret** fields to copy credentials to your clipboard. # Running in dev mode Source: https://docs.lerian.studio/en/platform/console/console-dev-mode Run the Lerian Console locally in development mode to iterate on changes before promoting them. If you're actively developing Lerian Console, we recommend running it outside Docker to avoid conflicts and improve speed. ### Steps ```bash Bash theme={null} docker stop console ``` ```bash Bash theme={null} cd product-console ``` ```bash Bash theme={null} npm install ``` The project ships a `.env.development` file that the development server loads automatically. Adjust its variables if your local services (Midaz APIs, Access Manager) run on non-default hosts or ports. ```bash Bash theme={null} npm run dev ``` The development server runs at [http://localhost:8081](http://localhost:8081). The first build might take a minute as pages are compiled. # Managing Applications Source: https://docs.lerian.studio/en/platform/console/managing-applications Manage applications that securely connect external systems and services to the Console APIs using unique machine-to-machine credentials. Applications in Lerian Console represent Access Manager API clients that can authenticate and interact with the platform's services. Each application has unique credentials (Client ID and Client Secret) used for machine-to-machine (M2M) authentication. In multi-tenant deployments, the list is scoped to the tenant of the logged-in administrator. Console does not expose applications from other tenants. ## Accessing the Applications page *** To access the **Applications** section: Click the gear icon () in the top navigation bar. Select **Applications** from the sidebar. The **Applications** page displays all registered applications in a table with the following columns: * **Name** – The application identifier * **ClientId** – The unique client identifier for authentication * **ClientSecret** – The secret key for authentication * **Creation Date** – When the application was created * **Actions** – Available operations for each application Default applications (such as the Lerian Console application) cannot be modified or deleted. These are protected system applications required for platform functionality. ## Available actions *** From the **Applications** page, you can: Register a new API client with authentication credentials. View the details and credentials of an existing application. Remove an application and revoke its access. ## Security considerations *** **Keep your credentials secure.** The Client ID and Client Secret are sensitive information. Store them securely and never expose them in client-side code or public repositories. * Credentials are displayed in the applications table and can be copied using the copy button * Deleting an application immediately revokes access for all services using those credentials * Create separate applications for different services or environments to maintain security isolation * Use the Identity APIs when you need provider links, default-provider configuration, or other application settings that are not exposed in Console # Managing Users Source: https://docs.lerian.studio/en/platform/console/managing-users Manage user accounts, permission groups, and credentials in Lerian Console so each person sees only what their role and organization membership allows. The Users section in Lerian Console allows you to manage Access Manager user accounts, control access permissions, and maintain user credentials. Each user has a unique account with assigned permission groups that determine what actions they can perform in the platform. In multi-tenant deployments, the list is scoped to the tenant of the logged-in administrator. Console does not expose users from other tenants. ## Accessing the Users page *** To access the **Users** section: Click the gear icon () in the top navigation bar. Select **Users** from the sidebar. The **Users** page displays all registered users in a table with the following columns: * **Name** – The user's full name (first and last name) * **E-mail** – The user's email address * **Group** – The permission group assigned to the user * **Actions** – Available operations for each user You cannot delete your own user account. This prevents accidentally locking yourself out of the platform. ## Available actions *** From the **Users** page, you can: Add a new user account with credentials and group assignment. Update user information such as name, email, and group assignment. Reset or change a user's password. Remove a user account from the platform. ## User groups *** Users are assigned to groups that define their permissions within the platform. The available groups determine what resources users can access and what actions they can perform. When creating or editing a user, you can assign one or more groups to define their access level. # Plugins in Midaz Module Source: https://docs.lerian.studio/en/platform/console/mc-console-plugins Overview of the optional plugins that extend the Midaz Module in Lerian Console — Fees Engine, Bank Transfer (TED), Pix, CRM, Smart Templates, and more. Plugins are optional capabilities that add features to the Midaz Module on top of Midaz Core. ## How plugins work *** * **Settings controls availability.** Each plugin must be enabled in [Plugin settings](/en/platform/console/plugin-settings) before its menu item appears in the Midaz Module sidebar. * **Some plugins require a license.** Settings shows the license requirement next to each plugin. * **Disabling preserves data.** Turning a plugin off hides its UI but does not delete configurations you created with it. # Plugin settings Source: https://docs.lerian.studio/en/platform/console/plugin-settings Enable Midaz Module plugins from Console settings and review which capabilities your organization license makes available across environments and modules. The **Plugin settings** page is where you control plugin **enablement** for the Midaz Module. Use it to turn on the optional plugins your organization has access to and to expose their menu items in the Midaz Module sidebar. 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. ## What you see depends on your license *** The list of plugins on this page is **not the same for every organization**. What appears here is driven by your **organization license / entitlements**: * **Available plugins** show up with a working enable toggle. * **Locked or unavailable plugins** may appear visible but cannot be enabled until the required license is in place. * **Plugins your organization is not licensed for may not appear at all.** If you expect a plugin and do not see it, or you see a plugin you cannot enable, the cause is almost always licensing or unmet prerequisites — not a bug in the page. Contact the Lerian team to review your entitlements. ## Plugins *** The Plugins section lists every optional plugin your organization can see, along with its current status and any license or prerequisite requirement. ### Examples of plugins **Status**: Available Manage fee configurations with structured relationships. The Fees Engine allows you to define, test, and apply fee rules to transactions. **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 Complete guide to managing fees through Lerian Console. ## Enabling or disabling a plugin *** From the Midaz Module sidebar, click **Settings** > **Plugins**. Find the plugin in the list. 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. The Products page allows you to enable or disable Lerian products in your environment. This controls which modules are available to users in the Lerian Console. Products can also be enabled directly from the Console entry page, where they appear as cards with an **Enable** button. ## Accessing the Products page *** To access the **Products** section: Click the gear icon () in the top navigation bar. Select **Products** from the sidebar. 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. Provides functionality for managing organizations, ledgers, assets, accounts, portfolios, and transactions. | | **Reporter** | Reporting and analytics module. Translates complexity into clear and actionable narratives for data-driven decision making. | | **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. | | **Matcher** | Reconciliation module. Automates transaction matching between data sources with intelligent, rule-based matching. | ## Enabling or disabling a product *** To change a product's status: From the **Products** page, locate the product you want to enable or disable. Click the toggle switch next to the product. The change takes effect immediately. A confirmation message appears indicating the product has been enabled or disabled. Disabling a product removes access to its features for all users. Users will no longer see the disabled module in the sidebar navigation. Before disabling a product, **ensure that no critical workflows depend on it**. Disabling Midaz, for example, will remove access to all ledger management features. # System Source: https://docs.lerian.studio/en/platform/console/settings-system Configure global preferences that affect the entire Lerian Console experience, including language, regional formats, and other system-wide settings. The System page contains global preferences that affect the entire Lerian Console experience. Configure system-wide settings such as language preferences from this section. ## Accessing the System page *** To access the **System** section: Click the gear icon () in the top navigation bar. Select **System** from the sidebar. The **System** page displays the available system configuration options. ## Available settings *** ### Language Select the display language for the Lerian Console interface. This setting affects all text, labels, and messages displayed in the console. **Languages ​​available:** * English - EN-US * Portuguese - PT-BR ### Theme Select the appearance theme of the Lerian Console interface. This setting affects all text, labels, and messages displayed in the console. **Themes ​​available:** * Light: Uses light backgrounds and dark text for a clean and bright interface. * 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: On the **System** page, locate the suspended menu that you wish to change (**Language** or **Theme**). Select the option you want. Click on **Save** to apply the changes. The interface updates immediately to display the content in the selected option. These alterations only apply to the current user session. Each user can define their preferences. If you accidentally change the language to one you do not understand, the save button is always located on the bottom edge of the page. # Access Manager via Helm Source: https://docs.lerian.studio/en/platform/helm/access-manager/access-manager-helm Helm chart installation and version compatibility reference for deploying Access Manager on Kubernetes, including Auth, Identity, and supporting parts. Access Manager handles authentication, authorization, and identity management across all Lerian products. It includes an Auth service for token management and an Identity service for user/role administration. ## Prerequisites *** * Standalone — can be installed independently of Midaz. * Kubernetes 1.30+ and Helm 3+. * PostgreSQL instance for identity data storage. ## Key configuration *** ```yaml theme={null} auth: image: tag: "2.6.7" identity: image: tag: "2.4.5" ``` ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/plugin-access-manager ``` ## Installation *** ```bash theme={null} helm install access-manager oci://registry-1.docker.io/lerianstudio/plugin-access-manager --version -n access-manager --create-namespace ``` Replace `` with the desired chart version from the table below. ## Version compatibility *** | Chart version | appVersion | Auth | Identity | | :------------ | :--------- | :---- | :------- | | 8.3.0 | 2.6.7 | 2.6.7 | 2.4.5 | | 7.0.0 | 2.6.6 | 2.6.6 | 2.4.5 | | 6.0.0 | 2.3.0 | 2.4.4 | 2.1.1 | | 5.2.2 | 2.3.0 | 2.4.4 | 2.1.1 | | 4.0.0 | 2.0.0 | 2.2.0 | 2.0.0 | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [Access Manager documentation](/en/platform/access-manager/access-manager) * [Helm chart repository](https://github.com/LerianStudio/helm) # Lerian Console via Helm Source: https://docs.lerian.studio/en/platform/helm/console/console-helm Helm chart installation and version compatibility reference for deploying the Lerian Console on Kubernetes, with configuration values and upgrade guidance. Lerian Console is the unified web interface for managing Lerian products. It provides dashboards for ledger operations, account management, transaction monitoring, and plugin configuration through a single UI. ## Prerequisites *** * A running Midaz deployment. Lerian Console connects to Midaz APIs for data. * Access Manager deployed (for authentication). * Kubernetes 1.30+ and Helm 3+. ## Key configuration *** ```yaml theme={null} image: tag: "1.10.0" configmap: MIDAZ_BASE_PATH: "http://midaz-ledger:3002/v1" MIDAZ_TRANSACTION_BASE_PATH: "http://midaz-ledger:3002/v1" ``` ## Runtime environment variables *** The Console reads a set of `NEXT_PUBLIC_*` variables at runtime (injected into the container by its entrypoint), so they can be changed per environment by updating the variable and restarting the Console container — no image rebuild is required. | Variable | Default | Purpose | | :---------------------------------- | :--------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NEXT_PUBLIC_REPORTER_DOCS_URL` | `https://docs.lerian.studio/en/reporter` | URL of the Reporter documentation linked from the Reporter Module UI. Override it to point operators at an internal or localized documentation site. | | `NEXT_PUBLIC_BANK_TRANSFER_ENABLED` | `false` (disabled) | Runtime plugin flag that controls whether the **Bank Transfer** plugin is visible. Set to `true` to enable it for the environment. | | `NEXT_PUBLIC_BC_CORREIOS_ENABLED` | `false` (disabled) | Runtime plugin flag that controls whether the **BC Correios** provider is visible in the Reporter Module. Set to `true` to enable it for the environment. | The plugin flags are **disabled by default**; a plugin only becomes visible when its flag is set to the string `true`. When a flag is off, the plugin's pages are hidden and show a *feature not available* message. ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/product-console-helm ``` ## Installation *** ```bash theme={null} helm install console oci://registry-1.docker.io/lerianstudio/product-console-helm --version -n console --create-namespace ``` Replace `` with the desired chart version from the table below. ## Version compatibility *** | Chart version | Lerian Console | | :------------ | :------------- | | 3.2.0 | 1.10.0 | | 3.0.0 | 1.6.0 | | 2.0.1 | 1.5.0 | | 1.0.0 | 1.0.0 | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [Lerian Console documentation](/en/platform/console/about-lerian-console) * [Helm chart repository](https://github.com/LerianStudio/helm) # Helm at Lerian Source: https://docs.lerian.studio/en/platform/helm/helm-overview Overview of Lerian's Helm charts — the OCI registry, versioning scheme, chart compatibility, and links to product-specific Kubernetes deployment guides. Lerian publishes Helm charts for all of its products and infrastructure components. Every chart is distributed through an OCI-compatible registry and follows a consistent versioning scheme, so you can install, upgrade, and manage any Lerian component with the same workflow. ## Available Helm charts *** Lerian maintains charts for the following products and components: * [**Midaz**](/en/platform/helm/midaz/midaz-helm-overview): Core ledger engine — installation, ingress, observability, dependencies, and upgrade guides. * [**Reporter**](/en/platform/helm/reporter/reporter-helm): Standalone reporting product for generating reports from Midaz data. * [**Access Manager**](/en/platform/helm/access-manager/access-manager-helm): Shared authentication and identity service. * [**Console**](/en/platform/helm/console/console-helm): Unified visual interface for Lerian products. * [**Plugins**](/en/platform/helm/plugins/plugins-helm-overview): Enterprise charts — CRM, Fees Engine, Pix, Pix Indirect (BTG), and Bank Transfer. * [**Infrastructure**](/en/platform/helm/infrastructure/infrastructure-helm): Supporting components — OTEL Collector, Fetcher, Flowker, and Tracer. ## OCI registry *** All Lerian Helm charts are hosted on Docker Hub's OCI registry: ```bash theme={null} oci://registry-1.docker.io/lerianstudio/ ``` For example, to pull the Midaz chart: ```bash theme={null} helm pull oci://registry-1.docker.io/lerianstudio/midaz-helm --version ``` Helm 3.8+ is required for OCI registry support. ## Versioning scheme *** Lerian uses a **prefixed semantic versioning** scheme for Helm charts: ``` {component}-v{MAJOR}.{MINOR}.{PATCH}[-{prerelease}] ``` **Examples:** * `midaz-v5.7.0` — Midaz Helm chart version 5.7.0 * `plugin-crm-v4.0.0` — CRM plugin chart version 4.0.0 * `reporter-v2.0.0` — Reporter chart version 2.0.0 The **Chart version** refers to the Helm chart package version. The **App version** refers to the actual application container image version deployed by the chart. For the full compatibility matrix across all products, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Getting started *** 1. **New to Lerian?** Start with [Installing Midaz](/en/platform/helm/midaz/midaz-installation). 2. **Upgrading?** See [Upgrading Midaz Helm](/en/platform/helm/midaz/midaz-upgrading-overview). 3. **Using plugins?** See [Plugin Helm charts](/en/platform/helm/plugins/plugins-helm-overview). ## Resources *** * [Helm chart repository](https://github.com/LerianStudio/helm) * [Version compatibility](/en/platform/helm/helm-version-compatibility) # Helm chart version compatibility Source: https://docs.lerian.studio/en/platform/helm/helm-version-compatibility Reference table mapping Helm chart versions to application versions for Midaz, plugins, and related Lerian products, so you can plan safe upgrades. This page provides the official version mapping between Helm chart versions and application versions. Use it as a reference before deploying or upgrading your environment. Looking for upgrade instructions? Check the [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrade-guide) guide for step-by-step procedures. ## Source of truth *** The **official and most up-to-date version matrix** is maintained in the [Helm repository README](https://github.com/LerianStudio/helm?tab=readme-ov-file#lerian-studio-helm-charts). This matrix is **automatically updated** whenever applications have a new release, ensuring it always reflects the latest compatibility information. The tables on this page are provided for quick reference, but always check the Helm repository for the most current versions. **Always update the Helm chart when updating applications.** The Helm chart version and application versions are tightly coupled. Updating an application (like Midaz) without updating its corresponding Helm chart can cause deployment failures. **Exception:** Some hotfixes may not require a Helm chart update. Check the release notes for specific guidance. ## Versioning scheme *** Lerian uses a **prefixed semantic versioning** scheme for Helm charts: ``` {component}-v{MAJOR}.{MINOR}.{PATCH}[-{prerelease}] ``` **Examples:** * `midaz-v4.4.8` - Midaz Helm chart version 4.4.8 * `plugin-crm-v3.2.1` - CRM plugin chart version 3.2.1 * `plugin-crm-v3.2.1-rc.1` - CRM plugin, release-candidate prerelease The **Chart version** refers to the Helm chart package version. The **App version** refers to the actual application container image version deployed by the chart. ## Midaz core *** The main Midaz Helm chart deploys core services. Starting from chart v5.x, a unified **Ledger** service is available that combines Onboarding and Transaction functionality. | Chart version | Ledger | CRM | Onboarding | Transaction | | :------------ | :----- | :---- | :--------- | :---------- | | 8.7.0 | 3.8.0 | 3.8.0 | - | - | | 8.4.0 | 3.7.7 | 3.7.6 | - | - | | 5.7.0 | 3.5.3 | 3.5.3 | 3.5.3 | 3.5.3 | | 5.3.0 | 3.5.1 | 3.5.1 | 3.5.1 | 3.5.1 | | 4.4.8 | - | - | 3.4.4 | 3.4.7 | | 4.3.4 | - | - | 3.4.3 | 3.4.6 | | 4.0.0 | - | - | 3.0.0 | 3.0.0 | Starting from chart v5.x, the **Ledger** service is the recommended deployment option, combining Onboarding and Transaction into a single service. The CRM backend is also included in the main Midaz chart (enabled via `crm.enabled: true`). See the [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrade-guide) guide for migration details. For the complete version history, check the [Helm repository tags](https://github.com/LerianStudio/helm/tags). ## Plugins *** Plugins are deployed as **separate Helm charts** with independent versioning. Each plugin requires a valid Enterprise license. ### CRM The standalone plugin-crm chart includes a UI component and remains compatible with both chart v4.x and v5.x deployments. | Chart version | Backend | UI | Midaz Helm chart compatibility | | :------------ | :------ | :---- | :----------------------------- | | 5.0.0 | 3.5.1 | 2.0.0 | v4.x, v5.x | | 4.0.0 | 3.5.1 | 2.0.0 | v4.x, v5.x | | 3.2.1 | 2.0.0 | 2.0.0 | v3.x, v4.x | | 3.1.0 | 2.0.0 | 2.0.0 | v3.x, v4.x | ### Fees Engine | Chart version | Backend | UI | Midaz Helm chart compatibility | | :------------ | :------ | :---- | :----------------------------- | | 7.3.0 | 3.4.0 | - | - | | 7.0.0 | 3.2.1 | 3.0.0 | - | | 4.1.2 | 3.0.8 | 3.0.0 | v3.x, v4.x, v5.x | | 3.5.1 | 3.0.5 | 3.0.0 | v3.x, v4.x, v5.x | | 3.4.7 | 3.0.3 | 3.0.0 | v3.x, v4.x | Fees Engine is only available for Midaz v3 and later. It is not compatible with Midaz v2. ### Pix | Chart version | Pix | Job | Midaz Helm chart compatibility | | :------------ | :------------ | :------------ | :----------------------------- | | 3.0.0 | 1.2.1-beta.11 | 1.2.1-beta.12 | - | | 1.2.6 | 1.2.1-beta.11 | 1.2.1-beta.11 | v3.x, v4.x, v5.x | ## Related products *** These products have their own Helm charts and are not Midaz plugins. ### Reporter Reporter is a standalone product for generating reports from Midaz data. | Chart version | Manager | Worker | | :------------ | :------ | :----- | | 3.1.1 | 2.1.2 | 2.1.2 | | 2.0.0 | 1.1.1 | 1.1.1 | | 1.1.7 | 4.0.0 | 4.0.0 | ### Access Manager (shared service) Access Manager provides authentication and identity services shared across Lerian products. | Chart version | appVersion | Auth | Identity | | :------------ | :--------- | :---- | :------- | | 8.3.0 | 2.6.7 | 2.6.7 | 2.4.5 | | 8.2.0 | - | 2.6.7 | 2.4.5 | | 6.0.0 | 2.3.0 | 2.4.4 | 2.1.1 | | 5.2.2 | - | 2.4.4 | 2.1.1 | | 4.0.0 | - | 2.2.0 | 2.0.0 | ### Product Console Product Console provides the unified visual interface for Lerian products. | Chart version | Console | | :------------ | :------ | | 3.2.0 | 1.10.0 | | 3.1.0 | 1.6.0 | | 2.0.1 | 1.5.0 | | 1.0.0 | 1.0.0 | ### OTEL Collector Lerian OpenTelemetry Collector configured for Lerian's observability stack. | Chart version | OTEL | | :------------ | :------ | | 4.1.0 | 0.142.0 | | 2.2.1 | 2.1.0 | ## Infrastructure dependencies *** The following table lists the platform prerequisites and the versions of the infrastructure subcharts bundled with the latest Midaz Helm chart. Kubernetes and the Helm CLI are minimum required versions; the data-store and broker rows are the bundled subchart versions, not minimums for an external managed service. | Dependency | Version | Notes | | :------------- | :------ | :------------------------------------------------------------------------------ | | Kubernetes | 1.20+ | Minimum; 1.30+ recommended | | Helm CLI | 3.8+ | Minimum; required for OCI registry | | PostgreSQL | 16.3.5 | Bundled subchart version — primary database | | MongoDB | 16.4.0 | Bundled subchart version — document storage | | RabbitMQ | 2.1.11 | Bundled subchart version — message broker | | Valkey | 2.4.7 | Bundled subchart version — cache (Redis fork) | | OTEL Collector | 4.1.0 | Separate `otel-collector-lerian` chart — no longer bundled with the Midaz chart | ## Version compatibility rules *** Follow these guidelines to ensure a stable deployment: * **Upgrade Midaz Core first** — Always upgrade the main Midaz Helm chart before upgrading any plugins. Plugins depend on Midaz Core APIs. * **Check plugin compatibility** — Before upgrading a plugin, verify it's compatible with your current Midaz version using the tables above. * **Test in staging** — Always test new version combinations in a staging environment before deploying to production. * **Review breaking changes** — Check the [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrade-guide) guide for breaking changes between major versions. ## Helm chart repository *** All Lerian Helm charts are available from the OCI registry: ```bash theme={null} # Midaz Core oci://registry-1.docker.io/lerianstudio/midaz-helm # Plugins and other products oci://registry-1.docker.io/lerianstudio/plugin-fees-helm oci://registry-1.docker.io/lerianstudio/plugin-br-pix-direct-jd oci://registry-1.docker.io/lerianstudio/plugin-br-pix-indirect-btg-helm oci://registry-1.docker.io/lerianstudio/plugin-br-bank-transfer-helm oci://registry-1.docker.io/lerianstudio/reporter oci://registry-1.docker.io/lerianstudio/plugin-access-manager oci://registry-1.docker.io/lerianstudio/fetcher-helm oci://registry-1.docker.io/lerianstudio/tracer-helm ``` ### Pix Indirect (BTG) | Chart version | appVersion | | :------------ | :--------- | | 3.5.0 | 1.8.0 | | 3.3.1 | 1.7.5 | | 2.1.1 | 1.5.1 | ### Bank Transfer | Chart version | appVersion | | :------------ | :--------- | | 2.0.0 | 2.4.0 | | 1.4.0 | 2.4.0 | | 1.2.1 | 1.1.0 | ### Fetcher Lerian's internal data-extraction service. Required only when running Reporter or Matcher in managed/multi-tenant mode (`FETCHER_ENABLED=true`); self-hosted deployments connect directly to datasources and don't need it. See [Infrastructure via Helm](/en/platform/helm/infrastructure/infrastructure-helm). | Chart version | appVersion | | :------------ | :--------- | | 3.1.0 | 3.0.2 | | 3.0.0 | 1.3.0 | | 2.0.3 | 1.2.0 | ### Flowker | Chart version | appVersion | | :------------ | :------------ | | 3.1.1 | 1.2.0-beta.82 | | 3.0.0 | 1.0.0-beta.22 | | 1.0.0 | 1.0.0 | ### Tracer | Chart version | appVersion | | :------------ | :--------- | | 2.1.0 | 1.0.0 | | 1.0.0 | 1.0.0 | For source code and detailed chart documentation, visit the [GitHub repository](https://github.com/LerianStudio/helm). # Infrastructure via Helm Source: https://docs.lerian.studio/en/platform/helm/infrastructure/infrastructure-helm Helm charts for deploying Lerian infrastructure components on Kubernetes — OTEL Collector, Fetcher, Flowker, and Tracer — with version compatibility notes. Lerian provides Helm charts for infrastructure components that support the core platform. These charts handle telemetry collection, data synchronization, workflow orchestration, and transaction validation. Each is deployed independently with its own versioning. ## Prerequisites *** * A running Midaz deployment (most infrastructure components connect to Midaz services). * Kubernetes 1.30+ and Helm 3+. ## OTEL Collector Lerian *** OpenTelemetry Collector configured for Lerian's observability stack. Collects, processes, and exports telemetry data from Lerian services. **OCI registry:** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/otel-collector-lerian ``` | Chart version | OTEL | | :------------ | :------ | | 4.1.0 | 0.142.0 | | 2.2.1 | 2.1.0 | ## Fetcher *** Fetcher is Lerian's internal data-extraction service. It's required **only in managed / multi-tenant deployments** (`FETCHER_ENABLED=true`), where products like Reporter and Matcher use it to reach per-tenant data sources — deploy this chart alongside them in that case. **Single-tenant, self-hosted deployments don't require it**: those products connect to their databases directly through datasource configuration. Fetcher runs behind the scenes and is never used as a standalone product. **OCI registry:** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/fetcher-helm ``` | Chart version | appVersion | | :------------ | :--------- | | 3.1.0 | 3.0.2 | | 2.0.3 | 1.2.0 | ## Flowker *** Flowker handles workflow orchestration and event processing within the Lerian platform. **OCI registry:** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/flowker-helm ``` | Chart version | appVersion | | :------------ | :------------ | | 3.1.1 | 1.2.0-beta.82 | | 1.0.0 | 1.0.0 | ## Tracer *** Tracer provides transaction validation, rule enforcement, and spending limit management for the Lerian platform. **OCI registry:** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/tracer-helm ``` | Chart version | appVersion | | :------------ | :--------- | | 2.1.0 | 1.0.0 | | 1.0.0 | 1.0.0 | ## Resources *** For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). * [Helm chart repository](https://github.com/LerianStudio/helm) # Midaz components reference Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-components-reference Configure Helm chart components for Midaz — Ledger and CRM — with deployment, service, and ingress settings. The Midaz Helm chart deploys two service components: **Ledger** (the unified core service, enabled by default) and **CRM** (optional). Each component has its own deployment, service, ingress, and autoscaling configuration. **Quick decision:** Need CRM? → set `crm.enabled: true`. ### Ledger The `ledger` service combines the onboarding and transaction modules in a single deployment. It is the only core service and is enabled by default. | Parameter | Description | Default Value | | :----------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------- | | `ledger.enabled` | Enable or disable the ledger service. | `true` | | `ledger.name` | Service name. | `"ledger"` | | `ledger.replicaCount` | Number of replicas for the ledger service. | `2` | | `ledger.revisionHistoryLimit` | Number of old ReplicaSets to retain for deployment rollback. | `10` | | `ledger.image.repository` | Repository for the ledger service container image. | `"lerianstudio/midaz-ledger"` | | `ledger.image.pullPolicy` | Image pull policy. | `"IfNotPresent"` | | `ledger.image.tag` | Image tag used for deployment. | Chart appVersion (e.g. `"3.8.0"`); empty falls back to Chart.AppVersion | | `ledger.imagePullSecrets` | Secrets for pulling images from a private registry. | `[]` | | `ledger.nameOverride` | Overrides the default generated name by Helm. | `""` | | `ledger.fullnameOverride` | Overrides the full name generated by Helm. | `""` | | `ledger.podAnnotations` | Pod annotations for additional metadata. | `{}` | | `ledger.podSecurityContext` | Security context applied at the pod level. | `{}` | | `ledger.securityContext.*` | Defines security context settings for the container. | See `values.yaml` | | `ledger.pdb.enabled` | Specifies whether PodDisruptionBudget is enabled. | `true` | | `ledger.pdb.minAvailable` | Minimum number of available pods. | `1` | | `ledger.pdb.maxUnavailable` | Maximum number of unavailable pods. | `1` | | `ledger.pdb.annotations` | Annotations for the PodDisruptionBudget. | `{}` | | `ledger.deploymentUpdate.*` | Deployment update strategy. | See `values.yaml` | | `ledger.service.type` | Kubernetes service type. | `"ClusterIP"` | | `ledger.service.port` | Port for the HTTP API. | `3002` | | `ledger.service.annotations` | Annotations for the service. | `{}` | | `ledger.ingress.enabled` | Specifies whether Ingress is enabled. | `false` | | `ledger.ingress.className` | Ingress class name. | `""` | | `ledger.ingress.annotations` | Additional ingress annotations. | `{}` | | `ledger.ingress.hosts` | Configured hosts for Ingress and associated paths. | `[]` | | `ledger.ingress.tls` | TLS configurations for Ingress. | `[]` | | `ledger.resources.*` | CPU/Memory resource requests/limits. | See `values.yaml` | | `ledger.livenessProbe.*` | Liveness probe fields that override the chart defaults: `path: /health`, `initialDelaySeconds: 0`, `periodSeconds: 10`, `timeoutSeconds: 1`, `successThreshold: 1`, `failureThreshold: 3`. | `{}` | | `ledger.readinessProbe.*` | Readiness probe fields that override the chart defaults: `path: /readyz`, `initialDelaySeconds: 0`, `periodSeconds: 10`, `timeoutSeconds: 1`, `successThreshold: 1`, `failureThreshold: 3`. | `{}` | | `ledger.initContainer.timeoutSeconds` | Seconds the init container waits for each database and broker endpoint before the pod fails to start. | `300` | | `ledger.autoscaling.enabled` | Specifies whether autoscaling is enabled. | `true` | | `ledger.autoscaling.minReplicas` | Minimum number of replicas for autoscaling. | `2` | | `ledger.autoscaling.maxReplicas` | Maximum number of replicas for autoscaling. | `9` | | `ledger.autoscaling.targetCPUUtilizationPercentage` | Target CPU utilization percentage for autoscaling. | `80` | | `ledger.autoscaling.targetMemoryUtilizationPercentage` | Target memory utilization percentage for autoscaling. | `80` | | `ledger.nodeSelector` | Node selectors for pod scheduling. | `{}` | | `ledger.tolerations` | Tolerations for pod scheduling. | `{}` | | `ledger.affinity` | Affinity rules for pod scheduling. | `{}` | | `ledger.configmap.*` | Environment variables for the service. | See `values.yaml` | | `ledger.secrets.*` | Secrets for the service. | See `values.yaml` | | `ledger.useExistingSecret` | Use an existing secret instead of creating a new one. | `false` | | `ledger.existingSecretName` | The name of the existing secret to use. | `""` | | `ledger.extraEnvVars` | A list of extra environment variables. | `[]` | | `ledger.serviceAccount.create` | Specifies whether the service account should be created. | `true` | | `ledger.serviceAccount.annotations` | Annotations for the service account. | `{}` | | `ledger.serviceAccount.name` | Service account name. If not defined, it will be generated automatically. | `""` | #### Creating Ledger secret manually If you want to use an existing Kubernetes Secret for the ledger service, you can create it manually: ```bash theme={null} kubectl create secret generic midaz-ledger \ --from-literal=DB_ONBOARDING_PASSWORD='' \ --from-literal=DB_ONBOARDING_REPLICA_PASSWORD='' \ --from-literal=MONGO_ONBOARDING_PASSWORD='' \ --from-literal=DB_TRANSACTION_PASSWORD='' \ --from-literal=DB_TRANSACTION_REPLICA_PASSWORD='' \ --from-literal=MONGO_TRANSACTION_PASSWORD='' \ --from-literal=REDIS_PASSWORD='' \ --from-literal=RABBITMQ_DEFAULT_PASS='' \ --from-literal=RABBITMQ_CONSUMER_PASS='' \ -n midaz ``` The ledger service uses module-specific database credentials (onboarding and transaction) since it combines both modules. Then configure the ledger service to use this existing secret: ```yaml theme={null} ledger: enabled: true useExistingSecret: true existingSecretName: "midaz-ledger" ``` ### CRM (Customer relationship management) The `crm` service provides APIs for managing holder data and their relationships with ledger accounts. Previously available as a separate chart (`plugin-crm`) deployed in the `midaz-plugins` namespace, the CRM is now integrated into the main Midaz Helm chart and deployed in the `midaz` namespace. If you are currently using `plugin-crm` in the `midaz-plugins` namespace, we recommend migrating to this new integrated CRM workload. See the [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrade-guide) guide for migration steps. For more details about CRM functionality, refer to the [CRM Documentation](/en/midaz/crm/crm-overview). **Enabling CRM service:** ```yaml theme={null} crm: enabled: true configmap: MONGO_HOST: "midaz-mongodb" # Use your MongoDB host MONGO_NAME: "crm" MONGO_USER: "midaz" secrets: MONGO_PASSWORD: "lerian" ``` | Parameter | Description | Default Value | | :-------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------- | | `crm.enabled` | Enable or disable the CRM service. | `false` | | `crm.name` | Service name. | `"crm"` | | `crm.replicaCount` | Number of replicas for the CRM service. | `1` | | `crm.revisionHistoryLimit` | Number of old ReplicaSets to retain for deployment rollback. | `10` | | `crm.image.repository` | Repository for the CRM service container image. | `"lerianstudio/midaz-crm"` | | `crm.image.pullPolicy` | Image pull policy. | `"Always"` | | `crm.image.tag` | Image tag used for deployment. | Chart appVersion (e.g. `"3.8.0"`) | | `crm.imagePullSecrets` | Secrets for pulling images from a private registry. | `[]` | | `crm.nameOverride` | Overrides the default generated name by Helm. | `""` | | `crm.fullnameOverride` | Overrides the full name generated by Helm. | `""` | | `crm.podAnnotations` | Pod annotations for additional metadata. | `{}` | | `crm.podSecurityContext` | Security context applied at the pod level. | `{}` | | `crm.securityContext.*` | Defines security context settings for the container. | See `values.yaml` | | `crm.pdb.enabled` | Specifies whether PodDisruptionBudget is enabled. | `true` | | `crm.pdb.minAvailable` | Minimum number of available pods. | `1` | | `crm.pdb.maxUnavailable` | Maximum number of unavailable pods. | `1` | | `crm.pdb.annotations` | Annotations for the PodDisruptionBudget. | `{}` | | `crm.deploymentUpdate.type` | Type of deployment strategy. | `"RollingUpdate"` | | `crm.deploymentUpdate.maxSurge` | Maximum number of pods that can be created over the desired number of pods. | `1` | | `crm.deploymentUpdate.maxUnavailable` | Maximum number of pods that can be unavailable during the update. | `1` | | `crm.service.type` | Kubernetes service type. | `"ClusterIP"` | | `crm.service.port` | Service port. | `4003` | | `crm.service.annotations` | Annotations for the service. | `{}` | | `crm.ingress.enabled` | Specifies whether Ingress is enabled. | `false` | | `crm.ingress.className` | Ingress class name. | `""` | | `crm.ingress.annotations` | Additional ingress annotations. | `{}` | | `crm.ingress.hosts` | Configured hosts for Ingress and associated paths. | `[]` | | `crm.ingress.tls` | TLS configurations for Ingress. | `[]` | | `crm.resources.*` | CPU/Memory resource requests/limits. | See `values.yaml` | | `crm.livenessProbe.*` | Liveness probe fields that override the chart defaults: `path: /health`, `initialDelaySeconds: 10`, `periodSeconds: 5`, `timeoutSeconds: 1`, `successThreshold: 1`, `failureThreshold: 3`. | `{}` | | `crm.readinessProbe.*` | Readiness probe fields that override the chart defaults: `path: /readyz`, `initialDelaySeconds: 10`, `periodSeconds: 5`, `timeoutSeconds: 1`, `successThreshold: 1`, `failureThreshold: 3`. | `{}` | | `crm.autoscaling.enabled` | Specifies whether autoscaling is enabled. | `true` | | `crm.autoscaling.minReplicas` | Minimum number of replicas for autoscaling. | `1` | | `crm.autoscaling.maxReplicas` | Maximum number of replicas for autoscaling. | `3` | | `crm.autoscaling.targetCPUUtilizationPercentage` | Target CPU utilization percentage for autoscaling. | `80` | | `crm.autoscaling.targetMemoryUtilizationPercentage` | Target memory utilization percentage for autoscaling. | `80` | | `crm.nodeSelector` | Node selectors for pod scheduling. | `{}` | | `crm.tolerations` | Tolerations for pod scheduling. | `{}` | | `crm.affinity` | Affinity rules for pod scheduling. | `{}` | | `crm.configmap.*` | Environment variables for the service. | See `values.yaml` | | `crm.secrets.*` | Secrets for the service. | See `values.yaml` | | `crm.useExistingSecret` | Use an existing secret instead of creating a new one. | `false` | | `crm.existingSecretName` | The name of the existing secret to use. | `""` | | `crm.extraEnvVars` | A list of extra environment variables. | `{}` | #### Creating CRM secret manually If you want to use an existing Kubernetes Secret for the CRM service: ```bash theme={null} kubectl create secret generic midaz-crm \ --from-literal=LCRYPTO_HASH_SECRET_KEY='' \ --from-literal=LCRYPTO_ENCRYPT_SECRET_KEY='' \ --from-literal=MONGO_PASSWORD='' \ -n midaz ``` Then configure the CRM service to use this existing secret: ```yaml theme={null} crm: enabled: true useExistingSecret: true existingSecretName: "midaz-crm" ``` # Configuration reference Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-configuration-reference Reference all Ledger service Helm parameters — module-specific databases, external secrets, and deployment flags for Midaz on Kubernetes. ## Ledger service configuration reference *** The Ledger service uses module-specific database configurations: ```yaml values.yaml expandable theme={null} ledger: enabled: true name: "ledger" replicaCount: 2 image: repository: lerianstudio/midaz-ledger tag: "" # Defaults to Chart.AppVersion pullPolicy: IfNotPresent configmap: # App Configuration ENV_NAME: "production" LOG_LEVEL: "debug" SERVER_PORT: "3002" SERVER_ADDRESS: ":3002" # Auth Configuration PLUGIN_AUTH_ENABLED: "false" PLUGIN_AUTH_HOST: "" # Accounting Configuration # Managed via Ledger Settings API — see PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings # Request body: {"accounting": {"validateRoutes": true, "validateAccountType": true}} # PostgreSQL - Onboarding Module DB_ONBOARDING_HOST: "midaz-postgresql-primary.midaz.svc.cluster.local." DB_ONBOARDING_USER: "midaz" DB_ONBOARDING_NAME: "onboarding" DB_ONBOARDING_PORT: "5432" DB_ONBOARDING_SSLMODE: "disable" DB_ONBOARDING_REPLICA_HOST: "midaz-postgresql-replication.midaz.svc.cluster.local." # PostgreSQL - Transaction Module DB_TRANSACTION_HOST: "midaz-postgresql-primary.midaz.svc.cluster.local." DB_TRANSACTION_USER: "midaz" DB_TRANSACTION_NAME: "transaction" DB_TRANSACTION_PORT: "5432" DB_TRANSACTION_SSLMODE: "disable" DB_TRANSACTION_REPLICA_HOST: "midaz-postgresql-replication.midaz.svc.cluster.local." # MongoDB - Onboarding Module MONGO_ONBOARDING_HOST: "midaz-mongodb.midaz.svc.cluster.local." MONGO_ONBOARDING_NAME: "onboarding" MONGO_ONBOARDING_USER: "midaz" MONGO_ONBOARDING_PORT: "27017" # MongoDB - Transaction Module MONGO_TRANSACTION_HOST: "midaz-mongodb.midaz.svc.cluster.local." MONGO_TRANSACTION_NAME: "transaction" MONGO_TRANSACTION_USER: "midaz" MONGO_TRANSACTION_PORT: "27017" # Redis (shared) REDIS_HOST: "midaz-valkey-primary.midaz.svc.cluster.local.:6379" # RabbitMQ (shared) RABBITMQ_HOST: "midaz-rabbitmq.midaz.svc.cluster.local." # RABBITMQ_VHOST: "/" # Database Connection Pools # DB_ONBOARDING_MAX_OPEN_CONNS: "" # DB_ONBOARDING_MAX_IDLE_CONNS: "" # DB_TRANSACTION_MAX_OPEN_CONNS: "" # DB_TRANSACTION_MAX_IDLE_CONNS: "" # Balance Sync Worker BALANCE_SYNC_WORKER_ENABLED: "false" BALANCE_SYNC_MAX_WORKERS: "5" BALANCE_SYNC_BATCH_SIZE: "50" BALANCE_SYNC_FLUSH_TIMEOUT_MS: "500" BALANCE_SYNC_POLL_INTERVAL_MS: "50" # Multi-Tenant MULTI_TENANT_ENABLED: "false" # Point to your deployed https:// Tenant Manager endpoint so MULTI_TENANT_SERVICE_API_KEY # does not travel over cleartext HTTP. Only in local, non-production clusters you can use # in-cluster HTTP with an explicit opt-in: http://tenant-manager..svc.cluster.local:4026 # MULTI_TENANT_URL: "https://tenant-manager." # MULTI_TENANT_ALLOW_INSECURE_HTTP: "true" # Local development only; never enable in production. # MULTI_TENANT_SERVICE_API_KEY: "" # MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC: "" # MULTI_TENANT_CACHE_TTL_SEC: "120" # MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" # MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" # MULTI_TENANT_REDIS_HOST: "" # MULTI_TENANT_REDIS_PORT: "6379" # MULTI_TENANT_REDIS_PASSWORD: "" # MULTI_TENANT_REDIS_TLS: "false" # Multi-Tenant (CRM only) # MULTI_TENANT_TIMEOUT: "30" # MULTI_TENANT_MAX_TENANT_POOLS: "100" # MULTI_TENANT_IDLE_TIMEOUT_SEC: "300" # RabbitMQ Circuit Breaker RABBITMQ_CIRCUIT_BREAKER_CONSECUTIVE_FAILURES: "15" RABBITMQ_CIRCUIT_BREAKER_FAILURE_RATIO: "50" RABBITMQ_CIRCUIT_BREAKER_INTERVAL: "120" RABBITMQ_CIRCUIT_BREAKER_MAX_REQUESTS: "3" RABBITMQ_CIRCUIT_BREAKER_MIN_REQUESTS: "10" RABBITMQ_CIRCUIT_BREAKER_TIMEOUT: "30" RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_INTERVAL: "30" RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_TIMEOUT: "10" # Audit Logging AUDIT_LOG_ENABLED: "false" RABBITMQ_AUDIT_EXCHANGE: "audit.append_log.exchange" RABBITMQ_AUDIT_KEY: "audit.append_log.key" # Transaction Events RABBITMQ_TRANSACTION_EVENTS_ENABLED: "false" RABBITMQ_TRANSACTION_EVENTS_EXCHANGE: "transaction.transaction_events.exchange" # Async Transaction Processing RABBITMQ_TRANSACTION_ASYNC: "false" # RABBITMQ_NUMBERS_OF_WORKERS: "5" # RABBITMQ_NUMBERS_OF_PREFETCH: "10" # Bulk Recorder (requires RABBITMQ_TRANSACTION_ASYNC=true and BULK_RECORDER_ENABLED=true) BULK_RECORDER_ENABLED: "true" # BULK_RECORDER_SIZE: "0" # BULK_RECORDER_FLUSH_TIMEOUT_MS: "" # BULK_RECORDER_MAX_ROWS_PER_INSERT: "" # Pagination MAX_PAGINATION_MONTH_DATE_RANGE: "3" # MAX_PAGINATION_LIMIT: "100" secrets: # Onboarding Module DB_ONBOARDING_PASSWORD: "" DB_ONBOARDING_REPLICA_PASSWORD: "" MONGO_ONBOARDING_PASSWORD: "" # Transaction Module DB_TRANSACTION_PASSWORD: "" DB_TRANSACTION_REPLICA_PASSWORD: "" MONGO_TRANSACTION_PASSWORD: "" # Shared REDIS_PASSWORD: "" RABBITMQ_DEFAULT_PASS: "" RABBITMQ_CONSUMER_PASS: "" ``` ## External secrets support ```yaml values.yaml theme={null} ledger: useExistingSecret: true existingSecretName: ``` ## Deployment flags reference | Flag | Default | Description | | :------------- | :------ | :--------------------------------- | | ledger.enabled | true | Enables the unified Ledger service | ## Environment variables reference Detailed descriptions for variables that aren't self-explanatory from the YAML block above. ### Database connection pools | Variable | Description | Default | | :------------------------------ | :---------------------------------------------------------------- | :--------------------- | | `DB_ONBOARDING_MAX_OPEN_CONNS` | Maximum open connections to the Onboarding PostgreSQL database. | Go default (unlimited) | | `DB_ONBOARDING_MAX_IDLE_CONNS` | Maximum idle connections kept alive for the Onboarding database. | Go default (2) | | `DB_TRANSACTION_MAX_OPEN_CONNS` | Maximum open connections to the Transaction PostgreSQL database. | Go default (unlimited) | | `DB_TRANSACTION_MAX_IDLE_CONNS` | Maximum idle connections kept alive for the Transaction database. | Go default (2) | For production workloads, set explicit pool limits to prevent connection exhaustion. A common starting point: `MAX_OPEN_CONNS=25`, `MAX_IDLE_CONNS=10` per database module. Tune based on your PostgreSQL `max_connections` setting and the number of Midaz replicas. ### RabbitMQ | Variable | Description | Default | | :--------------- | :------------------------------------------------------------------------------------- | :------ | | `RABBITMQ_VHOST` | RabbitMQ virtual host. Useful for isolating Midaz traffic in shared RabbitMQ clusters. | `/` | ### Audit logging When enabled, Midaz publishes detailed operation-level audit logs to a RabbitMQ exchange after each transaction. The audit payload includes the full operation data (amounts, accounts, balances before/after) serialized as JSON. You consume these events by binding your own queue to the audit exchange. | Variable | Description | Default | | :------------------------ | :----------------------------------------- | :-------------------------- | | `AUDIT_LOG_ENABLED` | Enable transaction audit logging. | `false` | | `RABBITMQ_AUDIT_EXCHANGE` | RabbitMQ exchange name for audit messages. | `audit.append_log.exchange` | | `RABBITMQ_AUDIT_KEY` | Routing key for audit messages. | `audit.append_log.key` | ### Transaction events | Variable | Description | Default | | :------------------------------------- | :----------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `RABBITMQ_TRANSACTION_EVENTS_ENABLED` | Enable real-time transaction event publishing. See [Event publisher](/en/midaz/event-publisher) for details. | `false` | | `RABBITMQ_TRANSACTION_EVENTS_EXCHANGE` | RabbitMQ exchange name for transaction events. | `transaction.transaction_events.exchange` | ### Async transaction processing When `RABBITMQ_TRANSACTION_ASYNC` is enabled, Midaz processes transactions asynchronously through RabbitMQ consumers instead of inline during the API request. The bulk recorder further batches database writes for higher throughput. `BULK_RECORDER_*` variables only take effect when **both** `RABBITMQ_TRANSACTION_ASYNC=true` and `BULK_RECORDER_ENABLED=true`. | Variable | Description | Default | | :---------------------------------- | :------------------------------------------------------------------------------------------- | :--------- | | `RABBITMQ_TRANSACTION_ASYNC` | Enables asynchronous transaction processing via RabbitMQ consumers. | `false` | | `RABBITMQ_NUMBERS_OF_WORKERS` | Number of async consumer worker goroutines. | `5` | | `RABBITMQ_NUMBERS_OF_PREFETCH` | RabbitMQ prefetch count per worker — controls how many messages each worker buffers at once. | `10` | | `BULK_RECORDER_ENABLED` | Enables bulk insert mode for operation writes when async processing is active. | `true` | | `BULK_RECORDER_SIZE` | Batch size for bulk inserts. Set to `0` for automatic sizing based on load. | `0` (auto) | | `BULK_RECORDER_FLUSH_TIMEOUT_MS` | Maximum time (in milliseconds) to wait before flushing an incomplete batch. | `100` | | `BULK_RECORDER_MAX_ROWS_PER_INSERT` | Maximum number of rows per INSERT statement during bulk writes. | `1000` | ### Pagination | Variable | Description | Default | | :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | :------ | | `MAX_PAGINATION_MONTH_DATE_RANGE` | Maximum date range (in months) allowed for paginated queries. Limits how far back list endpoints can query. Set to `0` for unlimited range. | `3` | | `MAX_PAGINATION_LIMIT` | Maximum number of items returned per page in paginated API responses. | `100` | The pagination date range limit prevents expensive full-table scans on large datasets. If your use case requires querying historical data beyond 3 months, increase this value or set it to `0` — but monitor query performance accordingly. # Configuring dependencies Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-dependencies Configure Midaz Helm dependencies — Valkey, PostgreSQL, MongoDB, RabbitMQ — using bundled charts or external managed services for production. The Midaz Helm Chart has the following dependencies for the project's default installation. All dependencies are enabled by default. **Production recommendation:** By default, the Midaz chart bundles all dependencies. For production deployments, disable bundled dependencies and connect to managed services (AWS RDS, Amazon MQ, etc.) for better reliability, automated backups, and scaling. ### Valkey * **Version**: 2.4.7 * **Repository**: [Bitnami](https://charts.bitnami.com/bitnami) * **Disable**: `valkey.enabled = false` **Production:** Use managed Redis/Valkey (ElastiCache, Memorystore) for high availability and automatic failover. If you have an existing Valkey or Redis instance, you can disable this dependency and configure Midaz components to use your external instance. ```yaml theme={null} ledger: configmap: REDIS_HOST: "{your-host}:{your-host-port}" secrets: REDIS_PASSWORD: "{your-host-pass}" ``` ### PostgreSQL * **Version**: 16.3.5 * **Repository**: [Bitnami](https://charts.bitnami.com/bitnami) * **Disable**: `postgresql.enabled = false` **Production:** Use managed PostgreSQL (RDS, CloudSQL, Azure Database) with automated backups and point-in-time recovery. If you have an existing PostgreSQL instance, you can disable this dependency and configure Midaz components to use your external PostgreSQL. ```yaml expandable theme={null} ledger: configmap: # Onboarding module database DB_ONBOARDING_HOST: "{your-host}" DB_ONBOARDING_USER: "{your-host-user}" DB_ONBOARDING_PORT: "{your-host-port}" DB_ONBOARDING_REPLICA_HOST: "{your-replication-host}" DB_ONBOARDING_REPLICA_USER: "{your-replication-host-user}" DB_ONBOARDING_REPLICA_PORT: "{your-replication-host-port}" # Transaction module database DB_TRANSACTION_HOST: "{your-host}" DB_TRANSACTION_USER: "{your-host-user}" DB_TRANSACTION_PORT: "{your-host-port}" DB_TRANSACTION_REPLICA_HOST: "{your-replication-host}" DB_TRANSACTION_REPLICA_USER: "{your-replication-host-user}" DB_TRANSACTION_REPLICA_PORT: "{your-replication-host-port}" secrets: DB_ONBOARDING_PASSWORD: "{your-host-pass}" DB_ONBOARDING_REPLICA_PASSWORD: "{your-replication-host-pass}" DB_TRANSACTION_PASSWORD: "{your-host-pass}" DB_TRANSACTION_REPLICA_PASSWORD: "{your-replication-host-pass}" ``` #### External PostgreSQL Bootstrap Job When using an external PostgreSQL (`postgresql.enabled: false`), this chart provides a one-shot bootstrap Job that: * Creates the `onboarding` and `transaction` databases if they do not exist. * Creates the `midaz` role/user if it does not exist and sets its password. * Grants database privileges and `public` schema permissions so `midaz` can create tables. * Waits for connectivity with a 300s timeout. * Is idempotent: if everything already exists, it prints and exits. Configure in `values.yaml`: ```yaml expandable theme={null} postgresql: enabled: false # disable bundled PostgreSQL to use an external one global: externalPostgresDefinitions: enabled: true connection: host: "your-postgres-host" port: "5432" postgresAdminLogin: # Option A: Use an existing Secret (recommended) # Required keys: DB_USER_ADMIN, DB_ADMIN_PASSWORD useExistingSecret: name: "my-postgres-admin-secret" # Option B: Inline credentials (not recommended in production) # username: "postgres" # password: "s3cret" midazCredentials: # Option A: Use an existing Secret (recommended) # Required key: DB_PASSWORD_MIDAZ useExistingSecret: name: "my-midaz-credentials-secret" # Option B: Inline password (not recommended in production) # password: "midaz-password" ``` All secrets must be in the same namespace as the release. The Job has a TTL of 300 seconds after completion. ### MongoDB * **Version**: 16.4.0 * **Repository**: [Bitnami](https://charts.bitnami.com/bitnami) * **Disable**: `mongodb.enabled = false` **Production:** Use managed MongoDB (Atlas, DocumentDB) for automatic scaling, backups, and encryption at rest. If you have an existing MongoDB instance, you can disable this dependency and configure Midaz components to use your external MongoDB. ```yaml theme={null} ledger: configmap: # Onboarding module MONGO_ONBOARDING_HOST: "{your-host}" MONGO_ONBOARDING_NAME: "{your-host-name}" MONGO_ONBOARDING_USER: "{your-host-user}" MONGO_ONBOARDING_PORT: "{your-host-port}" # Transaction module MONGO_TRANSACTION_HOST: "{your-host}" MONGO_TRANSACTION_NAME: "{your-host-name}" MONGO_TRANSACTION_USER: "{your-host-user}" MONGO_TRANSACTION_PORT: "{your-host-port}" secrets: MONGO_ONBOARDING_PASSWORD: "{your-host-pass}" MONGO_TRANSACTION_PASSWORD: "{your-host-pass}" ``` ### RabbitMQ * **Version**: 2.1.11 * **Repository**: [Groundhog2k](https://Groundhog2k.github.io/helm-charts) * **Disable**: `rabbitmq.enabled = false` **Production:** Use managed RabbitMQ (Amazon MQ, CloudAMQP) with HA configuration for message durability and automatic failover. If you're using an external RabbitMQ instance, you **need to load** the required [load\_definitions.json](https://github.com/LerianStudio/helm/blob/main/charts/midaz/files/rabbitmq/load_definitions.json) file. Without these queues, exchanges, and bindings in place, **Midaz won't work as expected**. You can load the definitions in one of two ways: **Automatically** Enable the bootstrap job in your `values.yaml` to automatically apply the RabbitMQ definitions to your external instance: ```yaml theme={null} global: externalRabbitmqDefinitions: enabled: true ``` **Manually** If you prefer to apply the definitions yourself, use RabbitMQ's HTTP API: ```bash theme={null} curl -u {user}:{pass} -X POST -H "Content-Type: application/json" \ -d @load_definitions.json http://{host}:{port}/api/definitions ``` The `load_definitions.json` file is located at: `charts/midaz/files/rabbitmq/load_definitions.json`. #### External RabbitMQ Bootstrap Job To streamline external RabbitMQ setup, this chart provides a one-shot Job that: * Applies the standard definitions file via the HTTP API. * Creates/updates the `transaction` and `consumer` users with custom passwords. * Waits for AMQP connectivity with a 300s timeout. * Is idempotent: if users already exist, it skips and exits. Configure in `values.yaml`: ```yaml expandable theme={null} rabbitmq: enabled: false # disable bundled RabbitMQ to use an external one global: externalRabbitmqDefinitions: enabled: true connection: protocol: "http" # http or https host: "your-rabbitmq-host" port: "15672" # HTTP management port portAmqp: "5672" # AMQP port (for connectivity check) rabbitmqAdminLogin: # Option A: Use an existing Secret (recommended) # Required keys: RABBITMQ_ADMIN_USER, RABBITMQ_ADMIN_PASS useExistingSecret: name: "my-rabbitmq-admin-secret" # Option B: Inline credentials (not recommended in production) # username: "admin" # password: "s3cret" appCredentials: # Option A: Use an existing Secret (recommended) # Required keys: RABBITMQ_DEFAULT_PASS, RABBITMQ_CONSUMER_PASS useExistingSecret: name: "my-rabbitmq-app-credentials" # Option B: Inline passwords (not recommended in production) # transactionPassword: "transaction-pass" # consumerPassword: "consumer-pass" ``` All secrets must be in the same namespace as the release. The Job has a TTL of 300 seconds after completion. Users created: `midaz` (admin), `transaction`, `consumer`. #### RabbitMQ over TLS/SSL If your RabbitMQ server requires TLS/SSL, update the client environment variables to use secure protocols: ```yaml theme={null} ledger: configmap: RABBITMQ_URI: "amqps" # was "amqp" RABBITMQ_PROTOCOL: "https" # was "http" ``` #### Use your own RabbitMQ If you already have a RabbitMQ instance running, you can disable the built-in dependency and point Midaz components to your external setup. The Ledger connects as two users: a producer user and a consumer user. Set the name and the password of both. The chart defaults are `transaction` for the producer and `consumer` for the consumer — the same two users the bootstrap job provisions. ```yaml theme={null} ledger: configmap: RABBITMQ_HOST: "{your-host}" RABBITMQ_DEFAULT_USER: "{your-rabbitmq-producer-user}" RABBITMQ_CONSUMER_USER: "{your-rabbitmq-consumer-user}" RABBITMQ_PORT_HOST: "{your-amqp-port}" RABBITMQ_PORT_AMQP: "{your-management-port}" secrets: RABBITMQ_DEFAULT_PASS: "{your-rabbitmq-producer-password}" RABBITMQ_CONSUMER_PASS: "{your-rabbitmq-consumer-password}" ``` # Midaz via Helm Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-helm-overview Deploy Midaz on Kubernetes with the official Helm chart — installation, configuration, upgrading, migration, and troubleshooting for platform teams. This section covers everything you need to deploy and manage Midaz on Kubernetes using Helm. From initial installation to version upgrades, you'll find guides tailored for both new deployments and existing environments. New to Midaz? Start with [Installing Midaz with Helm](/en/platform/helm/midaz/midaz-installation) for a complete walkthrough of the installation process. ## Why Helm? *** Helm is the recommended way to deploy Midaz on Kubernetes. It provides: * **Simplified deployment**: Install Midaz with a single command. * **Consistent configuration**: Manage all settings through a centralized `values.yaml` file. * **Easy upgrades**: Update Midaz versions with minimal downtime. * **Dependency management**: Automatically handles PostgreSQL, MongoDB, RabbitMQ, Valkey, and other required services. ## Content *** In this section, you will find: * [Installing Midaz with Helm](/en/platform/helm/midaz/midaz-installation): Install and configure Midaz on Kubernetes. * [Configuring ingress](/en/platform/helm/midaz/midaz-ingress): Expose Midaz services outside the cluster with NGINX, AWS ALB, or Traefik. * [Configuring observability](/en/platform/helm/midaz/midaz-observability): Set up Grafana and OpenTelemetry for monitoring. * [Configuring dependencies](/en/platform/helm/midaz/midaz-dependencies): Configure PostgreSQL, MongoDB, RabbitMQ, and Valkey. * [Midaz components reference](/en/platform/helm/midaz/midaz-components-reference): Detailed parameters for the Ledger and CRM services. * [Upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide): Quick start for upgrading Midaz Core and plugins. * [Migration guides](/en/platform/helm/midaz/midaz-migrate-v4-to-v5): Step-by-step migration paths between Helm chart versions. * [Configuration reference](/en/platform/helm/midaz/midaz-configuration-reference): Ledger service configuration, external secrets, and deployment flags. * [Upgrade checklist](/en/platform/helm/midaz/midaz-upgrade-checklist): Best practices and post-upgrade verification steps. * [Rollback and troubleshooting](/en/platform/helm/midaz/midaz-rollback-troubleshooting): Rollback procedures and common issue resolution. * [Version compatibility](/en/platform/helm/helm-version-compatibility): Check the compatibility matrix between Helm chart versions and Midaz components. ## Prerequisites *** Before deploying Midaz with Helm, make sure you have: * [**Kubernetes (v1.30+)**](https://kubernetes.io/releases/download/): A running cluster. * [**Helm 3+**](https://helm.sh/docs/intro/install/): Installed and available (`helm version`). * Access to a container registry with Midaz images. * DNS and TLS certificates for ingress (or cert-manager installed). ## Quick start *** To install Midaz using Helm, run: ```bash theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm --version -n midaz --create-namespace ``` Replace `` with the desired Helm chart version. Check the latest version with: ```bash theme={null} helm show chart oci://registry-1.docker.io/lerianstudio/midaz-helm ``` For the full version history, check the [Helm repository tags](https://github.com/LerianStudio/helm/tags) or the [version compatibility matrix](/en/platform/helm/helm-version-compatibility). We recommend using the **latest stable version** for production deployments to ensure you have the latest features and security patches. For detailed configuration options, see [Installing Midaz with Helm](/en/platform/helm/midaz/midaz-installation). ## Resources *** * [Helm chart repository](https://github.com/LerianStudio/helm) * [Midaz repository](https://github.com/LerianStudio/midaz) * [Midaz architecture overview](/en/midaz/architecture) # Configuring ingress Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-ingress Expose Midaz services through Kubernetes ingress — set hostnames, TLS secrets, and controller annotations for NGINX, ALB, or Traefik. You can expose each Midaz service independently through ingress. The ingress block is identical across Ledger and CRM. Configure it under each service's `.ingress` section in `values.yaml`. To use ingress, you need an [ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) running in your cluster (e.g., **NGINX**, **AWS ALB**, or **Traefik**) and DNS entries pointing to it. You can enable ingress per service in your values.yaml file and configure hostnames, TLS secrets, and any controller-specific annotations. **cert-manager integration:** If you use cert-manager for automatic TLS, add the annotation `cert-manager.io/cluster-issuer: ` and set `tls.secretName` — cert-manager will provision the certificate automatically. The following sections provide configuration examples for the most common ingress controllers. ## NGINX ingress controller *** To use the **NGINX Ingress Controller**, configure the `values.yaml` as follows: ```yaml expandable theme={null} ingress: enabled: true className: "nginx" # The `annotations` field is used to add custom metadata to the Nginx resource. # Annotations are key-value pairs that can be used to attach arbitrary non-identifying metadata to objects. # These annotations can be used by various tools and libraries to augment the behavior of the Nginx resource. # See more https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md annotations: {} hosts: - host: midaz.example.com paths: - path: / pathType: Prefix tls: - secretName: midaz-tls # Ensure this secret exists or is managed by cert-manager hosts: - midaz.example.com ``` Check the ingress-nginx official documentation for a full reference on Nginx annotations. ## AWS ALB (Application load balancer) *** For **AWS ALB Ingress Controller**, configure the `values.yaml` as follows: ```yaml expandable theme={null} ingress: enabled: true className: "alb" annotations: alb.ingress.kubernetes.io/scheme: internal # Use "internet-facing" for public ALB alb.ingress.kubernetes.io/target-type: ip # Use "instance" if targeting EC2 instances alb.ingress.kubernetes.io/group.name: "midaz" # Group ALB resources under this name alb.ingress.kubernetes.io/healthcheck-path: "/healthz" # Health check path alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' # Listen on HTTP and HTTPS hosts: - host: midaz.example.com paths: - path: / pathType: Prefix tls: [] # TLS is managed by the ALB using ACM certificates ``` ## Traefik Ingress controller *** For **Traefik**, configure the `values.yaml` as follows: ```yaml expandable theme={null} ingress: enabled: true className: "traefik" annotations: traefik.ingress.kubernetes.io/router.entrypoints: "web, websecure" # Entrypoints defined in Traefik traefik.ingress.kubernetes.io/router.tls: "true" # Enable TLS for this route hosts: - host: midaz.example.com paths: - path: / pathType: Prefix tls: - secretName: midaz-tls # Ensure this secret exists and contains the TLS certificate hosts: - midaz.example.com ``` ## Multiple services example *** To expose both the Ledger API and CRM with different hostnames: ```yaml theme={null} ledger: ingress: enabled: true className: "nginx" hosts: - host: api.midaz.example.com paths: - path: / pathType: Prefix tls: - secretName: midaz-api-tls hosts: - api.midaz.example.com crm: ingress: enabled: true className: "nginx" hosts: - host: crm.midaz.example.com paths: - path: / pathType: Prefix tls: - secretName: midaz-crm-tls hosts: - crm.midaz.example.com ``` # Install & configure Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-install-configure-overview Deploy Midaz on Kubernetes with Helm — from initial install through fine-tuning ingress, observability, dependencies, and component-level configuration. This section covers installing and configuring Midaz on Kubernetes using Helm — from initial deployment to fine-tuning ingress, observability, dependencies, and component parameters. ## What you'll need *** * **Kubernetes 1.30+** — Running cluster with `kubectl` configured. * **Helm 3+** — Installed locally (`helm version` to verify). * **Registry access** — Credentials for pulling Midaz container images. * **DNS and TLS** — Domain names and certificates for ingress (or cert-manager for automatic provisioning). ## Guides *** * [Installation](/en/platform/helm/midaz/midaz-installation): Install Midaz on Kubernetes with a single Helm command. * [Ingress](/en/platform/helm/midaz/midaz-ingress): Expose Midaz services outside the cluster using NGINX, AWS ALB, or Traefik. * [Observability](/en/platform/helm/midaz/midaz-observability): Set up Grafana and OpenTelemetry for monitoring and tracing. * [Dependencies](/en/platform/helm/midaz/midaz-dependencies): Configure PostgreSQL, MongoDB, RabbitMQ, and Valkey — bundled or external. * [Components reference](/en/platform/helm/midaz/midaz-components-reference): Detailed parameters for the Ledger and CRM services. ## Quick start *** ```bash theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm --version -n midaz --create-namespace ``` For step-by-step instructions, see [Installation](/en/platform/helm/midaz/midaz-installation). # Installing Midaz via Helm Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-installation Install Midaz on Kubernetes with the official Helm chart — deploy the Ledger, databases, and full observability stack in a single command, with checks. **This page is intended for DevOps engineers and infrastructure teams.** It covers Kubernetes deployment configuration using Helm charts. If you're looking for a high-level understanding of Midaz's deployment options, see [SaaS and BYOC](/en/deployment-models). This is the recommended way to deploy Midaz on Kubernetes. The Helm chart installs everything you need in a single command: the core Ledger service, PostgreSQL, MongoDB, Valkey, RabbitMQ, and an optional observability stack (Grafana + OpenTelemetry). **Use the Ledger service for all new installations.** The chart removed the separate Onboarding and Transaction services in v7.0.0. This guide covers Midaz Helm chart v5.x and later. For upgrading from previous versions, see [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrade-guide). ## What gets deployed *** A default `helm install` deploys the following components: * **Ledger** — Unified API service (port 3002) handling onboarding, transactions, and account management. * **PostgreSQL** — Primary + replica for relational data storage (port 5432). * **MongoDB** — Document store for audit logs and metadata (port 27017). * **Valkey** — In-memory cache for session and balance data (port 6379). * **RabbitMQ** — Message broker for async event processing (ports 5672/15672). * **Grafana + OTEL LGTM** — Observability stack with dashboards, logs, traces, and metrics (port 3000, if enabled). ## Prerequisites *** Before deploying Midaz with Helm, make sure you have: * [**Kubernetes (v1.30+)**](https://kubernetes.io/releases/download/) – Running cluster. * [**Helm 3+**](https://helm.sh/docs/intro/install/) – Installed and available (`helm version`). * Access to a container registry with Midaz images. * DNS and TLS certificates for ingress (or cert-manager installed). The source code for this Helm chart is available at: * [midaz-helm](https://github.com/LerianStudio/helm/tree/main/charts/midaz) * [midaz](https://github.com/LerianStudio/midaz) The default installation matches the one provided in the [Midaz quick installation guide](https://github.com/LerianStudio/midaz?tab=readme-ov-file#quick-installation-guide-localhost). Want a deeper understanding of the architecture? Check the [Midaz architecture overview](/en/midaz/architecture). ## Install Midaz via Helm Chart *** To install Midaz using Helm, run the following command: ```bash theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm --version -n midaz --create-namespace ``` Replace `` with the desired Helm chart version. You can check the latest version by running: ```bash theme={null} helm show chart oci://registry-1.docker.io/lerianstudio/midaz-helm ``` For the full version history, check the [Helm repository tags](https://github.com/LerianStudio/helm/tags) or the [version compatibility matrix](/en/platform/helm/helm-version-compatibility). This creates a namespace called `midaz` (if it doesn't already exist) and deploys the chart. The Helm chart is in our [GitHub repository](https://github.com/LerianStudio/helm). You can fork it, customize values, or extend as needed. ## Verify the installation *** After running the install command, verify that all pods are running: ```bash theme={null} kubectl get pods -n midaz kubectl get svc -n midaz ``` **Expected result:** You should see 5-7 pods in `Running` state (Ledger, PostgreSQL primary, PostgreSQL replica, MongoDB, Valkey, RabbitMQ, and optionally Grafana). All services should have `ClusterIP` endpoints assigned. If any pod is in `CrashLoopBackOff` or `Pending`, check the pod logs with `kubectl logs -n midaz`. To confirm the Helm release is tracked: ```bash theme={null} helm list -n midaz ``` ## Next steps *** * [Configure ingress](/en/platform/helm/midaz/midaz-ingress) to expose Midaz services outside the cluster. * [Configure observability](/en/platform/helm/midaz/midaz-observability) with Grafana and OpenTelemetry. * [Configure dependencies](/en/platform/helm/midaz/midaz-dependencies) like PostgreSQL, MongoDB, RabbitMQ, and Valkey. * [Components reference](/en/platform/helm/midaz/midaz-components-reference) for detailed Midaz service parameters. # Migrating from v3.x to v4.x Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-migrate-v3-to-v4 Migrate your Midaz Helm deployment from v3.x to v4.x — handle breaking changes, configuration mappings, and post-upgrade verification. ## Pre-upgrade checklist Backup existing Helm releases: ```bash Shell theme={null} helm get values -n midaz midaz > midaz-v3-backup.yaml ``` **Critical**: Backup RabbitMQ data and definitions before upgrading. Schedule a maintenance window. ## Breaking changes in v4.x ### RabbitMQ dependency change to Groundhog2k The RabbitMQ chart dependency has been replaced from Bitnami to [Groundhog2k](https://Groundhog2k.github.io/helm-charts). This change may lead to **PersistentVolumeClaim (PVC) data loss** when upgrading existing installations because the underlying StatefulSet, volume mounts, and configuration differ from the previous dependency. **Important notes:** * The Groundhog2k chart **requires a valid Erlang cookie**. Set `rabbitmq.authentication.erlangCookie.value` to a 32+ character printable string with no spaces. If missing or empty, RabbitMQ will fail to start. * If you need to preserve existing data, back up and plan a controlled migration of PVCs and definitions before upgrading. **Required configuration:** ```yaml values.yaml theme={null} rabbitmq: authentication: erlangCookie: value: "<32+ printable characters without spaces>" ``` This breaking change only impacts deployments that use the chart's default RabbitMQ (`rabbitmq.enabled: true`). If you run an external or managed RabbitMQ, you are not affected. ### App version bump Midaz has been bumped to **v3.3.1**. Check the [app changelog](https://github.com/LerianStudio/midaz/blob/main/CHANGELOG.md) for the complete list of changes. ## New features in v4.x ### BitnamiSecure images for core data services The default images for core data services now use the BitnamiSecure repositories with the `latest` tag: | Service | Image Source | Tag | | :--------- | :------------ | :----- | | PostgreSQL | BitnamiSecure | latest | | MongoDB | BitnamiSecure | latest | | Valkey | BitnamiSecure | latest | If you require pinning to a specific version, override the tag in `values.yaml`: ```yaml values.yaml theme={null} postgresql: image: tag: "16.2.0" mongodb: image: tag: "7.0.5" valkey: image: tag: "7.2.4" ``` ### Official NGINX image for microfrontends The previous Bitnami NGINX dependency was replaced with an internal template based on the official `nginx` image. If you previously customized the Bitnami-based NGINX configuration, review the new templates under `templates/console/` and adjust your values accordingly. ## Why we changed Bitnami dependencies We moved away from Bitnami dependencies due to policy changes impacting stability and operations. For more context, see: * [bitnami/charts#36215](https://github.com/bitnami/charts/issues/36215) * [bitnami/containers#86191](https://github.com/bitnami/containers/issues/86191) * [bitnami/containers#83267](https://github.com/bitnami/containers/issues/83267) ## Upgrade command ```bash Shell theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm --version 4.0.0 -n midaz ``` ## Rollback procedure ```bash Shell theme={null} # List release history helm history midaz -n midaz # Rollback to previous version helm rollback midaz -n midaz ``` Due to the RabbitMQ dependency change, rolling back may require manual intervention to restore PVCs and data. Ensure you have backups before upgrading. ## Common issues **RabbitMQ fails to start** * Ensure the Erlang cookie is set correctly (32+ printable characters, no spaces). **RabbitMQ PVC data loss** * This is expected due to the dependency change. Export RabbitMQ definitions before upgrading and restore after. **NGINX configuration issues** * Review the new NGINX templates under `templates/console/` and update your overrides. # Migrating from v3.x to v5.x Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-migrate-v3-to-v5 Upgrade your Midaz Helm deployment directly from v3.x to v5.x — address breaking changes from both major versions in a single migration. If you're upgrading directly from v3.x to v5.x, you need to address breaking changes from both versions. ## Pre-upgrade checklist Backup existing Helm releases: ```bash Shell theme={null} helm get values -n midaz midaz > midaz-v3-backup.yaml ``` **Critical**: Backup RabbitMQ data and definitions (v4.x breaking change). **Decision required**: Choose your deployment strategy - Ledger service or legacy Onboarding/Transaction (v5.x breaking change). If migrating to Ledger service, prepare new secrets with module-specific prefixes. Schedule a maintenance window. ## Breaking changes to address ### From v4.x: RabbitMQ dependency change The RabbitMQ chart dependency changed from Bitnami to Groundhog2k. This may lead to **PVC data loss**. Back up RabbitMQ data before upgrading. **Required configuration:** ```yaml values.yaml theme={null} rabbitmq: authentication: erlangCookie: value: "<32+ printable characters without spaces>" ``` ### From v5.x: new Ledger service The unified Ledger service is available and will become mandatory in a future release. Plan your migration strategy. **Choose one of these configurations:** **Option A: Keep legacy services (gradual migration)** ```yaml values.yaml theme={null} ledger: enabled: false onboarding: enabled: true transaction: enabled: true rabbitmq: authentication: erlangCookie: value: "<32+ printable characters>" ``` **Option B: Migrate to Ledger (recommended)** ```yaml values.yaml theme={null} ledger: enabled: true onboarding: enabled: false transaction: enabled: false rabbitmq: authentication: erlangCookie: value: "<32+ printable characters>" ``` If using Option B, create new secrets with module-specific prefixes: * `DB_ONBOARDING_PASSWORD`, `DB_TRANSACTION_PASSWORD` * `MONGO_ONBOARDING_PASSWORD`, `MONGO_TRANSACTION_PASSWORD` ## Upgrade command ```bash Shell theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm --version 5.x.x -n midaz ``` ## What changes from v3.x | Change | Source Version | Impact | | :------------------- | :------------- | :---------------------------------------------- | | RabbitMQ Groundhog2k | v4.x | Requires Erlang cookie, possible PVC data loss | | BitnamiSecure images | v4.x | PostgreSQL, MongoDB, Valkey use hardened images | | Official NGINX | v4.x | Review custom NGINX configs | | Ledger service | v5.x | New unified service (optional but recommended) | | CRM integration | v5.x | Moves from midaz-plugins to midaz namespace | ## Common issues **RabbitMQ fails to start** * Ensure the Erlang cookie is set correctly (32+ printable characters, no spaces). **RabbitMQ PVC data loss** * This is expected due to the v4.x dependency change from Bitnami to Groundhog2k. Export RabbitMQ definitions before upgrading and restore after. **Ledger service fails to start** * Verify that all module-specific environment variables and secrets are configured with the new prefixes (`DB_ONBOARDING_*`, `DB_TRANSACTION_*`, etc.). **Ingress not routing to Ledger** * Ensure `ledger.enabled: true` and `migration.allowAllServices` is not set to `true`. **Missing secrets after enabling Ledger** * Create new secrets with module prefixes: * `DB_ONBOARDING_PASSWORD` instead of `DB_PASSWORD` * `DB_TRANSACTION_PASSWORD` instead of `DB_PASSWORD` * `MONGO_ONBOARDING_PASSWORD` instead of `MONGO_PASSWORD` * `MONGO_TRANSACTION_PASSWORD` instead of `MONGO_PASSWORD` **Console and NGINX overrides no longer apply** * Chart v5.0.0 removed the Console and NGINX components entirely — `templates/console/` no longer exists. Drop any `console.*` or NGINX overrides from your values file; they are inert and will be rejected by the chart schema on newer versions. # Migrating from v4.x to v5.x Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-migrate-v4-to-v5 Migrate your Midaz Helm deployment from v4.x to v5.x — follow the pre-upgrade checklist, run migrations, and validate the new release. ## Pre-upgrade checklist Backup existing Helm releases: ```bash Shell theme={null} helm get values -n midaz midaz > midaz-v4-backup.yaml ``` **Decision required**: Choose your deployment strategy (Ledger service or legacy Onboarding/Transaction). If migrating to Ledger service, prepare new secrets with module-specific prefixes. Schedule a maintenance window. ## Breaking changes in v5.x ### New Ledger service available Starting from version 5.0, the **Ledger service** is available (`ledger.enabled: false` by default). When enabled, this service combines the functionality of both `onboarding` and `transaction` modules into a single deployment. The separate `onboarding` and `transaction` services will become legacy in a future release. The unified Ledger service will become mandatory. We strongly recommend planning your migration to the Ledger service. **Default values:** | Setting | v4.x (before) | v5.x (after) | | :------------------ | :------------ | :------------------------------------------ | | ledger.enabled | N/A | false | | onboarding.enabled | true | true (auto-disabled when ledger is enabled) | | transaction.enabled | true | true (auto-disabled when ledger is enabled) | **Impact when enabling Ledger:** * The `midaz-onboarding` and `midaz-transaction` deployments will be removed. * A new `midaz-ledger` deployment will be created. * Ingresses will automatically redirect to the Ledger service (DNS compatibility maintained). * Environment variables and secrets structure changes (module-specific prefixes). ### App version bump Chart v5.0.0 ships Midaz app version **3.4.8** (`appVersion` in `Chart.yaml`). Later v5.x patches raise it — check the `Chart.yaml` of the exact chart version you target. Check the [app changelog](https://github.com/LerianStudio/midaz/blob/main/CHANGELOG.md) for the complete list of changes. ## Migration options ### Option 1: keep using Onboarding and Transaction (gradual migration) Add the following to your values override to maintain the current behavior: ```yaml values.yaml theme={null} ledger: enabled: false onboarding: enabled: true transaction: enabled: true ``` This allows you to upgrade the chart version without changing your infrastructure. ### Option 2: run all services simultaneously (testing/migration period) Use the hidden `migration.allowAllServices` flag to run all three services during the migration: ```yaml values.yaml theme={null} ledger: enabled: true onboarding: enabled: true transaction: enabled: true migration: allowAllServices: true ``` This mode is intended for testing and migration only. Do not use in production long-term. ### Option 3: migrate to Ledger (recommended) Accept the new architecture and migrate to the unified Ledger service: **Before upgrading**: Ensure your databases are ready (same databases, new environment variable names). **Update secrets**: Create new secrets with module-specific prefixes (see [Configuration reference](/en/platform/helm/midaz/midaz-configuration-reference)). **Upgrade**: Run helm upgrade with the new chart version. **Verify**: Check that the Ledger service is healthy and ingresses are working. ```yaml values.yaml theme={null} ledger: enabled: true onboarding: enabled: false transaction: enabled: false ``` ## New features in v5.x ### Unified Ledger service A new Ledger service that combines `onboarding` and `transaction` modules into a single deployment. **Key characteristics:** * Single HTTP endpoint (port 3000 by default) * Separate database configurations for each module * Shared Redis and RabbitMQ connections * New Balance Sync Worker for background processing **New environment variables:** ```yaml values.yaml theme={null} # Balance Sync Worker BALANCE_SYNC_WORKER_ENABLED: "false" BALANCE_SYNC_MAX_WORKERS: "5" ``` `BALANCE_SYNC_WORKER_ENABLED` and `BALANCE_SYNC_MAX_WORKERS` are still the current names — do not remove them. Later chart versions **add** three more keys: `BALANCE_SYNC_BATCH_SIZE` (default `50`), `BALANCE_SYNC_FLUSH_TIMEOUT_MS` (default `500`), and `BALANCE_SYNC_POLL_INTERVAL_MS` (default `50`). ### Ingress redirection to Ledger When Ledger is enabled, existing ingresses automatically redirect traffic to the Ledger service, maintaining DNS compatibility. | ledger.enabled | migration.allowAllServices | onboarding ingress target | transaction ingress target | | :------------- | :------------------------- | :------------------------ | :------------------------- | | false | false (default) | midaz-onboarding | midaz-transaction | | true | false (default) | midaz-ledger | midaz-ledger | | true | true | midaz-onboarding | midaz-transaction | ### CRM service integration The CRM service is now available as an integrated component, moving from `midaz-plugins` to `midaz` namespace. For more details, refer to the [CRM Documentation](/en/midaz/crm/crm-overview). **Migration from plugin-crm:** Deploy the new CRM in the midaz namespace: ```yaml values.yaml theme={null} crm: enabled: true configmap: MONGO_HOST: "midaz-mongodb" MONGO_NAME: "crm" ``` Migrate your data from the old MongoDB to the new one (if using separate databases). Update your ingress/DNS to point to the new CRM service. Remove the old plugin-crm release from midaz-plugins namespace. ## Upgrade command ```bash Shell theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm --version 5.x.x -n midaz ``` ## Rollback procedure ```bash Shell theme={null} # List release history helm history midaz -n midaz # Rollback to previous version helm rollback midaz -n midaz # Or explicitly disable ledger helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --set ledger.enabled=false \ --set onboarding.enabled=true \ --set transaction.enabled=true \ -n midaz ``` ## Common issues **Ledger service fails to start** * Verify that all module-specific environment variables and secrets are configured with the new prefixes (`DB_ONBOARDING_*`, `DB_TRANSACTION_*`, etc.). **Ingress not routing to Ledger** * Ensure `ledger.enabled: true` and `migration.allowAllServices` is not set to `true`. **Missing secrets after enabling Ledger** * Create new secrets with module prefixes: * `DB_ONBOARDING_PASSWORD` instead of `DB_PASSWORD` * `DB_TRANSACTION_PASSWORD` instead of `DB_PASSWORD` * `MONGO_ONBOARDING_PASSWORD` instead of `MONGO_PASSWORD` * `MONGO_TRANSACTION_PASSWORD` instead of `MONGO_PASSWORD` # Migrating Midaz via Helm Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-migrating-overview Plan migrations between major Midaz Helm chart versions — pick the right upgrade path, review breaking changes, and prepare data and configuration first. This section covers migrating between major versions of the Midaz Helm chart, including breaking changes, migration steps, and configuration mapping. ## Migration paths *** | From | To | Guide | | ---- | ---- | --------------------------------------------------------------------- | | v4.x | v5.x | [Migrate v4.x → v5.x](/en/platform/helm/midaz/midaz-migrate-v4-to-v5) | | v3.x | v4.x | [Migrate v3.x → v4.x](/en/platform/helm/midaz/midaz-migrate-v3-to-v4) | | v3.x | v5.x | [Migrate v3.x → v5.x](/en/platform/helm/midaz/midaz-migrate-v3-to-v5) | **These paths stop at v5.x.** The current chart line is **v8.x**, and the hops beyond v5 carry their own breaking changes — most notably **v7.0.0** (the `onboarding` and `transaction` services were removed; everything runs in `ledger`) and **v8.4.0** (the `otel-collector-lerian` subchart is no longer installed and its schema now rejects legacy keys). Those hops are not yet covered by a page here; read the chart's own upgrade notes at [`charts/midaz/docs/`](https://github.com/LerianStudio/helm/tree/main/charts/midaz/docs) (`UPGRADE-7.0.md`, `UPGRADE-8.x.md`) before going past v5.x, and see [Upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide) for the current upgrade procedure. ## Which path should I choose? *** * **v4.x → v5.x**: Most common. Introduces the unified Ledger service and CRM integration. * **v3.x → v4.x**: RabbitMQ dependency change (Bitnami → Groundhog2k) and BitnamiSecure images. * **v3.x → v5.x**: Direct jump that addresses both v4.x and v5.x breaking changes in a single upgrade. For routine upgrades within the same major version, see [Upgrading Midaz Helm](/en/platform/helm/midaz/midaz-upgrading-overview). # Configuring observability Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-observability Point Midaz telemetry at your OpenTelemetry collector — the env vars the chart injects, the fixed OTLP endpoint, and how to override it. The Midaz chart does **not** deploy an observability stack. It emits OpenTelemetry traces and metrics and expects you to run the collector and the backend. What the chart controls is a single thing: whether the OTEL environment variables are injected into the `ledger` and `crm` deployments. Earlier chart versions bundled a Grafana OTEL LGTM stack. Those templates were removed in chart **v5.0.0**; the `grafana:` key still exists in `values.yaml` but the chart renders no Grafana resources, so setting `grafana.enabled: true` has no effect. ## Injecting the OTEL environment variables *** Injection is controlled by `otel-collector-lerian.enabled`, which defaults to `true`: ```yaml theme={null} otel-collector-lerian: enabled: true ``` The schema for this key is strict — only `enabled` is accepted. Legacy keys (`external`, `extraEnvs`, `exporters`, `opentelemetry-collector`) are rejected at `helm install`/`helm upgrade` time. When enabled, the chart adds these variables to the `ledger` and `crm` containers: | Variable | Value | | :---------------------------- | :-------------------------------- | | `POD_IP` | `status.podIP` (field reference) | | `HOST_IP` | `status.hostIP` (field reference) | | `OTEL_EXPORTER_OTLP_ENDPOINT` | `$(HOST_IP):4317` | | `OTEL_RESOURCE_ATTRIBUTES` | `k8s.pod.ip=$(POD_IP)` | ## What you must provide *** The endpoint is **fixed at `$(HOST_IP):4317`** — the node the pod runs on. Keeping `enabled: true` therefore requires an OTLP collector reachable on port 4317 on every node, typically deployed as a DaemonSet with `hostNetwork` or a host port. Without one, the applications export to a dead endpoint. ## Using a different endpoint *** To send telemetry somewhere else — a central collector Service, a managed backend, your existing observability infrastructure — leave injection off and set the endpoint directly in the application ConfigMap: ```yaml theme={null} otel-collector-lerian: enabled: false ledger: configmap: OTEL_EXPORTER_OTLP_ENDPOINT: "otel-collector.observability.svc.cluster.local:4317" crm: configmap: OTEL_EXPORTER_OTLP_ENDPOINT: "otel-collector.observability.svc.cluster.local:4317" ``` ## Dashboards *** No dashboards ship with the chart. Build them in whatever backend receives the telemetry. For the standalone collector chart, see [Infrastructure Helm charts](/en/platform/helm/infrastructure/infrastructure-helm). # Production values reference Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-production-values Annotated production-ready values.yaml reference for deploying Midaz on Kubernetes with Helm, covering ingress, resources, databases, and observability. This page provides a complete, annotated `values.yaml` reference optimized for production deployments. Copy it, adapt the placeholders to your environment, and deploy. This is a **reference configuration**. You must replace all placeholder values (marked with `{...}`) with your actual infrastructure details before deploying. ## How to use *** 1. Copy the full configuration below into a file called `values-production.yaml` 2. Replace all `{...}` placeholders with your actual values 3. Review each section and adjust resource limits for your expected workload 4. Deploy: ```bash theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version \ -n midaz \ --create-namespace \ -f values-production.yaml ``` ## Full production values *** The `ACCOUNT_TYPE_VALIDATION` and `TRANSACTION_ROUTE_VALIDATION` environment variables have been deprecated and replaced by the **Ledger Settings API**. Use `PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings` to configure accounting validation per ledger. ```yaml expandable theme={null} # ============================================================================= # Midaz Production Values Reference # ============================================================================= # This file configures Midaz for a production Kubernetes environment with: # - Unified Ledger service # - External databases (PostgreSQL, MongoDB, RabbitMQ, Valkey/Redis) # - Ingress with TLS # - Observability enabled # - CRM enabled # - High availability with autoscaling # ============================================================================= # -- Global name overrides nameOverride: "midaz" fullnameOverride: "" namespaceOverride: "midaz" # ============================================================================= # GLOBAL: External database bootstrap jobs # ============================================================================= # These jobs run once on install to create databases, users, and permissions. # Enable them when using external (non-bundled) databases. global: # -- Bootstrap job for external PostgreSQL externalPostgresDefinitions: enabled: true connection: host: "{your-postgres-host}" # e.g., "prod-postgres.example.com" port: "5432" postgresAdminLogin: # Recommended: use an existing Kubernetes Secret useExistingSecret: name: "{your-postgres-admin-secret}" # Must contain DB_USER_ADMIN and DB_ADMIN_PASSWORD keys # Alternative: inline credentials (NOT recommended for production) # username: "postgres" # password: "{your-admin-password}" midazCredentials: useExistingSecret: name: "{your-midaz-credentials-secret}" # Must contain DB_PASSWORD_MIDAZ key # Alternative: inline # password: "{your-midaz-password}" # -- Bootstrap job for external RabbitMQ externalRabbitmqDefinitions: enabled: true connection: protocol: "https" # Use "https" for production host: "{your-rabbitmq-host}" # e.g., "prod-rabbitmq.example.com" port: "15672" # HTTP management port portAmqp: "5672" # AMQP port rabbitmqAdminLogin: useExistingSecret: name: "{your-rabbitmq-admin-secret}" # Must contain RABBITMQ_ADMIN_USER and RABBITMQ_ADMIN_PASS appCredentials: useExistingSecret: name: "{your-rabbitmq-app-credentials}" # Must contain RABBITMQ_DEFAULT_PASS and RABBITMQ_CONSUMER_PASS # ============================================================================= # LEDGER: Unified service (recommended for new installations) # ============================================================================= # The Ledger service combines onboarding and transaction into a single # deployment. This is the recommended approach for all new installations. ledger: enabled: true # -- High availability: start with 3 replicas replicaCount: 3 image: repository: lerianstudio/midaz-ledger pullPolicy: IfNotPresent tag: "" # Defaults to Chart.AppVersion; pin a specific version for production # -- Security context (non-root, read-only filesystem) securityContext: runAsGroup: 1000 runAsUser: 1000 runAsNonRoot: true capabilities: drop: - ALL readOnlyRootFilesystem: true # -- PodDisruptionBudget: ensure at least 2 pods during disruptions pdb: enabled: true minAvailable: 2 maxUnavailable: 1 # -- Rolling update with zero downtime deploymentUpdate: type: RollingUpdate maxSurge: 1 maxUnavailable: 0 service: type: ClusterIP port: 3002 # -- Ingress with TLS ingress: enabled: true className: "{your-ingress-class}" # e.g., "nginx", "alb", "traefik" annotations: # NGINX example: nginx.ingress.kubernetes.io/proxy-body-size: "10m" nginx.ingress.kubernetes.io/proxy-read-timeout: "60" # AWS ALB example (uncomment if using ALB): # alb.ingress.kubernetes.io/scheme: internal # alb.ingress.kubernetes.io/target-type: ip # alb.ingress.kubernetes.io/group.name: "midaz" hosts: - host: "{your-midaz-api-domain}" # e.g., "api.midaz.example.com" paths: - path: / pathType: Prefix tls: - secretName: "{your-tls-secret}" # e.g., "midaz-api-tls" hosts: - "{your-midaz-api-domain}" # -- Resource limits for production # Adjust based on your workload. These are starting recommendations. resources: requests: cpu: "1000m" memory: "512Mi" limits: cpu: "2000m" memory: "2Gi" # -- Autoscaling autoscaling: enabled: true minReplicas: 3 maxReplicas: 10 targetCPUUtilizationPercentage: 70 targetMemoryUtilizationPercentage: 80 # -- Pod anti-affinity: spread across nodes for high availability affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app.kubernetes.io/name operator: In values: - ledger topologyKey: kubernetes.io/hostname # -- External database configuration configmap: ENABLE_TELEMETRY: "true" # Auth Plugin (enable if using access management) PLUGIN_AUTH_ENABLED: "false" PLUGIN_AUTH_HOST: "" # Accounting Configuration # Managed via Ledger Settings API — see PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings # Request body: {"accounting": {"validateRoutes": true, "validateAccountType": true}} # -- PostgreSQL: Onboarding module DB_ONBOARDING_HOST: "{your-postgres-host}" DB_ONBOARDING_USER: "midaz" DB_ONBOARDING_NAME: "onboarding" DB_ONBOARDING_PORT: "5432" DB_ONBOARDING_REPLICA_HOST: "{your-postgres-replica-host}" DB_ONBOARDING_REPLICA_USER: "midaz" DB_ONBOARDING_REPLICA_NAME: "onboarding" DB_ONBOARDING_REPLICA_PORT: "5432" # -- MongoDB: Onboarding module MONGO_ONBOARDING_URI: "mongodb" MONGO_ONBOARDING_HOST: "{your-mongodb-host}" MONGO_ONBOARDING_NAME: "onboarding" MONGO_ONBOARDING_USER: "midaz" MONGO_ONBOARDING_PORT: "27017" # -- PostgreSQL: Transaction module DB_TRANSACTION_HOST: "{your-postgres-host}" DB_TRANSACTION_USER: "midaz" DB_TRANSACTION_NAME: "transaction" DB_TRANSACTION_PORT: "5432" DB_TRANSACTION_REPLICA_HOST: "{your-postgres-replica-host}" DB_TRANSACTION_REPLICA_USER: "midaz" DB_TRANSACTION_REPLICA_NAME: "transaction" DB_TRANSACTION_REPLICA_PORT: "5432" # -- MongoDB: Transaction module MONGO_TRANSACTION_URI: "mongodb" MONGO_TRANSACTION_HOST: "{your-mongodb-host}" MONGO_TRANSACTION_NAME: "transaction" MONGO_TRANSACTION_USER: "midaz" MONGO_TRANSACTION_PORT: "27017" # -- Redis/Valkey REDIS_HOST: "{your-redis-host}:6379" # -- RabbitMQ RABBITMQ_URI: "amqps" # Use "amqps" for TLS, "amqp" without RABBITMQ_HOST: "{your-rabbitmq-host}" RABBITMQ_PORT_HOST: "5672" RABBITMQ_PORT_AMQP: "15672" RABBITMQ_DEFAULT_USER: "transaction" RABBITMQ_CONSUMER_USER: "consumer" RABBITMQ_TRANSACTION_ASYNC: "false" # -- Audit AUDIT_LOG_ENABLED: "true" # Enable audit logging for production # -- Balance Sync Worker BALANCE_SYNC_BATCH_SIZE: "100" BALANCE_SYNC_FLUSH_TIMEOUT_MS: "1000" BALANCE_SYNC_POLL_INTERVAL_MS: "500" # -- Use existing Kubernetes Secrets (recommended) useExistingSecret: true existingSecretName: "midaz-ledger" # Create this secret manually before deploying: # kubectl create secret generic midaz-ledger \ # --from-literal=DB_ONBOARDING_PASSWORD='{password}' \ # --from-literal=DB_ONBOARDING_REPLICA_PASSWORD='{password}' \ # --from-literal=MONGO_ONBOARDING_PASSWORD='{password}' \ # --from-literal=DB_TRANSACTION_PASSWORD='{password}' \ # --from-literal=DB_TRANSACTION_REPLICA_PASSWORD='{password}' \ # --from-literal=MONGO_TRANSACTION_PASSWORD='{password}' \ # --from-literal=REDIS_PASSWORD='{password}' \ # --from-literal=RABBITMQ_DEFAULT_PASS='{password}' \ # --from-literal=RABBITMQ_CONSUMER_PASS='{password}' \ # -n midaz serviceAccount: create: true annotations: {} # ============================================================================= # CRM: Customer Relationship Management # ============================================================================= crm: enabled: true replicaCount: 2 image: repository: lerianstudio/midaz-crm pullPolicy: IfNotPresent tag: "" # Defaults to Chart.AppVersion securityContext: runAsGroup: 1000 runAsUser: 1000 runAsNonRoot: true capabilities: drop: - ALL readOnlyRootFilesystem: true pdb: enabled: true minAvailable: 1 maxUnavailable: 1 service: type: ClusterIP port: 4003 ingress: enabled: true className: "{your-ingress-class}" hosts: - host: "{your-crm-api-domain}" # e.g., "crm.midaz.example.com" paths: - path: / pathType: Prefix tls: - secretName: "{your-crm-tls-secret}" hosts: - "{your-crm-api-domain}" resources: requests: cpu: "200m" memory: "256Mi" limits: cpu: "500m" memory: "512Mi" autoscaling: enabled: true minReplicas: 2 maxReplicas: 5 targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: 80 configmap: ENV_NAME: "production" PLUGIN_AUTH_ENABLED: "false" PLUGIN_AUTH_ADDRESS: "" MONGO_HOST: "{your-mongodb-host}" MONGO_NAME: "crm" MONGO_PORT: "27017" MONGO_USER: "midaz" useExistingSecret: true existingSecretName: "midaz-crm" # Create this secret manually before deploying: # kubectl create secret generic midaz-crm \ # --from-literal=LCRYPTO_HASH_SECRET_KEY='{generate-a-random-64-hex-string}' \ # --from-literal=LCRYPTO_ENCRYPT_SECRET_KEY='{generate-a-random-64-hex-string}' \ # --from-literal=MONGO_PASSWORD='{password}' \ # -n midaz # ============================================================================= # BUNDLED DEPENDENCIES: All disabled (using external databases) # ============================================================================= # In production, use managed database services (AWS RDS, Atlas, etc.) # instead of bundled containers. postgresql: enabled: false mongodb: enabled: false rabbitmq: enabled: false valkey: enabled: false # ============================================================================= # OBSERVABILITY # ============================================================================= # -- Inject OTEL env vars into the ledger and crm deployments. # The schema accepts ONLY `enabled`. The exporter endpoint is fixed at # $(HOST_IP):4317, so this requires an OTLP collector on every node. # To export elsewhere, set enabled: false and put # OTEL_EXPORTER_OTLP_ENDPOINT in ledger.configmap / crm.configmap instead. otel-collector-lerian: enabled: true ``` Do not add `external`, `extraEnvs`, `exporters`, or `opentelemetry-collector` under `otel-collector-lerian`. The chart schema is strict and `helm install`/`helm upgrade` fails with `additional properties … not allowed`. Collector routing (processors, exporters, API keys) is configured in the collector's own chart, not here — see [Configuring observability](/en/platform/helm/midaz/midaz-observability). The chart no longer ships a Grafana stack — those templates were removed in chart v5.0.0. A `grafana:` block in your values file renders nothing. ## Key decisions explained *** ### Why Ledger instead of Onboarding + Transaction? The unified `ledger` service is the recommended approach for all new installations. It combines both modules into a single deployment, reducing operational overhead: * Fewer pods to manage and monitor * Simplified configuration (single configmap/secret) * Single ingress endpoint * Better resource utilization * Will become the only option in future releases ### Why external databases? Production deployments should use managed database services for: * **Automated backups and point-in-time recovery** * **High availability with automatic failover** * **Monitoring and alerting built-in** * **Scaling without application downtime** * **Security patching managed by the provider** Recommended managed services: | Dependency | AWS | GCP | Azure | | :----------- | :--------------------------------- | :------------------ | :---------------------------- | | PostgreSQL | Amazon RDS | Cloud SQL | Azure Database for PostgreSQL | | MongoDB | Amazon DocumentDB or MongoDB Atlas | MongoDB Atlas | Azure Cosmos DB (MongoDB API) | | RabbitMQ | Amazon MQ | Self-managed on GKE | Self-managed on AKS | | Redis/Valkey | Amazon ElastiCache | Memorystore | Azure Cache for Redis | ### Why use Kubernetes Secrets instead of inline passwords? Inline passwords in `values.yaml` are visible in Helm release history and may be committed to version control. Using `useExistingSecret: true` with pre-created Kubernetes Secrets: * Keeps credentials out of Helm values * Allows rotation without Helm upgrade * Integrates with secret management tools (Vault, External Secrets Operator, Sealed Secrets) ### Pod anti-affinity The `podAntiAffinity` rule distributes Ledger pods across different nodes, so a single node failure doesn't bring down the entire service. ## Checklist before deploying *** Create all required secrets in the `midaz` namespace before running `helm install`: * `midaz-ledger` (database and message broker credentials) * `midaz-crm` (encryption keys and MongoDB password) * `otel-api-key` (if using Lerian telemetry) Point your domains to the ingress controller's external IP: * `{your-midaz-api-domain}` → ingress IP * `{your-crm-api-domain}` → ingress IP * `{your-grafana-domain}` → ingress IP (only if you run Grafana independently — this chart renders none) Either use cert-manager for automatic certificate provisioning or manually create TLS secrets for each domain. Ensure your Kubernetes cluster can reach all external databases. Test with: ```bash theme={null} kubectl run test-pg --rm -it --image=postgres:17 -- \ psql -h {your-postgres-host} -U midaz -d onboarding -c "SELECT 1" ``` Adjust CPU and memory limits based on your expected transaction volume. The values in this reference are starting points for moderate workloads. ```bash theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version \ -n midaz \ --create-namespace \ -f values-production.yaml ``` ```bash theme={null} kubectl get pods -n midaz -o wide kubectl get ingress -n midaz helm list -n midaz ``` ## Related resources *** * [Using Helm](/en/platform/helm/midaz/midaz-installation) - Full installation guide with all configuration options * [Upgrading Midaz](/en/platform/helm/midaz/midaz-upgrade-guide) - Step-by-step upgrade procedures * [Troubleshooting](/en/platform/helm/midaz/midaz-troubleshooting) - Diagnosing common deployment issues * [Uninstalling Midaz](/en/platform/helm/midaz/midaz-uninstall) - Cleanup procedures * [Helm chart source](https://github.com/LerianStudio/helm) - Full `values.yaml` with all available parameters # Rollback and troubleshooting Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-rollback-troubleshooting Roll back a failed Midaz Helm upgrade and troubleshoot common post-upgrade issues — pod crashes, schema errors, and broken integrations. ## Rollback procedures *** If something goes wrong after an upgrade, you can rollback to a previous version. ### When to rollback Consider rolling back if you observe: * Pods failing to start or crash-looping * API errors or service unavailability * Data inconsistencies * Performance degradation ### Check release history View the revision history for a release: ```bash theme={null} helm history midaz -n midaz ``` Example output: ``` REVISION STATUS CHART APP VERSION DESCRIPTION 1 superseded midaz-helm-8.5.0 3.7.7 Install complete 2 deployed midaz-helm-8.6.0 3.8.0 Upgrade complete ``` ### Execute rollback Rollback to a specific revision: * **Rollback Midaz to previous revision** ```bash theme={null} helm rollback midaz 1 -n midaz --wait --timeout 10m ``` * **Rollback a plugin (example: Fees)** ```bash theme={null} helm rollback plugin-fees 1 -n midaz-plugins --wait --timeout 10m ``` CRM has no release of its own to roll back — it ships inside the Midaz chart, so rolling back `midaz` rolls CRM back with it. ### Verify rollback * **Verify release version** ```bash theme={null} helm list -n midaz ``` * **Verify pods** ```bash theme={null} kubectl get pods -n midaz ``` * **Check logs** ```bash theme={null} kubectl logs -n midaz deployment/midaz-ledger --tail=50 ``` ```bash theme={null} kubectl logs -n midaz deployment/midaz-onboarding --tail=50 kubectl logs -n midaz deployment/midaz-transaction --tail=50 ``` Rollbacks may not revert database schema changes. If the upgrade included database migrations, you may need to restore from a database backup. ## Troubleshooting *** ### Pods stuck in Pending state **Symptom:** Pods remain in `Pending` status after upgrade. **Solution:** Check for resource constraints: * **Describe pending pod** ```bash theme={null} kubectl describe pod -n midaz ``` * **Check node resources** ```bash theme={null} kubectl top nodes ``` ### Image pull errors **Symptom:** Pods show `ImagePullBackOff` or `ErrImagePull`. **Solution:** Verify image registry access and credentials: * **Check pod events** ```bash theme={null} kubectl describe pod -n midaz | grep -A 10 Events ``` * **Verify image exists** ```bash theme={null} docker pull lerianstudio/midaz-ledger: ``` ### Configuration errors **Symptom:** Pods crash with configuration-related errors in logs. **Solution:** Compare your values with the new defaults: * **View current values** ```bash theme={null} helm get values midaz -n midaz ``` * **View chart defaults** ```bash theme={null} helm show values oci://registry-1.docker.io/lerianstudio/midaz-helm --version 5.7.0 ``` ### Database connection issues **Symptom:** Services fail to connect to PostgreSQL or MongoDB. **Solution:** Verify database connectivity and credentials: * **Check database pods** ```bash theme={null} kubectl get pods -n midaz -l app.kubernetes.io/name=postgresql kubectl get pods -n midaz -l app.kubernetes.io/name=mongodb ``` * **Check service logs** ```bash theme={null} kubectl logs -n midaz deployment/midaz-ledger | grep -i database ``` ```bash theme={null} kubectl logs -n midaz deployment/midaz-onboarding | grep -i database ``` ## General rollback procedure *** If you encounter issues after any upgrade: ```bash Shell theme={null} # List release history helm history midaz -n midaz # Rollback to previous version helm rollback midaz -n midaz # Verify rollback helm list -n midaz kubectl get pods -n midaz ``` ## Related resources *** * [Installing Midaz with Helm](/en/platform/helm/midaz/midaz-installation) - Initial installation guide * [Helm chart version compatibility](/en/platform/helm/helm-version-compatibility) - Version mapping reference * [Helm repository](https://github.com/LerianStudio/helm) - Source code and release notes # Helm troubleshooting Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-troubleshooting Diagnose and resolve common issues when deploying or operating Midaz on Kubernetes — pod failures, ingress errors, and dependency conflicts. This guide helps you diagnose and resolve common issues when deploying or operating Midaz on Kubernetes with Helm. Each section covers a specific symptom, the diagnostic commands to investigate it, and the steps to resolve it. ## General diagnostic commands *** Start with these commands to get a broad picture of your deployment state before diving into specific issues. ```bash theme={null} # List all Helm releases in the midaz namespace helm list -n midaz # Check the status of a specific release helm status midaz -n midaz # List all pods and their current state kubectl get pods -n midaz # Get events for the namespace (useful for spotting recent failures) kubectl get events -n midaz --sort-by='.lastTimestamp' # Describe a specific pod (replace with the actual name) kubectl describe pod -n midaz # Tail logs for a pod kubectl logs -n midaz --tail=100 # Follow logs in real time kubectl logs -n midaz -f ``` *** ## Pods stuck in Pending *** **Symptom:** One or more pods remain in `Pending` state and never start. **Diagnostic commands:** ```bash theme={null} kubectl get pods -n midaz kubectl describe pod -n midaz kubectl get events -n midaz --sort-by='.lastTimestamp' kubectl top nodes ``` **Common causes and solutions:** * **Insufficient CPU or memory on nodes** — The scheduler cannot find a node that satisfies the pod's resource requests. Check the `Events` section of `kubectl describe pod`. Look for messages like `Insufficient cpu` or `Insufficient memory`. Either reduce `resources.requests` in your `values.yaml`, or add more nodes to the cluster. * **PersistentVolumeClaim not bound** — A PVC required by a dependency (PostgreSQL, MongoDB, Valkey) is stuck in `Pending`. ```bash theme={null} kubectl get pvc -n midaz kubectl describe pvc -n midaz ``` Verify that a StorageClass is available and set as the default. See [PVC stuck in Pending](#pvc-stuck-in-pending) below. * **Node selector or affinity mismatch** — The pod requires a specific node label that no node in the cluster has. Check your `values.yaml` for `nodeSelector` or `affinity` settings, and verify that your nodes have the expected labels: ```bash theme={null} kubectl get nodes --show-labels ``` *** ## ImagePullBackOff *** **Symptom:** Pods show `ImagePullBackOff` or `ErrImagePull` status. **Diagnostic commands:** ```bash theme={null} kubectl describe pod -n midaz kubectl get events -n midaz --sort-by='.lastTimestamp' | grep -i image ``` **Common causes and solutions:** * **Wrong image tag** — The specified tag does not exist in the registry. Check the `image.tag` value in your `values.yaml` against the [version compatibility table](/en/platform/helm/helm-version-compatibility). * **Private registry requires authentication** — The cluster cannot pull images without credentials. Create an image pull secret and reference it in your `values.yaml`: ```bash theme={null} kubectl create secret docker-registry regcred \ --docker-server= \ --docker-username= \ --docker-password= \ -n midaz ``` ```yaml theme={null} ledger: imagePullSecrets: - name: regcred ``` * **Missing `imagePullSecrets`** — The secret exists but is not referenced in the component's config. Ensure `imagePullSecrets` is set for all affected components. *** ## CrashLoopBackOff *** **Symptom:** Pods start and immediately crash, restarting repeatedly. **Diagnostic commands:** ```bash theme={null} kubectl get pods -n midaz kubectl logs -n midaz --previous kubectl describe pod -n midaz ``` Use `--previous` to see logs from the last crashed container instance, not the currently restarting one. **Common causes and solutions:** * **Bad or missing environment variables** — A required config key is absent or has an incorrect value. Check the logs for messages like `missing env var`, `invalid config`, or similar. Review the `configmap` section of your `values.yaml`. * **Missing Kubernetes Secret** — The pod references a secret that does not exist. ```bash theme={null} kubectl get secrets -n midaz kubectl describe secret -n midaz ``` If the secret is missing, create it manually or re-run the Helm install. * **Wrong database credentials** — The service cannot authenticate with PostgreSQL, MongoDB, or Redis. Check logs for `authentication failed` or `connection refused`. Verify the `secrets` section in your `values.yaml` and confirm the credentials match those used when the databases were provisioned. * **OOMKilled** — The container exceeded its memory limit and was killed by the kernel. ```bash theme={null} kubectl describe pod -n midaz | grep -A5 "Last State" ``` Look for `OOMKilled` in the `Last State` section. Increase `resources.limits.memory` in your `values.yaml`. See [Pod eviction / OOMKilled](#pod-eviction--oomkilled) below. *** ## Helm install timeout *** **Symptom:** `helm install` or `helm upgrade` fails with a timeout error before the release reaches `deployed` state. **Diagnostic commands:** ```bash theme={null} helm status midaz -n midaz kubectl get pods -n midaz kubectl describe pod -n midaz kubectl get events -n midaz --sort-by='.lastTimestamp' ``` **Common causes and solutions:** * **Slow image pulls** — Large images on a slow connection can exceed the default timeout. Increase the timeout: ```bash theme={null} helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version \ -n midaz \ --create-namespace \ --timeout 15m ``` * **Init containers failing** — An init container (e.g., the database bootstrap job) is hanging or retrying. Check init container logs: ```bash theme={null} kubectl logs -n midaz -c ``` * **Readiness probes failing** — The pod is running but not passing its readiness check, so Helm waits indefinitely. Describe the pod and look at the `Conditions` and `Events` sections. You may need to increase `initialDelaySeconds` in your readiness probe settings, or investigate why the service is not healthy on startup. *** ## Services not reachable *** **Symptom:** Midaz APIs are unreachable from outside the cluster, or services cannot communicate internally. **Diagnostic commands:** ```bash theme={null} kubectl get ingress -n midaz kubectl describe ingress -n midaz kubectl get svc -n midaz kubectl get endpoints -n midaz ``` **Common causes and solutions:** * **Ingress misconfiguration** — The Ingress resource exists but the controller is not picking it up. Verify that `ingress.className` matches the class of your installed ingress controller: ```bash theme={null} kubectl get ingressclass ``` Also check that the ingress controller pod itself is running: ```bash theme={null} kubectl get pods -n ingress-nginx ``` * **DNS not pointing to the load balancer** — The hostname in your Ingress does not resolve to the controller's external IP. Get the external IP and compare with your DNS record: ```bash theme={null} kubectl get svc -n ingress-nginx ``` * **TLS misconfiguration** — A missing or expired TLS secret causes the ingress to fail silently. Verify the secret exists and is not expired: ```bash theme={null} kubectl get secret -n midaz kubectl describe secret -n midaz ``` If using cert-manager, check the Certificate resource status: ```bash theme={null} kubectl get certificate -n midaz kubectl describe certificate -n midaz ``` *** ## PVC stuck in Pending *** **Symptom:** A PersistentVolumeClaim remains in `Pending` state and the dependent pod cannot start. **Diagnostic commands:** ```bash theme={null} kubectl get pvc -n midaz kubectl describe pvc -n midaz kubectl get storageclass ``` **Common causes and solutions:** * **No default StorageClass** — No StorageClass is marked as default in the cluster. ```bash theme={null} kubectl get storageclass ``` If none shows `(default)`, either create a StorageClass or explicitly set one in your `values.yaml` for the affected dependency (e.g., `postgresql.primary.persistence.storageClass`). * **Wrong access mode** — The StorageClass does not support the access mode requested by the PVC (e.g., `ReadWriteMany` on a storage driver that only supports `ReadWriteOnce`). Check the `Events` section of `kubectl describe pvc`. Adjust `accessModes` in your `values.yaml` to match what your StorageClass supports. * **Volume binding mode is `WaitForFirstConsumer`** — Some StorageClasses use delayed binding. The PVC will stay `Pending` until a pod consuming it is scheduled. This is normal behavior; wait for the pod to be scheduled. *** ## Pod eviction / OOMKilled *** **Symptom:** Pods are repeatedly evicted or show `OOMKilled` in their last state. **Diagnostic commands:** ```bash theme={null} kubectl get pods -n midaz kubectl describe pod -n midaz | grep -A10 "Last State" kubectl top pods -n midaz kubectl top nodes ``` **Common causes and solutions:** * **Memory limits set too low** — The container's `resources.limits.memory` is below what the service actually needs under load. Review the current memory usage with `kubectl top pods`, then increase the limit in your `values.yaml`: The chart defaults are `requests: 256Mi / 1500m` and `limits: 512Mi / 2000m`. Your override **replaces** them, so treat the defaults as the sizing baseline and set the memory limit above the default when the container is being OOMKilled: ```yaml theme={null} ledger: resources: requests: memory: "512Mi" cpu: "1500m" limits: memory: "1Gi" cpu: "2000m" ``` * **Node under memory pressure** — The node itself is under pressure and the kubelet is evicting lower-priority pods. Check node conditions: ```bash theme={null} kubectl describe node | grep -A5 Conditions ``` Consider adding nodes or enabling cluster autoscaler. You can also set `PriorityClass` on Midaz pods to protect them from eviction. *** ## RabbitMQ definitions not loaded *** **Symptom:** Midaz services start but transactions fail, queues are missing, or messages are not being processed. Logs may show AMQP connection errors or missing exchanges/queues. **Diagnostic commands:** ```bash theme={null} kubectl get pods -n midaz | grep rabbit kubectl logs -n midaz --tail=100 # Check if the bootstrap job ran kubectl get jobs -n midaz kubectl logs job/midaz-bootstrap-rabbitmq -n midaz ``` The bootstrap Jobs are named `-bootstrap-postgres`, `-bootstrap-mongodb`, and `-bootstrap-rabbitmq`. They set `ttlSecondsAfterFinished: 300`, so they delete themselves five minutes after finishing — collect their logs promptly or the command returns `NotFound`. **Common causes and solutions:** * **External RabbitMQ missing `load_definitions.json`** — When using an external RabbitMQ instance, the required queues, exchanges, and bindings are not present. Enable the bootstrap job in your `values.yaml`: ```yaml theme={null} global: externalRabbitmqDefinitions: enabled: true connection: protocol: "http" host: "your-rabbitmq-host" port: "15672" portAmqp: "5672" ``` Or apply the definitions manually: ```bash theme={null} curl -u {user}:{pass} -X POST -H "Content-Type: application/json" \ -d @load_definitions.json \ http://{host}:{port}/api/definitions ``` The `load_definitions.json` file is at `charts/midaz/files/rabbitmq/load_definitions.json` in the [Helm repository](https://github.com/LerianStudio/helm). * **Bootstrap job failed silently** — The job ran but encountered an error (wrong credentials, network timeout, wrong port). ```bash theme={null} kubectl logs job/midaz-bootstrap-rabbitmq -n midaz ``` Verify the `rabbitmqAdminLogin` credentials and that the management port (default `15672`) is reachable from within the cluster. *** ## Related resources * [Deploy Midaz using Helm](/en/platform/helm/midaz/midaz-installation) — Initial installation guide * [Upgrading Midaz and plugins via Helm](/en/platform/helm/midaz/midaz-upgrade-guide) — Upgrade procedures and rollback * [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrading-overview) — Breaking changes and migration paths between major versions * [Version compatibility](/en/platform/helm/helm-version-compatibility) — Version mapping reference * [Helm repository](https://github.com/LerianStudio/helm) — Source code and release notes # Uninstalling Midaz deployment Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-uninstall Fully remove a Midaz Helm deployment from Kubernetes — Helm-managed resources, persistent volumes, secrets, and the namespace itself. This guide walks you through fully removing a Midaz deployment from Kubernetes. `helm uninstall` removes the Helm-managed resources — and, importantly, **it also deletes the MongoDB PersistentVolumeClaim along with them**. Only the PostgreSQL volumes survive, because they are created from a StatefulSet `volumeClaimTemplate`. This page covers what goes with the release, what stays behind, and how to clean the rest up. Uninstalling Midaz permanently deletes application data. `helm uninstall` alone is enough to destroy the MongoDB volume — back up **before** running it, not after. This operation is irreversible. ## What uninstall deletes and what survives *** | Resource | On `helm uninstall` | | :------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | | Deployments, Services, ConfigMaps, Secrets, ServiceAccounts, RBAC, HPA, PDB, NetworkPolicy | Deleted | | Bootstrap Jobs (`-bootstrap-postgres`, `-bootstrap-mongodb`, `-bootstrap-rabbitmq`) | Deleted (they also self-delete 300s after finishing) | | **PVC `-mongodb`** | **Deleted — MongoDB data is lost** | | PVCs `data--postgresql-primary-0`, `data--postgresql-replication-0` | Survive (StatefulSet volumeClaimTemplates are not garbage-collected) | | RabbitMQ storage | Nothing to delete — RabbitMQ runs on `emptyDir`, so its data is already gone when the pod stops | | Secrets you created yourself (`kubectl create secret`) | Survive | | The namespace | Survives | No chart resource carries a `helm.sh/resource-policy: keep` annotation. Do not rely on Helm to protect any volume. ## Prerequisites *** Before uninstalling, back up your databases — `helm uninstall` destroys the MongoDB volume: ```bash theme={null} kubectl exec -n midaz midaz-mongodb-0 -- mongodump --archive=/tmp/dump.gz --gzip kubectl cp midaz/midaz-mongodb-0:/tmp/dump.gz ./mongo-backup.gz ``` The PostgreSQL volumes survive `helm uninstall`, but the PVC cleanup below destroys them. Dump PostgreSQL too if you intend to run that step: ```bash theme={null} kubectl exec -n midaz midaz-postgresql-primary-0 -- \ pg_dumpall -U postgres > ./postgres-backup.sql ``` Then back up your current Helm values so you can reinstall with the same configuration if needed: ```bash theme={null} helm get values midaz -n midaz > midaz-values-backup.yaml ``` Also back up any plugin releases you run separately (plugins install into their own namespace, `midaz-plugins`): ```bash theme={null} helm get values plugin-fees -n midaz-plugins > plugin-fees-values-backup.yaml helm get values plugin-br-pix-direct-jd -n midaz-plugins > plugin-pix-values-backup.yaml ``` CRM is part of the Midaz chart, not a separate release — there is no `plugin-crm` chart to back up or uninstall. Verify all releases that will be affected: ```bash theme={null} helm list -n midaz helm list -n midaz-plugins ``` *** ## Uninstalling the Helm release *** Run the following command to uninstall the Midaz Helm release: ```bash theme={null} helm uninstall midaz -n midaz ``` This removes all Kubernetes resources created by the Helm chart — Deployments, StatefulSets, Services, Ingresses, ConfigMaps, chart-managed Secrets, ServiceAccounts, RBAC resources, bootstrap Jobs, **and the `midaz-mongodb` PVC**. It does not remove the PostgreSQL StatefulSet PVCs, Secrets you created outside Helm, or the namespace. Verify that all Helm-managed pods have been removed: ```bash theme={null} kubectl get pods -n midaz ``` *** ## Cleaning up persistent resources *** ### PersistentVolumeClaims After uninstall, the PostgreSQL claims are what remains. List them to confirm: ```bash theme={null} kubectl get pvc -n midaz ``` You should see `data-midaz-postgresql-primary-0` and `data-midaz-postgresql-replication-0`. The `midaz-mongodb` claim is already gone — Helm deleted it with the release. Delete all remaining PVCs in the namespace (this destroys the PostgreSQL data): ```bash theme={null} kubectl delete pvc --all -n midaz ``` Or delete specific PVCs by name: ```bash theme={null} kubectl delete pvc -n midaz ``` Deleting these PVCs permanently destroys the PostgreSQL volume data — the ledger's system of record. Make sure database backups are in place before running this command. ### Secrets Secrets created outside the Helm release lifecycle (e.g., `kubectl create secret`) are not removed by `helm uninstall`. List all secrets in the namespace and identify any that are no longer needed: ```bash theme={null} kubectl get secrets -n midaz ``` Delete individual orphaned secrets: ```bash theme={null} kubectl delete secret -n midaz ``` Or delete all secrets in the namespace: ```bash theme={null} kubectl delete secrets --all -n midaz ``` ### ConfigMaps ConfigMaps created manually or by bootstrap jobs may also remain. List them: ```bash theme={null} kubectl get configmaps -n midaz ``` Delete orphaned ConfigMaps: ```bash theme={null} kubectl delete configmap -n midaz ``` *** ## Namespace cleanup *** Once all resources inside the namespace have been removed, delete the namespace itself: ```bash theme={null} kubectl delete namespace midaz ``` Deleting the namespace will forcefully remove any remaining resources inside it. If a resource is stuck in `Terminating` state, you may need to remove its finalizers manually. Verify the namespace is gone: ```bash theme={null} kubectl get namespace midaz ``` *** ## Complete cleanup *** For staging, evaluation, or CI environments where a full teardown is safe, the following script automates the entire process: **Data loss is permanent.** Run this only in environments where you have confirmed backups or where data loss is acceptable (staging, evaluation, CI). Do not run this in production without a full backup and team sign-off. ```bash expandable theme={null} #!/bin/bash set -e NAMESPACE=midaz RELEASE=midaz echo "==> Uninstalling Helm release: $RELEASE" helm uninstall "$RELEASE" -n "$NAMESPACE" || true echo "==> Deleting all PersistentVolumeClaims" kubectl delete pvc --all -n "$NAMESPACE" || true echo "==> Deleting all Secrets" kubectl delete secrets --all -n "$NAMESPACE" || true echo "==> Deleting all ConfigMaps" kubectl delete configmaps --all -n "$NAMESPACE" || true echo "==> Deleting namespace: $NAMESPACE" kubectl delete namespace "$NAMESPACE" || true echo "==> Done. Midaz has been fully removed." ``` Save this as `midaz-cleanup.sh`, make it executable (`chmod +x midaz-cleanup.sh`), and run it with `./midaz-cleanup.sh`. *** ## Production considerations *** In production, a full uninstall requires careful coordination. Follow these steps before running any cleanup commands: 1. **Back up all databases *before* `helm uninstall`.** Export a full snapshot of PostgreSQL and MongoDB — the MongoDB volume is destroyed by the uninstall itself, not by the cleanup steps. 2. **Export critical data.** If any data needs to be migrated or preserved, export it before uninstalling. 3. **Coordinate with your team.** Notify all stakeholders of planned downtime and confirm the maintenance window. 4. **Uninstall plugins first.** Remove plugin releases (Fees, Pix) before uninstalling the core Midaz release. CRM needs no separate uninstall — it ships inside the Midaz chart. 5. **Verify no traffic.** Confirm that no active traffic is hitting the services before proceeding. Uninstall plugins before the core release: ```bash theme={null} # Skips a release that is genuinely absent, and aborts on anything else # (auth, API, timeout) instead of silently leaving it installed. # On Helm 3.13+ this is just: helm uninstall -n midaz-plugins --ignore-not-found for release in plugin-fees plugin-br-pix-direct-jd; do if err=$(helm status "$release" -n midaz-plugins 2>&1 >/dev/null); then helm uninstall "$release" -n midaz-plugins elif printf '%s' "$err" | grep -q 'release: not found'; then echo "Skipping $release — not installed." else echo "Aborting: helm status $release failed: $err" >&2 exit 1 fi done helm uninstall midaz -n midaz ``` Then proceed with the persistent resource cleanup steps described above. *** ## Related resources * [Deploy Midaz using Helm](/en/platform/helm/midaz/midaz-installation) — Installation guide if you need to reinstall * [Upgrading Midaz and plugins via Helm](/en/platform/helm/midaz/midaz-upgrade-guide) — Upgrade and rollback procedures * [Troubleshooting](/en/platform/helm/midaz/midaz-troubleshooting) — Diagnose issues before deciding to uninstall * [Version compatibility](/en/platform/helm/helm-version-compatibility) — Version mapping reference # Midaz Helm upgrade checklist Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-upgrade-checklist Follow recommended best practices and verification steps before, during, and after a Midaz Helm upgrade to ensure safe, predictable production rollouts. ## Production recommendation *** We do not recommend using the Midaz Helm chart's default dependencies (databases, cache, and message broker) in production environments. For production-grade deployments, follow our [best practices](/en/midaz/midaz-production-best-practices) to operate these dependencies with proper security, observability, backups, disaster recovery, and SLOs. ## Best practices *** ### Upgrade order Always upgrade in this order: 1. Infrastructure (if needed) 2. Midaz Core 3. Plugins (one at a time) ### Test first Always test upgrades in a staging environment that mirrors production before applying to production. ### Monitor closely Monitor logs and metrics closely for the first 15-30 minutes after an upgrade. ### Keep backups Maintain backups of your Helm values and database before major upgrades. ## Upgrade checklist summary *** | Step | Command | Verified | | :------------------ | :------------------------------------------------------------------------------ | :------: | | Backup databases | Snapshot PostgreSQL and MongoDB — `helm rollback` cannot undo schema migrations | | | Backup values | `helm get values -n midaz > backup.yaml` | | | Check compatibility | Review [compatibility table](/en/platform/helm/helm-version-compatibility) | | | Upgrade Midaz | `helm upgrade midaz ...` | | | Verify Midaz pods | `kubectl get pods -n midaz` | | | Upgrade plugins | `helm upgrade ...` | | | Verify plugin pods | `kubectl get pods -n midaz` | | | Check logs | `kubectl logs -n midaz deployment/` | | ## Post-upgrade verification *** ### 1. Check pod status ```bash Shell theme={null} kubectl get pods -n midaz ``` All pods should be `Running` and `READY`. ### 2. Verify services ```bash Shell theme={null} kubectl get svc -n midaz ``` ### 3. Check the logs ```bash Shell theme={null} # The ledger service (the only application Deployment by default) kubectl logs -n midaz deployment/midaz-ledger # RabbitMQ kubectl logs -n midaz statefulset/midaz-rabbitmq # CRM, if crm.enabled: true kubectl logs -n midaz deployment/midaz-crm ``` On charts before v7.0.0 you also had `midaz-onboarding` and `midaz-transaction` Deployments. Those services were removed in v7.0.0 — on any current chart those names return `NotFound`. ### 4. Verify ingress routing ```bash Shell theme={null} kubectl get ingress -n midaz kubectl describe ingress midaz-ledger -n midaz ``` # Midaz Helm upgrade guide Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-upgrade-guide Upgrade your Midaz Helm deployment — quick start, the breaking releases between v5 and v8, plugin upgrades, and post-upgrade checks. This guide walks you through upgrading your Midaz Helm deployment to the current chart line, **v8.x**. You'll find a quick start for experienced operators, the breaking releases you must not skip past blindly, plugin upgrades, and post-upgrade checks. Need a refresher on installing Midaz with Helm? Check the [Installing Midaz with Helm](/en/platform/helm/midaz/midaz-installation) guide before starting your upgrade. ## Quick start *** ### 1. Check the prerequisites * **Helm v3.8+** installed and available (`helm version`) — required for OCI registry support. * **Kubernetes v1.20+** cluster running. * **Backup** your databases and your values file. ### 2. Identify your current version ```bash theme={null} helm list -n midaz ``` The `CHART` column shows your chart version (for example `midaz-helm-8.6.0`). ### 3. Run the upgrade command ```bash theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version \ -n midaz \ -f your-values.yaml ``` ### 4. Verify the upgrade ```bash theme={null} helm list -n midaz kubectl get pods -n midaz ``` ## Version compatibility *** | Component | Requirement | | :--------- | :----------------- | | Kubernetes | 1.20+ | | Helm | 3.8+ (OCI support) | | PostgreSQL | 13+ | | MongoDB | 4.4+ | | Valkey | 7.x | The chart bundles PostgreSQL, MongoDB, RabbitMQ, and Valkey as subchart dependencies. Point the chart at your own managed instances by disabling each dependency (`postgresql.enabled: false`, and so on) — see [Production values](/en/platform/helm/midaz/midaz-production-values). ## Breaking releases you must account for *** Do not jump several major versions in one `helm upgrade`. Read the release's upgrade note in the chart repository (`charts/midaz/docs/UPGRADE-*.md`) for every major version between your current chart and your target. | Chart release | What changed | | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **v5.0.0** | Console and NGINX components removed. The bundled Grafana observability templates were removed with them. | | **v7.0.0** | `onboarding` and `transaction` services removed entirely — all functionality consolidated into the single `ledger` service. Template helpers for the old services are gone. | | **v8.4.0** | The `otel-collector-lerian` subchart is no longer installed. The key now only injects OTEL env vars, and its schema accepts **only** `enabled` — legacy keys (`external`, `extraEnvs`, `exporters`, `opentelemetry-collector`) now fail validation at upgrade time. | If you still run a v4.x or v5.x chart, migrate through the paths in [Migration overview](/en/platform/helm/midaz/midaz-migrating-overview) rather than upgrading straight to v8. ## Upgrading Midaz core *** When upgrading Midaz or any plugin, always upgrade the corresponding Helm chart. Updating application versions without upgrading the Helm chart can lead to deployment failures or inconsistent environments. ### 1. Check available versions The charts are distributed as **OCI artifacts only** — there is no Helm repository index to search, so `helm search repo` does not work here. Browse the release tags to discover versions, then inspect a specific one: ```bash theme={null} helm show chart oci://registry-1.docker.io/lerianstudio/midaz-helm --version ``` Or browse the release tags: * Visit [https://github.com/LerianStudio/helm/tags](https://github.com/LerianStudio/helm/tags) * Filter by the `midaz-v` prefix ### 2. Review changes before upgrading Compare your current values with the target chart's defaults: ```bash theme={null} helm show values oci://registry-1.docker.io/lerianstudio/midaz-helm --version > new-defaults.yaml ``` Then render the upgrade without applying it: ```bash theme={null} helm template midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version \ -n midaz \ -f your-values.yaml ``` A schema violation (for example a legacy `otel-collector-lerian` key) fails here rather than mid-upgrade. ### 3. Run the upgrade ```bash theme={null} helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \ --version \ -n midaz \ -f your-values.yaml \ --wait --timeout 10m ``` Always pass your values with `-f`. Without it, Helm reuses nothing from your previous configuration and the release falls back to chart defaults. ### 4. Verify the upgrade * **Check release status** ```bash theme={null} helm list -n midaz ``` * **Verify the pods are running** ```bash theme={null} kubectl get pods -n midaz ``` * **Check pod logs for errors** ```bash theme={null} kubectl logs -n midaz deployment/midaz-ledger --tail=50 ``` If you run CRM (`crm.enabled: true` — it is **off** by default): ```bash theme={null} kubectl logs -n midaz deployment/midaz-crm --tail=50 ``` All pods should show `Running` status and a ready container count. `midaz-ledger` is the only application Deployment the chart creates by default; `midaz-crm` is added when `crm.enabled: true`. `midaz-onboarding` and `midaz-transaction` no longer exist as of chart v7.0.0. ## Upgrading plugins *** Always upgrade Midaz Core **before** upgrading plugins. Plugins depend on Midaz Core APIs. Plugins are separate releases and install into their own namespace, `midaz-plugins`. Check the plugin's own release tags at [https://github.com/LerianStudio/helm/tags](https://github.com/LerianStudio/helm/tags) for the current version. ### CRM CRM is part of the Midaz chart — there is no separate CRM release to upgrade. If you enable it (`crm.enabled: true`), verify its pods after the core upgrade: ```bash theme={null} kubectl get pods -n midaz -l app.kubernetes.io/name=midaz-crm ``` ### Fees Engine ```bash theme={null} helm upgrade plugin-fees oci://registry-1.docker.io/lerianstudio/plugin-fees-helm \ --version \ -n midaz-plugins \ -f plugin-fees-values-backup.yaml ``` ```bash theme={null} kubectl get pods -n midaz-plugins -l app.kubernetes.io/instance=plugin-fees ``` ### Pix ```bash theme={null} helm upgrade plugin-br-pix-direct-jd oci://registry-1.docker.io/lerianstudio/plugin-br-pix-direct-jd \ --version \ -n midaz-plugins \ -f plugin-pix-values-backup.yaml ``` ```bash theme={null} kubectl get pods -n midaz-plugins -l app.kubernetes.io/instance=plugin-br-pix-direct-jd ``` The chart labels every workload with the `app.kubernetes.io/*` label set. A selector like `-l app=midaz-crm` matches nothing. # Upgrading Midaz via Helm Source: https://docs.lerian.studio/en/platform/helm/midaz/midaz-upgrading-overview Plan and execute Midaz Helm upgrades with version-to-version guides, configuration reference, checklists, and rollback procedures for production clusters. This section covers upgrading your Midaz Helm deployment — from routine version upgrades to configuration reference and rollback procedures. ## Guides *** * [Upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide): Quick start for upgrading Midaz Core and plugins to the latest version. * [Configuration reference](/en/platform/helm/midaz/midaz-configuration-reference): Ledger service parameters, external secrets, and deployment flags. * [Upgrade checklist](/en/platform/helm/midaz/midaz-upgrade-checklist): Best practices, pre/post-upgrade verification steps. * [Rollback & troubleshooting](/en/platform/helm/midaz/midaz-rollback-troubleshooting): Rollback procedures and common issue resolution. Looking for major version migrations? See [Migrating Midaz Helm](/en/platform/helm/midaz/midaz-migrating-overview) for step-by-step migration paths between chart versions. # Bank Transfer (TED) via Helm Source: https://docs.lerian.studio/en/platform/helm/plugins/plugin-bank-transfer-helm Helm chart installation and version compatibility reference for deploying the Bank Transfer (TED) plugin on Kubernetes alongside Midaz and Access Manager. The Bank Transfer plugin enables Brazilian bank transfers (TED) through the Lerian platform. It handles outbound and inbound TED transactions with settlement processing and reconciliation. The Bank Transfer plugin requires an Enterprise license. Contact Lerian for access to plugin images and deployment credentials. ## Prerequisites *** * A running Midaz deployment (v3.x or later). * Enterprise license and registry credentials. ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/plugin-br-bank-transfer-helm ``` ## Installation *** ```bash theme={null} helm install plugin-bank-transfer oci://registry-1.docker.io/lerianstudio/plugin-br-bank-transfer-helm --version -n midaz-plugins --create-namespace ``` ## Version compatibility *** | Chart version | appVersion | | :------------ | :--------- | | 1.1.0 | 2.1.0 | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [Bank Transfer documentation](/en/rails/ted/ted-overview) * [Helm chart repository](https://github.com/LerianStudio/helm) # CRM via Helm Source: https://docs.lerian.studio/en/platform/helm/plugins/plugin-crm-helm Helm chart installation and version compatibility reference for deploying the CRM plugin on Kubernetes alongside Midaz and other Lerian products. The CRM plugin provides APIs for managing holder data and their relationships with ledger accounts. It includes both a backend and a UI component. Starting from Midaz chart v5.x, the CRM backend is also available as an integrated workload within the main Midaz chart (enabled via `crm.enabled: true`). The standalone plugin-crm chart remains available for deployments that prefer a separate installation. ## Prerequisites *** * A running Midaz deployment. * MongoDB instance for CRM data storage. ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/plugin-crm ``` ## Installation *** ```bash theme={null} helm install plugin-crm oci://registry-1.docker.io/lerianstudio/plugin-crm --version -n midaz-plugins --create-namespace ``` ## Version compatibility *** | Chart version | Backend | UI | Midaz Helm chart compatibility | | :------------ | :------ | :---- | :----------------------------- | | 4.0.0 | 3.5.1 | 2.0.0 | v4.x, v5.x | | 3.2.1 | 2.0.0 | 2.0.0 | v3.x, v4.x | | 3.1.0 | 2.0.0 | 2.0.0 | v3.x, v4.x | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [CRM documentation](/en/midaz/crm/crm-overview) * [Helm chart repository](https://github.com/LerianStudio/helm) # Fees Engine via Helm Source: https://docs.lerian.studio/en/platform/helm/plugins/plugin-fees-helm Helm chart installation and version compatibility reference for deploying Fees Engine on Kubernetes as part of the Midaz product family and stack. The Fees Engine is part of Midaz — a separately-licensed fee calculation and simulation capability for Midaz transactions. In current Midaz versions it runs embedded in the unified ledger process (see the [Fees Engine overview](/en/midaz/fees/fees-engine-overview)); this chart remains available for stacks that deploy the Fees backend and UI as separate workloads. Fees Engine requires an Enterprise license. Contact Lerian for access to registry images and deployment credentials. Fees Engine is only available for Midaz v3 and later. It is not compatible with Midaz v2. ## Prerequisites *** * A running Midaz deployment (v3.x or later). * Enterprise license and registry credentials. ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/plugin-fees-helm ``` ## Installation *** ```bash theme={null} helm install plugin-fees oci://registry-1.docker.io/lerianstudio/plugin-fees-helm --version -n midaz-plugins --create-namespace ``` ## Version compatibility *** | Chart version | Backend | UI | Midaz Helm chart compatibility | | :------------ | :------ | :---- | :----------------------------- | | 4.1.2 | 3.0.8 | 3.0.0 | v3.x, v4.x, v5.x | | 3.5.1 | 3.0.5 | 3.0.0 | v3.x, v4.x, v5.x | | 3.4.7 | 3.0.3 | 3.0.0 | v3.x, v4.x | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [Fees Engine documentation](/en/midaz/fees/fees-engine-overview) * [Helm chart repository](https://github.com/LerianStudio/helm) # Pix via Helm Source: https://docs.lerian.studio/en/platform/helm/plugins/plugin-pix-helm Helm chart installation and version compatibility reference for deploying the Pix plugin on Kubernetes alongside Midaz and other Lerian products. The Pix plugin enables Brazilian Pix instant payment processing as a direct participant. The Pix plugin requires an Enterprise license. Contact Lerian for access to plugin images and deployment credentials. ## Prerequisites *** * A running Midaz deployment (v3.x or later). * Enterprise license and registry credentials. * Brazilian payment infrastructure access (SPI/BACEN connectivity). ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/plugin-br-pix-direct-jd ``` ## Installation *** ```bash theme={null} helm install plugin-pix oci://registry-1.docker.io/lerianstudio/plugin-br-pix-direct-jd --version -n midaz-plugins --create-namespace ``` ## Version compatibility *** | Chart version | Pix | Job | Midaz Helm chart compatibility | | :------------ | :------------ | :------------ | :----------------------------- | | 1.2.6 | 1.2.1-beta.11 | 1.2.1-beta.11 | v3.x, v4.x, v5.x | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [Pix documentation](/en/rails/pix/pix-overview) * [Helm chart repository](https://github.com/LerianStudio/helm) # Indirect Pix (BTG) via Helm Source: https://docs.lerian.studio/en/platform/helm/plugins/plugin-pix-indirect-helm Helm chart installation and version compatibility reference for deploying the Indirect Pix (BTG) plugin on Kubernetes alongside Midaz and Access Manager. The Pix Indirect plugin enables Brazilian Pix instant payment processing as an indirect participant through BTG Pactual. This is for institutions that connect to the Pix network through a direct participant rather than connecting directly to BACEN. The Pix Indirect plugin requires an Enterprise license. Contact Lerian for access to plugin images and deployment credentials. ## Prerequisites *** * A running Midaz deployment (v3.x or later). * Enterprise license and registry credentials. * BTG Pactual partnership for indirect Pix participation. ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/plugin-br-pix-indirect-btg-helm ``` ## Installation *** ```bash theme={null} helm install plugin-pix-indirect oci://registry-1.docker.io/lerianstudio/plugin-br-pix-indirect-btg-helm --version -n midaz-plugins --create-namespace ``` ## Version compatibility *** | Chart version | appVersion | | :------------ | :--------- | | 2.1.1 | 1.5.1 | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [Pix documentation](/en/rails/pix/pix-overview) * [Helm chart repository](https://github.com/LerianStudio/helm) # Plugins via Helm Source: https://docs.lerian.studio/en/platform/helm/plugins/plugins-helm-overview Overview of Helm charts for Lerian Enterprise plugins — Fees Engine, Bank Transfer, Pix, Indirect Pix, and CRM — with install and version compatibility. Lerian Enterprise plugins are deployed as separate Helm charts with independent versioning. Each plugin requires a valid Enterprise license. Plugins extend Midaz with domain-specific functionality and are installed alongside the core Midaz Helm chart. ## Available plugins *** | Plugin | Chart name | Description | | :----------------------------------------------------------------------- | :-------------------------------- | :------------------------------------------------------------------ | | [CRM](/en/platform/helm/plugins/plugin-crm-helm) | `plugin-crm` | Customer Relationship Management with holder and alias account APIs | | [Fees Engine](/en/platform/helm/plugins/plugin-fees-helm) | `plugin-fees-helm` | Fee calculation and simulation engine | | [Pix](/en/platform/helm/plugins/plugin-pix-helm) | `plugin-br-pix-direct-jd` | Brazilian Pix instant payment (direct participant) | | [Pix Indirect (BTG)](/en/platform/helm/plugins/plugin-pix-indirect-helm) | `plugin-br-pix-indirect-btg-helm` | Brazilian Pix instant payment (indirect participant via BTG) | | [Bank Transfer](/en/platform/helm/plugins/plugin-bank-transfer-helm) | `plugin-br-bank-transfer-helm` | Brazilian bank transfers (TED) | ## OCI registry *** All plugin charts are available from: ```bash theme={null} oci://registry-1.docker.io/lerianstudio/ ``` ## Prerequisites *** * A running Midaz deployment (chart v4.x or v5.x) * A valid Lerian Enterprise license * Helm 3.8+ for OCI registry support ## Resources *** * [Plugin documentation](/en/platform/plugins/what-are-plugins) * [Version compatibility](/en/platform/helm/helm-version-compatibility) * [Helm chart repository](https://github.com/LerianStudio/helm) # Reporter via Helm Source: https://docs.lerian.studio/en/platform/helm/reporter/reporter-helm Helm chart installation and version compatibility reference for deploying the Reporter product on Kubernetes, with configuration values and upgrade notes. Reporter is a standalone reporting product that generates financial reports, regulatory filings, and custom data exports from Midaz ledger data. It runs a manager/worker architecture for parallel report processing. ## Prerequisites *** * A running Midaz deployment with accessible API endpoints. * Kubernetes 1.30+ and Helm 3+. ## Key configuration *** ```yaml theme={null} reporter: manager: image: tag: "1.1.1" worker: image: tag: "1.1.1" ``` ## OCI registry *** ```bash theme={null} oci://registry-1.docker.io/lerianstudio/reporter ``` ## Installation *** ```bash theme={null} helm install reporter oci://registry-1.docker.io/lerianstudio/reporter --version -n reporter --create-namespace ``` Replace `` with the desired chart version from the table below. ## Version compatibility *** | Chart version | Manager | Worker | | :------------ | :------ | :----- | | 2.0.0 | 1.1.1 | 1.1.1 | | 1.1.7 | 4.0.0 | 4.0.0 | For the full cross-product compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). ## Resources *** * [Reporter documentation](/en/reporter/what-is-reporter) * [Helm chart repository](https://github.com/LerianStudio/helm) # Overview Source: https://docs.lerian.studio/en/platform/lifecycle-management Orchestrate the full lifecycle of Lerian applications in customer-owned infrastructure with native Helm Chart and Kubernetes integration. The **Lifecycle Management** is built on the [Distr platform](https://distr.sh/docs/getting-started/what-is-distr/) to orchestrate the full lifecycle of Lerian applications in customer-owned infrastructure. Whether you're running in a private cloud, on-premises, or a hybrid setup, this tool gives your team direct access to our official **Helm Charts**, enabling fast, secure, and self-managed deployments. With native Kubernetes integration and enterprise-grade security, this tool is the foundation for reliable, scalable, and compliant operations in any environment. Lifecycle Management is the operational backbone of the [BYOC deployment model](/en/deployment-models). If you're using the **SaaS** model, Lerian handles deployment and lifecycle operations for you — this tool is not required. ## Why use Lifecycle Management? *** Lifecycle Management is built to support the realities of modern infrastructure: * **Bring Your Own Cluster (BYOC)**: Run Lerian in customer-managed Kubernetes environments. * **Token-protected distribution**: Secure access to Helm repositories and OCI images. * **Operational visibility**: Monitor deployed versions and system health in real time. * **Full autonomy**: Take full control over installations, upgrades, and rollbacks. ### Key features #### Deploy in your infrastructure Install Lerian services in environments you fully control, whether on-premises, in a private cloud, or in a hybrid setup. The plugin is built to meet strict compliance, data protection, and security standards. #### Integrated with Kubernetes Our applications are packaged as OCI images and Helm Charts, so you can use standard Kubernetes tools to deploy, configure, and operate your services. #### Secure by default All artifact access is token-protected. Only authenticated users can retrieve Helm Charts and container images, ensuring deployment integrity. #### Visibility without compromise Our support team can remotely monitor your deployment without infringing on your autonomy. We track: * Deployed versions. * Health and availability signals. * Compatibility with supported releases. ### Total lifecycle control Manage your entire deployment workflow using the dashboard or native Kubernetes tools: * Install new environments. * Apply upgrades and patches. * Roll back to a previous version. * Pin a version as needed. ### Standardized, zero-friction updates Our update pipeline ensures you can adopt the latest versions quickly, with minimal disruption to your production environment. ## Accessing the Lifecycle dashboard *** The Lifecycle dashboard is available to **Enterprise BYOC** customers only. Community users manage deployments directly using Helm Charts and the documentation. If you’re on the SaaS model, Lerian handles deployment for you. Before you can install Lerian applications, you’ll need access to the Lifecycle dashboard. This is where you’ll create deployments, configure agents, and manage your environments. To get started: Send a valid **company email** to the **Implementation Team,** or via email to [suporte@lerian.studio](mailto:suporte@lerian.studio). We'll register your email and trigger an invite from the platform. Check your inbox and follow the invitation link. Create your password to activate your account. Log in at [https://lifecycle.lerian.studio/](https://lifecycle.lerian.studio/) and begin managing your deployments. These steps give you access to the dashboard. The actual plugin installation starts when you create a deployment and install the agent in your Kubernetes cluster. ## Installation guide *** Once you have dashboard access, follow these steps to install and configure the Lerian product you want to deploy. ### 1. Access the Lifecycle Management dashboard Go to [https://lifecycle.lerian.studio/login](https://lifecycle.lerian.studio/login) and log in with your credentials. ### 2. Create a new deployment In the navigation bar, click Deployments. * If you don't have any deployments yet, the **Install a new Agent** window appears automatically. * Otherwise, click **+ New Agent.** Select **Kubernetes via Helm Chart**. Fill in the **Agent Name** and the **Kubernetes Namespace**. Make sure **Enable cluster-scope permissions** is not enabled. Click Continue . ### 3. Install the agent in your cluster After selecting the agent type, the following screen shows a custom `kubectl` command for your deployment. #### 3.1. Before running the command: Check if the namespace exists: ``` kubectl get namespace ``` If it doesn't exist, create it: ``` kubectl create namespace midaz ``` #### 3.2. Copy the Helm install command from the install new Agent window. It typically looks like this, but with your unique token and environment: ``` kubectl apply -n midaz -f "https://app.distr.sh/api/v1/connect?targetId=115c891b-89da-46bb-ac22-b13f507dcf5d&targetSecret=40767acb4e78974d445df26db1b3c642" ``` Always copy the command exactly as shown in the UI. It includes deployment-specific values. #### 3.3. Run the command in your terminal. Use a terminal connected to your Kubernetes cluster and run the following command: ``` kubectl apply -n midaz -f "https://app.distr.sh/api/v1/connect?targetId=115c891b-89da-46bb-ac22-b13f507dcf5d&targetSecret=40767acb4e78974d445df26db1b3c642" ``` #### 3.4. Confirm that the agent was installed To verify the agent was successfully installed: ``` kubectl get pods -n midaz ``` You should see a pod named `distr-agent-xxxxx` with the status Running. #### 3.5. Click the Continue button in the Install a new Agent window. ### 4. Configure your application Select the application you want to deploy and the version. The Helm values will be displayed automatically. You can use the **Helm values** section to apply environment-specific configuration, like: * Replica counts. * Secrets and environment variables. * Domain and ingress settings. **Never** use the Helm default information. It is example information and should never be used in production environment. It is recommended that after you define your secrets and variables to use in Helm, **save a copy of your defined secrets in your secrets manager service**. ### 5. Monitor your deployment You can track the status in two ways: * In your Kubernetes cluster, using kubectl or your preferred dashboard. * In the Lifecycle dashboard, with a centralized view of agent status and deployed versions. For detailed Helm agent configuration and advanced capabilities, refer to the official Distr documentation: [Onboarding a Helm App](https://distr.sh/docs/guides/onboarding-a-helm-app/#creating-a-new-deployment) # About Observability Source: https://docs.lerian.studio/en/platform/observability Understand how telemetry — metrics, logs, and traces — flows through Midaz so you can monitor performance and troubleshoot issues. Observability is how we understand what’s happening inside a system by analyzing its outputs: metrics, logs, and traces. This page explains how telemetry flows through **Midaz**, the tools that power it, and how you can connect your own systems to monitor performance, troubleshoot issues, and ensure operational excellence. ## Who configures what? *** To avoid confusion, here’s a quick split of responsibilities: ### Client side On your infrastructure, the main configuration for observability lives in the `components/infra/grafana/otelcol-config.yaml` file. In this file, you define the collector’s behavior: * **Processors**: batching, memory limits, filtering, obfuscation, sampling, etc. * **Exporters**: for example, dual routing to Prometheus * **API key authentication secrets** After editing this file, you **must** restart the stack with `make down && make up` for changes to take effect. This setup ensures telemetry is processed efficiently, secured properly, and routed to the right destinations. ### Lerian side On Lerian’s managed infrastructure, the observability stack is centrally configured and operated. This includes: * **Central Collector** * **Prometheus** * **Loki** * **Tempo** * **Grafana** These components are preconfigured and maintained by Lerian. You don’t edit them directly. This ensures consistency across environments and removes the need for local maintenance on your side. ## How the data flows *** Telemetry data originates in your application and flows through a **Client Collector**, powered by [OpenTelemetry](https://opentelemetry.io/). Running in your environment, this collector enriches the data and securely forwards it to a **Central Collector** managed by Lerian. From there, it’s routed to three specialized backends: * [**Prometheus**](https://grafana.com/oss/prometheus/) for metrics * [**Loki**](https://grafana.com/oss/loki) for logs * [**Tempo**](https://grafana.com/oss/tempo) for traces Grafana sits on top of everything, giving you a unified view of all telemetry signals. This flow ensures observability at scale, built on OpenTelemetry standards for portability and consistency. ## Stack components *** Together, these components form a complete observability pipeline: flexible on your side, consistent and secure on Lerian’s side, and fully based on **OpenTelemetry standards**. #### Client Collector The Client Collector is a lightweight [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) that runs close to your application, either as a DaemonSet or a Deployment. It enriches telemetry with Kubernetes metadata and your tenant identifier (`client_id`), then routes the data to the Central Collector. It matters because it reduces load on the central pipeline, enables source-level filtering, and attaches crucial metadata such as `k8s.pod.name`. Installation is managed via Helm and Terraform, making it easy to integrate into your infrastructure. #### Central Collector The Central Collector is a centralized [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) deployment that receives telemetry from all clients. It performs global processing, enforces multi-tenancy, and exports signals to the appropriate storage backends. The Central Collector is fully managed by Lerian. You don’t configure or modify it directly. This setup ensures consistency across tenants and guarantees that telemetry data is routed securely and efficiently to its final destinations. #### Prometheus Prometheus is optimized for storing and querying numerical time-series data. The Central Collector pushes metrics using `remote_write`. #### Loki Loki stores logs using label-based indexing, making it fast and cost-effective. Logs are sent from the Central Collector to the `loki-write` service. #### Tempo Tempo stores full distributed traces and integrates tightly with Prometheus and Loki through Grafana. #### Grafana Grafana is your single pane of glass. It connects to Prometheus, Loki, and Tempo, enabling you to correlate metrics, logs, and traces in one place. You can pivot between metrics, logs, and traces directly inside Grafana to speed up troubleshooting. ## Embedded Collector *** You can enable the Client Collector as a dependency of your Midaz application with a single configuration flag: ``` otel-collector-lerian: enabled:true ``` This automatically installs a DaemonSet and configures your application to export telemetry to it. The required environment variables and secrets are injected via Helm, so you don’t need to manage them manually. ## Editing the Client Collector *** When you need to customize behavior (obfuscation, filtering, sampling, etc.), you will: 1. Edit `components/infra/grafana/otelcol-config.yaml`. 2. Add or adjust the `processors` or `exporters` blocks. 3. Restart the stack: ``` make down make up ``` ## Client Collector Processors *** In the OpenTelemetry Collector, **processors** are the core of data manipulation. They run sequentially to enrich, filter, sample, and transform telemetry data before exporting it to the backends. Below is the list of processors configured in the **Lerian Client Collector**, their purpose, and how to configure them. **Where to configure**: add each block under `processors:` in `otelcol-config.yaml`. **1. batch** * **What it is:** Groups multiple telemetry signals (metrics, logs, or traces) into batches before sending them to the next stage. * **Why it matters:** Improves compression efficiency, reduces network requests, and enhances overall pipeline performance. * **Configuration:** ``` processors: batch: {} ``` **2. memory\_limiter** * **What it is:** Monitors the collector's memory usage and drops data if it approaches a defined threshold. * **Why it matters:** Prevents the collector from being OOMKilled by Kubernetes. * **Configuration:** ``` processors: memory_limiter: check_interval: 1s limit_percentage: 75 spike_limit_percentage: 15 ``` **3. spanmetrics** * **What it is:** Generates metrics directly from trace data. * **Why it matters:** Produces “RED” metrics (Rate, Errors, Duration) automatically. * **Configuration:** ``` processors: spanmetrics: metrics_exporter: prometheus dimensions: - name: http.method - name: http.status_code - name: service.name - name: client.id ``` **4. transform/remove\_sensitive\_attributes** * **What it is:** Removes sensitive span attributes using regex. * **Why it matters:** Keeps identifiers but strips headers, bodies, or other sensitive request data. * **Configuration:** ``` processors: transform/remove_sensitive_attributes: trace_statements: - context: span statements: - delete_matching_keys(attributes, "^app\\.request\\.(?!request_id$).*") ``` **5. tail\_sampling** * **What it is:** A sampling strategy applied after spans are received. * **Why it matters:** Keeps only high-value traces (errors, specific clients) and reduces storage costs. * **Configuration:** ``` processors: tail_sampling: policies: - name: keep_client_traces_policy - name: http_server_errors_policy - name: drop_all_other_traces_policy ``` **6. filter/drop\_node\_metrics** * **What it is:** Filters out node-level metrics. * **Why it matters:** Reduces noise and focuses on app-level telemetry. * **Configuration:** ``` processors: filter/drop_node_metrics: metrics: exclude: match_type: regexp metric_names: - ^k8s\\.node\\..*$ ``` **7. filter/include\_midaz\_namespaces** * **What it is:** Keeps only metrics from `midaz` and `midaz-plugins`. * **Why it matters:** Eliminates irrelevant Kubernetes workloads. * **Configuration:** ``` processors: filter/include_midaz_namespaces: metrics: include: match_type: regexp resource_attributes: - key: k8s.namespace.name value: '^(midaz|midaz-plugins)$' ``` **8. k8sattributes** * **What it is:** Adds Kubernetes metadata to telemetry. * **Why it matters:** Enables richer context in Grafana queries. * **Configuration:** ``` processors: k8sattributes: auth_type: "serviceAccount" passthrough: false extract: metadata: - k8s.pod.name - k8s.deployment.name - k8s.namespace.name - k8s.node.name ``` **9. resource/add\_client\_id** * **What it is:** Inserts or updates `client.id` in telemetry. * **Why it matters:** Critical for multi-tenancy. * **Configuration:** ``` processors: resource/add_client_id: attributes: - key: client.id value: "example-client" action: upsert ``` **10. transform/remove\_log\_body** * **What it is:** Removes log body content. * **Why it matters:** Prevents sensitive or PII data from persisting in logs. * **Configuration:** ``` processors: transform/remove_log_body: log_statements: - context: log statements: - set(body, "") ``` **11. transform/obfuscate\_attributes** * **What it is:** Obfuscates selected attributes. * **Why it matters:** Protects sensitive values (like `legalDocument` or `accountAlias`) before data leaves your cluster. * **Configuration (**`otelcol-config.yaml`**):** ``` processors: transform/obfuscate_attributes: trace_statements: - context: span statements: - replace_pattern(attributes["legalDocument"], ".*", "***") - replace_pattern(attributes["accountAlias"], ".*", "***") log_statements: - context: log statements: - replace_pattern(attributes["legalDocument"], ".*", "***") - replace_pattern(attributes["accountAlias"], ".*", "***") ``` * **Customizing the fields:** * Defaults: `legalDocument`, `accountAlias` * Add or remove fields as needed * Restart required: `make down && make up` ### In short | Processor | Data type | Function | Benefit | | :------------------------------------------ | :-------------------- | :----------------------------------- | :------------------------------------- | | **batch** | Metrics, Logs, Traces | Groups telemetry before export | Improves compression and network use | | **memory\_limiter** | Metrics, Logs, Traces | Drops data when memory limit is near | Prevents OOMKilled | | **spanmetrics** | Traces → Metrics | Creates RED metrics | Immediate performance insights | | **transform/remove\_sensitive\_attributes** | Traces | Strips sensitive span attrs | Keeps IDs, removes secrets | | **tail\_sampling** | Traces | Smart sampling of traces | Lower storage, focus on errors/targets | | **filter/drop\_node\_metrics** | Metrics | Excludes noisy node-level data | Cleaner dataset | | **filter/include\_midaz\_namespaces** | Metrics | Keeps only Midaz namespaces | Removes irrelevant metrics | | **k8sattributes** | Metrics, Logs, Traces | Adds K8s metadata | Richer Grafana context | | **resource/add\_client\_id** | All signals | Tags telemetry with client ID | Enables multi-tenancy | | **transform/remove\_log\_body** | Logs | Clears log body | Avoids storing PII | | **transform/obfuscate\_attributes** | All signals | Masks chosen fields | Ensures sensitive data never leaves | ## Protecting sensitive data *** Midaz treats the **Client Collector as a telemetry firewall**. All filtering, sampling, and transformation rules are defined in your configuration file (`components/infra/grafana/otelcol-config.yaml`). Observability Client Leria Jp This file runs inside **your infrastructure**, ensuring that sensitive attributes are removed or obfuscated *before* data leaves your cluster. Sensitive values such as request bodies, legal documents, or account aliases never reach Lerian’s Central Collector. Our architecture enforces this separation: * **Client Collector (you configure):** Runs in your cluster. Apply processors such as `transform/remove_sensitive_attributes`, `transform/remove_log_body`, and `transform/obfuscate_attributes`. * **Central Collector (Lerian managed):** Receives only the filtered, sanitized telemetry streams and routes them to Prometheus, Loki, and Tempo. Configurations are fully packaged and managed via Helm, keeping deployments consistent, traceable, and aligned with best practices. You decide what’s sensitive in `otelcol-config.yaml`. Lerian only sees sanitized telemetry. ## Telemetry flow *** Here’s what happens when telemetry is enabled: 1. **Application starts** and detects OpenTelemetry configuration. 2. **Telemetry is exported** to the local Client Collector. 3. **Client Collector enriches data** with Kubernetes metadata and your `client_id`. 4. **Processors enrich**, filter, and transform the data. 5. **Data is forwarded** to the Central Collector. 6. **Central Collector processes and routes data**: * Metrics → Prometheus * Logs → Loki * Traces → Tempo 7. **Grafana lets you query it all**, correlating across signals. You can, for example, run: ``` sum(rate(http_server_duration_seconds_count{ k8s_pod_name=~"checkout-.*", client_id="client-name" }[5m])) ``` And then jump straight to the related logs or traces. ## Authenticating collector requests *** To ensure data security and integrity, all telemetry sent from your cluster to Lerian’s platform must be authenticated using a secure API key. ### How to set it up 1. **Create the Kubernetes Secret** to store your API token: ``` kubectl create secret generic otel-api-key \ --from-literal=OTEL_API_KEY='YOUR_TOKEN_HERE' \ -n midaz ``` 2. **Reference the secret in your Helm values file** to inject it as an environment variable: ``` extraEnvs: - name: OTEL_API_KEY valueFrom: secretKeyRef: name: otel-api-key key: OTEL_API_KEY ``` 3. **Telemetry is securely sent** to Lerian’s telemetry endpoint over HTTPS, with the API key included in the headers. ``` ``` This key must remain private. If compromised, contact Lerian support immediately to rotate the token. ## Data encryption in transit *** All telemetry data, including metrics, logs, and traces, is transmitted from your environment to Lerian’s observability platform using **HTTPS with TLS encryption**. This means: * The communication between the Client Collector and the Central Collector is fully encrypted. * Data in transit is protected against interception, tampering, or unauthorized access. * Even if network traffic is inspected, the contents remain unreadable without the proper cryptographic keys. Combined with API key authentication, this ensures your telemetry is both **secure and verifiable** from source to destination. We enforce encrypted transport by default. No data is accepted over insecure channels. ## Dual routing *** Need to keep a copy of your metrics internally? You can configure the Client Collector to send telemetry to multiple destinations. ### Example ``` exporters: otlphttp/server: endpoint: "" headers: x-api-key: "${OTEL_API_KEY}" prometheus/local: endpoint: prometheus-server-example:8889 ``` Add both exporters to your metrics pipeline, and the same metrics will be sent to our platform and your internal Prometheus. This setup is ideal for local monitoring without disrupting the standard flow to Lerian’s observability stack. ## Glossary *** A Kubernetes workload type that ensures a Pod runs on every (or selected) node in a cluster. Used for deploying the **Client Collector**, so that it can collect node-level data like Kubelet metrics. A Kubernetes workload that manages replicas of a Pod. Used for the **Central Collector** and other platform services. Sends telemetry data from the Collector to one or more backends (e.g., Prometheus for metrics, Loki for logs, Tempo for traces). A source-available visualization layer. Grafana connects to Prometheus, Loki, and Tempo to provide a unified interface for querying and exploring metrics, logs, and traces. Our backend for **logs**. Loki indexes metadata labels rather than full log content, making it fast and cost-efficient for high-volume use cases. An architectural approach where a single platform serves multiple clients (tenants). In Midaz, telemetry data is tagged with a `client_id` to ensure isolation and traceability across tenants. The ability to understand a system’s internal state by analyzing its external outputs. In practice, it means collecting and analyzing **metrics**, **logs**, and **traces** to monitor performance and troubleshoot issues. A source-available framework with tools, APIs, and SDKs for instrumenting, generating, collecting, and exporting telemetry data — metrics, logs, and traces. A standalone service that receives, processes, and exports telemetry data. It acts as a bridge between instrumented applications and backends like Prometheus or Grafana. The default protocol used by OpenTelemetry to transport telemetry data between applications, collectors, and backends via gRPC or HTTP. Defines how telemetry flows through the Collector. A pipeline typically chains together **receivers**, **processors**, and **exporters** for a given signal type (metrics, logs, or traces). Handles data transformation inside the Collector, such as enriching signals with metadata, filtering unwanted data, batching messages, or enforcing sampling policies. Our backend for storing and querying **metrics**. It supports powerful time-series queries (PromQL) and integrates with the OpenTelemetry Collector via remote write. The component of the Collector that ingests incoming telemetry data. Supports formats like OTLP, Jaeger, Prometheus, and others. A set of libraries you embed in your application code to produce telemetry signals like spans, counters, or logs. Our backend for **traces**. It stores full distributed traces and integrates closely with Prometheus and Loki for seamless correlation in Grafana. An Infrastructure as Code (IaC) tool we use to provision and manage cloud infrastructure, including the installation of observability components via Helm. # Installing and deploying Source: https://docs.lerian.studio/en/platform/plugins/installing-and-deploying-plugins Step-by-step guide for installing and deploying Midaz plugins on Kubernetes with Helm, covering prerequisites, values configuration, and post-install work. Midaz plugins are distributed as **independent Helm charts** and follow the same deployment model as Midaz Core. Each plugin runs as a separate service alongside the platform, with its own configuration, dependencies, and lifecycle. This guide walks you through installing, configuring, and verifying plugin deployments on Kubernetes. Before deploying any plugin, make sure you have a running **Midaz Core** instance. Plugins depend on Midaz Core APIs and cannot operate independently. See the [Midaz Helm deployment guide](/en/platform/helm/midaz/midaz-installation) if you haven't set up Midaz yet. ## Prerequisites *** Before deploying plugins, ensure you have: * [**Kubernetes (v1.30+)**](https://kubernetes.io/releases/download/) – A running cluster with Midaz Core already deployed. * [**Helm 3+**](https://helm.sh/docs/intro/install/) – Installed and available. * **kubectl** configured with access to your cluster. * **Cluster admin** permissions or appropriate RBAC roles. * A valid **Enterprise license key** (required for all plugins except CRM). Verify your tools are ready: ```bash theme={null} helm version ``` ```bash theme={null} kubectl cluster-info ``` CRM does not require a license key and, starting with Midaz v5.x, is available as an embedded component of Midaz, shipped from the same source-available Midaz repository. All other plugins require a valid Enterprise license. Contact a Lerian representative if you need one. ## Available plugin charts *** Each plugin is published as an OCI-compatible Helm chart. The table below lists all available plugins and their chart references. | Plugin | Chart name | OCI registry | Default namespace | | :--------------------- | :--------------------------- | :--------------------------------------------------------------------- | :---------------- | | **CRM** | `plugin-crm` | `oci://registry-1.docker.io/lerianstudio/plugin-crm` | `midaz-plugins` | | **Fees Engine** | `plugin-fees` | `oci://registry-1.docker.io/lerianstudio/plugin-fees-helm` | `midaz-plugins` | | **Pix Direct (JD)** | `plugin-br-pix-direct-jd` | `oci://registry-1.docker.io/lerianstudio/plugin-br-pix-direct-jd` | `midaz-plugins` | | **Pix Indirect (BTG)** | `plugin-br-pix-indirect-btg` | `oci://registry-1.docker.io/lerianstudio/plugin-br-pix-indirect-btg` | `midaz-plugins` | | **Bank Transfer** | `plugin-br-bank-transfer` | `oci://registry-1.docker.io/lerianstudio/plugin-br-bank-transfer-helm` | `midaz-plugins` | Starting from Midaz v5.x, **CRM** is now available as an integrated component within the main Midaz Helm chart. If you are running v5.x, you can enable CRM directly in your Midaz values instead of deploying it separately. See the [Midaz Helm guide](/en/platform/helm/midaz/midaz-installation) for details. ## Installing a plugin *** The installation process is the same for all plugins. Replace the chart name, registry, and version for the plugin you want to deploy. ### 1. Check available versions You can find available chart versions by checking the [Helm repository tags](https://github.com/LerianStudio/helm/tags) on GitHub. Filter by the plugin prefix (e.g., `plugin-crm-v`, `plugin-fees-v`). You can also check the [version compatibility table](/en/platform/helm/helm-version-compatibility) to find the right chart version for your Midaz Core version. ### 2. Install the chart ```bash theme={null} helm install plugin-crm oci://registry-1.docker.io/lerianstudio/plugin-crm \ --version \ -n midaz-plugins \ --create-namespace ``` ```bash theme={null} helm install plugin-fees oci://registry-1.docker.io/lerianstudio/plugin-fees-helm \ --version \ -n midaz-plugins \ --create-namespace ``` ```bash theme={null} helm install plugin-br-pix-direct-jd oci://registry-1.docker.io/lerianstudio/plugin-br-pix-direct-jd \ --version \ -n midaz-plugins \ --create-namespace ``` ```bash theme={null} helm install plugin-br-pix-indirect-btg oci://registry-1.docker.io/lerianstudio/plugin-br-pix-indirect-btg \ --version \ -n midaz-plugins \ --create-namespace ``` Replace `` with the desired chart version. The `--create-namespace` flag creates the `midaz-plugins` namespace if it doesn't already exist. ### 3. Verify the installation After installing, confirm the release is deployed: ```bash theme={null} helm list -n midaz-plugins ``` Check that all pods are running: ```bash theme={null} kubectl get pods -n midaz-plugins ``` All pods should show `Running` status and `READY` state. To install a plugin with custom configuration, create a `values.yaml` file and pass it with the `-f` flag: ```bash theme={null} helm install plugin-crm oci://registry-1.docker.io/lerianstudio/plugin-crm \ --version \ -n midaz-plugins \ --create-namespace \ -f my-plugin-crm-values.yaml ``` ## Configuring license keys *** All plugins except CRM require a valid Enterprise license key. You configure it through the Helm chart's `secrets` section in your `values.yaml`: ```yaml theme={null} : secrets: LICENSE_KEY: "" ORGANIZATION_IDS: "" ``` Replace `` with the plugin's service key (e.g., `crm`, `fees`). Deploying a licensed plugin without a valid key will result in the service starting but rejecting API requests. Make sure your license key and organization IDs are configured before going to production. ## Configuring dependencies *** Plugins bundle their own database dependencies by default. This means you can deploy a plugin and have a working setup without any extra database configuration. However, for production environments, you'll likely want to use your own managed databases. ### MongoDB The **CRM**, **Fees Engine**, and **Pix** charts use MongoDB for data storage. Each chart includes a bundled [Bitnami MongoDB](https://charts.bitnami.com/bitnami) dependency (v16.4.0) that is enabled by default. To use an **external MongoDB** instance, disable the bundled dependency and point the plugin to your instance: ```yaml theme={null} mongodb: enabled: false : configmap: MONGO_HOST: MONGO_NAME: MONGO_USER: MONGO_PORT: "" secrets: MONGO_PASSWORD: ``` Replace `` with the plugin's service key (e.g., `crm`, `fees`). ## Using existing Kubernetes Secrets *** For production environments, you can manage secrets outside of Helm by referencing an existing Kubernetes Secret. This avoids storing sensitive values directly in your `values.yaml`. Create a Kubernetes Secret with the required keys for your plugin. For example, for CRM: ```bash theme={null} kubectl create secret generic plugin-crm-secrets \ --from-literal=LCRYPTO_HASH_SECRET_KEY='' \ --from-literal=LCRYPTO_ENCRYPT_SECRET_KEY='' \ --from-literal=MONGO_PASSWORD='' \ --from-literal=LICENSE_KEY='' \ --from-literal=ORGANIZATION_IDS='' \ -n midaz-plugins ``` Configure the plugin to use the existing secret: ```yaml theme={null} crm: useExistingSecret: true existingSecretName: "plugin-crm-secrets" ``` This pattern works for all plugins. Each plugin accepts `useExistingSecret` and `existingSecretName` parameters. ## Configuring ingress *** Plugin services are deployed as `ClusterIP` by default, meaning they are only accessible within the cluster. To expose a plugin externally, enable ingress in your `values.yaml`. The ingress configuration follows the same pattern as Midaz Core. Here's an example using NGINX: ```yaml theme={null} : ingress: enabled: true className: "nginx" annotations: {} hosts: - host: plugin.example.com paths: - path: / pathType: Prefix tls: - secretName: plugin-tls hosts: - plugin.example.com ``` Replace `` with the plugin's service key. For detailed ingress configuration examples with **AWS ALB** and **Traefik**, refer to the [Midaz Helm deployment guide](/en/platform/helm/midaz/midaz-ingress). The same patterns apply to plugin charts. ## Verifying your deployment *** After installing a plugin, verify that everything is running correctly. ### Check pod status ```bash theme={null} kubectl get pods -n midaz-plugins -o wide ``` All pods should be in `Running` state with all containers ready. ### Check pod logs ```bash theme={null} kubectl logs -n midaz-plugins deployment/ --tail=50 ``` Look for successful startup messages and verify there are no errors related to database connections, license validation, or missing configuration. ### Test the health endpoint All plugins expose a `/health` endpoint. You can verify it via port-forwarding: ```bash theme={null} kubectl port-forward -n midaz-plugins svc/ : ``` Then check the health endpoint: ```bash theme={null} curl http://localhost:/health ``` | Plugin | Service name | Default port | | :----------------- | :--------------------------- | :----------- | | CRM | `plugin-crm` | 4003 | | Fees Engine | `plugin-fees` | 4002 | | Pix Indirect (BTG) | `plugin-br-pix-indirect-btg` | 8080 | ## Upgrading plugins *** To upgrade a plugin to a new version, use `helm upgrade` with the target version: ```bash theme={null} helm upgrade \ --version \ -n midaz-plugins \ -f my-plugin-values.yaml ``` Always upgrade **Midaz Core before upgrading plugins**. Plugins depend on Midaz Core APIs, so upgrading in the wrong order may cause compatibility issues. For detailed upgrade procedures, pre-upgrade checklists, and rollback instructions, see the [Helm upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide). ## Uninstalling a plugin *** To remove a plugin from your cluster: ```bash theme={null} helm uninstall -n midaz-plugins ``` For example: ```bash theme={null} helm uninstall plugin-crm -n midaz-plugins ``` Uninstalling a plugin removes its Kubernetes resources (deployments, services, configmaps, secrets) but does **not** delete persistent data stored in databases. If you used the bundled MongoDB, the PersistentVolumeClaims may remain. Delete them manually if you want to fully clean up. ## Related resources *** * [Deploy Midaz using Helm](/en/platform/helm/midaz/midaz-installation) – Initial Midaz Core installation guide * [Helm upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide) – Upgrade procedures and rollback instructions * [Version compatibility](/en/platform/helm/helm-version-compatibility) – Helm chart and application version mapping * [Plugin version compatibility](/en/platform/plugins/midaz-version-compatibility) – Plugin compatibility with Midaz Core versions * [What are plugins?](/en/platform/plugins/what-are-plugins) – Overview of the plugin architecture * [Helm repository](https://github.com/LerianStudio/helm) – Source code, charts, and release notes # Version compatibility Source: https://docs.lerian.studio/en/platform/plugins/midaz-version-compatibility Check which plugin versions are compatible with each Midaz Core release before deploying or upgrading your environment, with a full compatibility table. By checking the table below before installing or upgrading, you ensure that **plugin deployments** remain aligned with your **Midaz Core version**, reducing the risk of incompatibilities or unexpected behavior. While most plugins have independent versioning, **CRM** follows a **shared release cycle** with Midaz Core, as it is maintained within the same repository. ## Compatibility table *** | Plugin | Midaz v2 Compatibility | Midaz v3 Compatibility | Notes | | :----------------- | :---------------------------------- | :------------------------------------- | :------------------------------------------------------------------- | | **Access Manager** | v1 | v2 | Each Midaz version requires a specific plugin version | | **CRM** | v2.x.x | v3.x.x | Source-available, versioned together with Midaz, deployed separately | | **Fees Engine** | Not supported | Supported | Only available starting from Midaz v3 | **Midaz v2.x.x** documentation is no longer available. If you're still using v2, we strongly recommend upgrading to **Midaz v3.x.x** to ensure full compatibility with **all** Lerian's products and plugins. ## Upgrade recommendations *** * Always upgrade **Midaz Core** first. * **CRM** is versioned together with Midaz Core (for example, Midaz `v3.5.0` → CRM `v3.5.0`), but it is **deployed independently**. * **Access Manager** and **Fees Engine** require explicit version alignment with Midaz. * Always validate upgrades in a staging environment before production rollout. # What are plugins? Source: https://docs.lerian.studio/en/platform/plugins/what-are-plugins Plugins let you extend a Lerian product without changing its core — learn what plugins are, how they integrate, and when to use them in your architecture. They're modular components that run alongside the platform, adding new logic, endpoints, or workflows to support specific business needs. Instead of reinventing the wheel or building custom features from scratch, you can plug in what you need and keep everything else **stable**, **scalable**, and **secure**. Plugins are premium features currently available only to **Enterprise customers** and are licensed separately. **CRM** is no longer a plugin. It is now an embedded component of Midaz, available out of the box with no additional license required. See [CRM Overview](/en/midaz/crm/crm-overview) for details. In Midaz, we use plugins to let you: * Handle real-time payments with [**Pix**](/en/rails/pix/pix-overview) * Process electronic transfers between banks through [**Bank Transfer (TED)**](/en/rails/ted/ted-overview) And much more, all without touching the platform's core engine. ### Why we designed it this way Midaz was built to be composable. That means everything runs as independent services that talk to each other, including plugins. 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 ### What a plugin can do A plugin can: * Expose new APIs * Persist and transform data * Run background jobs and scheduled tasks * React to events (like new transactions or ledger updates) * Define its own configuration, rules, and access policies As long as it follows Midaz's interface contract and authentication model, it can integrate just like a native feature. You can think of plugins as microservices with a clear purpose, designed to feel native to the rest of the platform, but built to evolve independently. For the full catalog of available plugins and information on building your own, see [Our plugins](/en/about-plugins). # Using Lifecycle Management Source: https://docs.lerian.studio/en/platform/using-lifecycle-management Manage deployed applications with Lifecycle Management — update versions, roll back releases, undeploy apps, and monitor health, drift, and alerts. * Update your application. * Roll back to a previous version. * Undeploy the application. * Monitor and alert app health and drift. Reach out to the Lerian support team if you'd like help configuring alerts or managing your update schedule. ## Update or rollback your application *** To update your application: Click the **Update** button on the **Deployments** page. Select the version from the **Version** dropdown. Adjust the Helm values if needed. Click **Deploy**. To roll back, follow the same steps but choose a previously deployed version. ## Undeploy the application *** To undeploy the application: From the **Deployments** page, click the three dots () next to the **Update** button and select **Undeploy**. A confirmation window will appear. Follow the instructions to continue. ## Delete the application *** To delete: After undeploying, click the three dots () next to **+ Deployment** and select **Delete**. A confirmation window will appear. Follow the instructions to continue. # Choosing the right product Source: https://docs.lerian.studio/en/reference/choosing-the-right-api Learn which Lerian product, plugin, or API to use for each job across ledger, reporting, and orchestration flows, and how the pieces fit together. Each Lerian product and plugin owns one job, and most real flows touch more than one. The rule that prevents most mistakes: > **Plugins perform actions; Midaz is the source of truth for state.** Tell a plugin to *do* something — move money, apply a fee. Ask **Midaz** what the money actually *is* — balances, history, who owns what. ## Which product for which job *** | You want to… | Call | Why | | ------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Authenticate before any call | [**Access Manager**](/en/reference/access-manager/am-api-introduction) | Issues the tokens every product API trusts | | Move money in or out | [**Bank Transfer**](/en/reference/midaz/plugins/ted/initiate-transfer) or **Pix** | Executes the transfer and records it in Midaz for you | | Read a balance, statement, or transaction | [**Midaz**](/en/reference/quick-start-guide) | The Ledger is the source of truth for state — don't infer a balance from a payment plugin | | Identify who owns an account | [**Midaz (CRM)**](/en/reference/midaz/crm/create-holder) | Account owners are Holders in Midaz — there's no separate identity service to register first | | Apply a fee | [**Fees**](/en/reference/midaz/plugins/fees-engine/create-package) | It calculates the fee and returns it; your app submits the transaction to Midaz | | Validate a transaction before it happens | [**Tracer**](/en/reference/tracer/tracer-api-quick-start) | Returns `ALLOW` / `DENY` / `REVIEW` before you submit anything | | Generate a report (including regulatory) | [**Reporter**](/en/reference/reporter/reporter-developer-quick-start) | Reads Midaz data read-only, on its own period and format | | Reconcile transactions across sources | [**Matcher**](/en/reference/matcher/matcher-developer-quick-start) | Automates multi-source matching with configurable rules and immutable audit trails | | Orchestrate a multi-step financial workflow | [**Flowker**](/en/reference/flowker/flowker-api-quick-start) | Manages validation pipelines and sequences of financial operations | **Access Manager** handles authentication and authorization — it controls who can use the products. Account owners are **Holders** stored in Midaz; there's no separate identity service to register before opening an account. ## Flows that span products *** Most real financial operations chain more than one product together. These are the common sequences. ### Payment with validation > Use **Tracer** → **Bank Transfer** or **Pix** → Midaz Call **Tracer** first. It returns `ALLOW`, `DENY`, or `REVIEW`. Stop here if the decision is `DENY` — no payment call needed. Call one of the plugins used for payment: * **Bank Transfer** for wire transfers * **Pix** (Direct or Indirect) for instant payments Read the result from **Midaz**. **Bank Transfer** or **Pix** confirms the transfer event; Midaz confirms the new balance. These are two different facts. ### Apply fees > Use **Fees** → **Midaz** Call **Fees** to calculate the applicable fee. It returns the amount — it does not post anything to the ledger. Submit the fee transaction to **Midaz**. Fees tells you what to charge; Midaz records it. ### Regulatory or period report > Use **Midaz** → **Reporter** The source data lives in **Midaz** — transactions, balances, holders. Call **Reporter** with the period and output format. It reads Midaz read-only and produces the BACEN or fiscal file. ### Transaction reconciliation > Use **Midaz** + external sources → **Matcher** **Matcher** reads from Midaz and one or more external sources. It applies configurable matching rules and produces match results, exceptions, and an immutable audit trail. It does not write to Midaz. ## See also *** * The conceptual map of how the pieces connect: [Building a complete core banking](/en/building-a-complete-core-banking) * Midaz-specific scenarios: [Midaz use cases](/en/midaz/use-cases) # API patterns and tools Source: https://docs.lerian.studio/en/reference/integration-essentials API patterns, conventions, and tooling that keep Lerian integrations stable, scalable, and maintainable as the platform and its services evolve over time. Seamless integration and smooth operation are at the core of Lerian's platform. Whether you're developing a new application or refining an existing one, it's essential to understand the key mechanisms that keep interactions reliable and efficient. This section breaks down the best practices that ensure your integrations are stable, scalable, and easy to maintain. ## Available pages ***
Ensure safe retries and prevent duplicate transactions using idempotency keys. Structure and optimize data use with custom metadata fields. Manage large datasets efficiently while keeping performance high. Build integrations that adapt to changes without breaking. Check the default ports used by Midaz and the plugins. Use our Postman collection to interact with our APIs.
# Welcome to our API Reference Source: https://docs.lerian.studio/en/reference/introduction Explore every API across the Lerian ecosystem, organized by product and plugin, with consistent REST patterns, explicit versioning, and shared conventions. **This section is intended for developers and technical integrators.** If you're looking for a business-level overview of what Lerian products do, start with [Who we are](/en/who-we-are) or explore individual product pages like [About Midaz](/en/midaz/about-midaz) and [What is Matcher?](/en/matcher/what-is-matcher). Lerian APIs offer reliable, scalable, and secure access to everything the platform provides, from onboarding workflows and ledger management to plugins for authentication, CRM, and reporting. This section is your gateway to **every API in the Lerian ecosystem**, organized by product. Whether you're integrating directly with **Midaz** or automating reports with **Reporter**, you'll find the full reference here—including any related plugin APIs. ## Product-structured, plugin-aware *** Each product—like **Midaz** or **Reporter**—has its own API section. When a product supports plugins (e.g., Midaz), those plugin APIs are embedded within that product's reference. This makes it easy to understand the full surface area of what you’re integrating with. For example, within the Midaz reference, you’ll find APIs for core entities (like Ledgers and Transactions) and for the Fees Engine, part of the Midaz product family, as well as for plugins such as Pix and CRM. ## Key highlights *** * **RESTful and predictable**: All endpoints follow standard HTTP methods and status codes. * **Consistent formats**: Requests and responses use JSON, with clear typing and structures. * **Explicit versioning**: APIs are versioned to prevent breaking changes. * **Plugin-first architecture**: Features are modular—each plugin has its own API set, documented alongside the product it extends. ## What you'll find here *** * **Getting Started**: Quick start guide, sandbox instructions, and how Lerian’s licensing model works. * **Integration Essentials**: Concepts like retries, pagination, metadata, and stability tools. * **Stability & Quality**: Error handling, testing best practices, QA process, and support coverage. * **Product APIs**: * *Midaz*: Ledgers, Transactions, Assets, Accounts, Portfolios, and plugin extensions * *Reporter*: Report creation, template upload, filters, and report exports * **Error Lists**: Each product and plugin includes dedicated error code references. You’re viewing the documentation for the **current version** of Lerian’s APIs. If you’re working with an earlier release, use the **version switcher** (top-left) to access **legacy docs** and avoid compatibility issues. ## Current vs Previous versions *** We maintain documentation for both: * **Current version**: The most up-to-date features, actively supported. * **Previous version**: Deprecated but still available with partial support for a limited time. Use the version switcher at the top-left of the screen to ensure you're reading the right docs for your environment. And if something’s unclear, let us know—we’re always evolving the docs to match your needs. ## How to navigate *** To access a product's reference: You’re already in the **API Reference** section of our documentation. Open the dropdown menu in the left sidebar and select the product you’re working with—such as **Midaz** or Reporter. You’ll land in a dedicated space with product-specific API details—including related plugins, full schema references, and real-world payload examples. # Metadata Source: https://docs.lerian.studio/en/reference/metadata Attach custom key-value metadata to Midaz entities so you can extend the standard ledger schema with data specific to your integration and business needs. Our platform provides a flexible way to enrich entities with user-defined information through **Metadata**. This feature is available across all entities within our API, allowing you to attach custom data as key-value pairs within a metadata object. Metadata is a powerful tool for adding relevant, user-defined information to API entities, enabling them to hold data specific to your integration needs. Metadata can be added at any time, making entities more adaptable to evolving requirements. We encourage using metadata to enhance your API integrations, creating an experience that aligns with your application's needs. ## What is metadata? *** Metadata lets you store supplementary information alongside an entity’s standard data model by adding custom attributes or annotations without altering the entity's predefined schema. This flexibility provides entities with additional context, making them adaptable to specific requirements. In Midaz, metadata is an object that supports key-value pairs in these data types: strings (**up to 100 characters**), integers, floats, and booleans. Metadata does **not** support nested values. ### Example of metadata Consider an Organization entity as shown below: ```jsonon JSON expandable theme={null} { "id": "cc15194a-6bc9-4ebb-b15d-43411a54ba4b", "parentOrganizationId": null, "legalName": "Empresa Teste Ltda", "doingBusinessAs": "Empresa Teste", "legalDocument": "86820799000188", "address": { "line1": "Rua Botucatu, 10", "line2": "Casa B", "zipCode": "04023060", "city": "São Paulo", "state": "SP", "country": "BR" }, "metadata": null, "status": { "code": "ACTIVE", "description": null }, "createdAt": "2024-02-08T16:59:31+0300", "updatedAt": "2024-02-08T16:59:31+0300", "deletedAt": null } ``` To enrich this organization with additional information, such as industry sector and number of employees, you can include a metadata object in your `POST`, `PUT`, or `PATCH` requests: ```jsonon JSON theme={null} { "metadata": { "sector": "Fintech", "employees": "50-100" } } ``` ## Creating entities with metadata *** When creating an entity, you can submit metadata through our `POST` endpoints, allowing you to add custom data from the start. This feature helps create a detailed and complete representation of each entity in your system. ## Updating and removing metadata *** Our API follows the JSON Merge Patch RFC for metadata updates, ensuring that metadata remains adaptable and current: * **Adding or Updating Values**: Submitting a key with a new value in a `PUT` or `PATCH` request updates the existing metadata. * **Removing Keys**: Omitting a previously included key in a subsequent `PUT` or `PATCH` request removes that key-value pair from the entity’s metadata. ### Practical example: modifying metadata Suppose an Organization entity initially included metadata for sector and employee count. To update the employee count while removing the sector, send a `PATCH` request with the following metadata: ```jsonon JSON theme={null} { "metadata": { "employees": "100-150" } } ``` After the request is processed, the sector key will be removed, and the employee count will be updated. # Retries and idempotency Source: https://docs.lerian.studio/en/reference/retries-idempotency Use idempotency keys to prevent duplicate operations on Midaz transaction endpoints when retrying transient failures, network timeouts, and 5xx responses. A key aspect of Midaz’s reliability is making sure operations are **safe to retry** and never processed more than once. Whether you're handling transactions or creating entities, your integration should be resilient, even when facing network hiccups, timeouts, or temporary disruptions. This page explains how Midaz protects against duplication using **idempotency keys**, and how you can implement them to handle retries in your system confidently. ### Making retries safe In real-world systems, failed API requests are common. Maybe a network timeout occurs. Maybe your service goes down right after sending a request. In these cases, it’s natural to retry, but how can you be sure Midaz won’t process the same operation twice? That’s where **idempotency keys** come in. By attaching a unique key to each request, you’re telling Midaz: "This is the same operation. If you’ve already processed it, don’t do it again." Midaz stores this key temporarily and uses it to determine whether the request is new, already completed, or still being processed. This protects your system from duplicates while giving you full control over your retry strategy. ## Idempotency in Midaz *** Midaz uses **idempotency keys** to make sure transaction operations are safe to retry and never processed more than once. This mechanism is available on all **transaction endpoints**: `/transactions/json`, `/transactions/dsl`, `/transactions/inflow`, `/transactions/outflow`, `/transactions/annotation`, and `/transactions/{id}/revert`. Other Lerian products also support idempotency through their own headers — see the [Idempotency across Lerian products](#idempotency-across-lerian-products) section below. This page focuses on idempotency for the Midaz Ledger API. The `commit` and `cancel` transaction endpoints use a Redis-based lock to prevent concurrent processing of the same transaction, but they do not support full idempotency (no cached responses or `X-Idempotency-Replayed` header). To use it, your request can include two headers: * `X-Idempotency`: the unique key that identifies the request. * `X-TTL`: the time-to-live (in seconds) that Midaz should store this key in cache. If you don’t send the `X-Idempotency` header, Midaz automatically generates one by computing a SHA-256 hash of the request body. This means identical request bodies sent to the same organization and ledger are automatically deduplicated. Here’s what happens behind the scenes: 1. When a new key arrives, Midaz marks it as `pending`, processes the request, and stores the full response in cache. 2. If the same key is used again within the TTL window: 1. If the operation is still running, Midaz returns a `409 Conflict` (error code `0084`) with `X-Idempotency-Replayed: false`. 2. If it’s done, Midaz returns the exact same response with a `201 Created` status code and `X-Idempotency-Replayed: true`. 3. If the transaction fails due to validation or insufficient balance errors, Midaz **deletes the idempotency key**, allowing you to retry with the same key after fixing the issue. In Midaz, idempotency keys are scoped by **organization and ledger**. This means the same key value can be used independently across different organizations or ledgers without conflict. Other Lerian products use different scoping strategies — see the [comparison table](#idempotency-across-lerian-products) for details. ### Workflow summary *Figure 1* shows the full lifecycle of an idempotent request: Full lifecycle of an idempotent request in Midaz, from receiving the idempotency key to returning the cached or newly processed response **How it works:** * If the request **doesn’t include an existing idempotency key**, Midaz creates a new one (or auto-generates one from the request body hash), processes the request, stores the response, and returns it with `X-Idempotency-Replayed: false`. * If the key **already exists**: * If the operation is still running, Midaz returns a `409 Conflict` with `X-Idempotency-Replayed: false`. * If the operation is complete, Midaz skips execution and returns the cached response with a `201 Created` status code and `X-Idempotency-Replayed: true`. * If the original request **failed due to validation or balance errors**, the key is cleaned up automatically, so you can safely retry with the same key. This behavior ensures every request is handled safely, predictably, and without duplication, even when retried. ### Example request Here’s how to send an idempotent request to create a transaction: ```http HTTP theme={null} POST /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/json HTTP/1.1 X-Idempotency: 7fb8e1d098cd4730bb932d038b3b8651 X-TTL: 60 Content-Type: application/json { "description": "Monthly subscription payment", "code": "SUB-2025-001", "send": { "asset": "USD", "value": "1500", "source": { "from": [ { "accountAlias": "customer-usd-1", "amount": { "asset": "USD", "value": "1500" } } ] }, "distribute": { "to": [ { "accountAlias": "merchant-usd-1", "amount": { "asset": "USD", "value": "1500" } } ] } } } ``` If the request succeeds and you send it again within 60 seconds, Midaz will return the cached result with: ```http HTTP theme={null} HTTP/1.1 201 Created X-Idempotency-Replayed: true ``` If you send it again while the original is still being processed, you’ll receive: ```http HTTP theme={null} HTTP/1.1 409 Conflict X-Idempotency-Replayed: false ``` ## Key generation *** You can provide your own `X-Idempotency` key or let Midaz generate one automatically. ### Automatic key generation If you omit the `X-Idempotency` header, Midaz computes a SHA-256 hash of the request body and uses it as the idempotency key. This means that sending the exact same JSON body to the same organization and ledger will be automatically deduplicated — no extra work needed. This is sufficient for most retry scenarios where the request body doesn’t change between attempts. ### Custom key generation Use a custom key when you need to: * Correlate the idempotency key with an ID in your own system (e.g., an order ID). * Retry with a modified request body while still deduplicating (e.g., after correcting a field). * Control the key format for logging or auditing purposes. The key should be deterministic: if the same logical operation is retried, the key remains the same. A common approach is to use a UUID or a hash based on your internal reference: ```python Python theme={null} import uuid # Option 1: Use an existing business ID as the key idempotency_key = f"order-{order_id}" # Option 2: Generate a UUID and store it for retries idempotency_key = str(uuid.uuid4()) ``` ## Best practices *** ### Always validate the `X-Idempotency-Replayed` header When your system receives a response from a transaction endpoint, **always check the `X-Idempotency-Replayed` response header** before processing the result. This header tells you whether the response is from a new operation or a cached replay: * `X-Idempotency-Replayed: false` — This is a fresh response. The transaction was just processed. * `X-Idempotency-Replayed: true` — This is a cached response. The transaction was already processed previously. Failing to check this header is a common integration mistake. Without it, your system may interpret a replayed response as a new transaction, leading to duplicate processing on your side — even though Midaz only executed it once. For example, if your system settles boletos (bank slips) based on transaction responses, you must verify `X-Idempotency-Replayed` to avoid settling the same boleto twice. ### Use explicit idempotency keys for critical flows While Midaz auto-generates keys from the request body, for critical financial flows (settlements, payouts, transfers), always provide an explicit `X-Idempotency` key tied to your business process ID. This gives you: * Full control over deduplication, even if the request body changes slightly between retries. * A clear audit trail linking Midaz transactions to your internal operations. * Protection against edge cases where request serialization might differ. ### Set appropriate TTL values Choose TTL values that match your retry window: * For synchronous operations with fast retries: **60–120 seconds**. * For asynchronous workflows with potential delays: **300–600 seconds**. * For batch processing with long retry windows: consider longer TTLs and explicit keys. ## Retry strategy *** When a request fails, how you retry matters. Here are recommended patterns for handling different failure scenarios: ### Retryable failures These failures are safe to retry with the same idempotency key: | Scenario | What to do | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Network timeout or connection error | Retry with the same key and body. If the original request was processed, you'll get the cached response. | | `5xx` server error | Retry with exponential backoff. The server may be temporarily overloaded. | | `409 Conflict` with `X-Idempotency-Replayed: false` | The previous request is still being processed. Wait and retry after a short delay. | | Validation or balance error | Fix the issue in your request, then retry with the same idempotency key (Midaz deletes the key on these failures). | ### Non-retryable failures These failures require a different approach: | Scenario | What to do | | ------------------------------------ | --------------------------------------------------------------------- | | `400 Bad Request` (schema error) | Fix the request format. Do not retry the same payload. | | `401 Unauthorized` / `403 Forbidden` | Check your authentication credentials. Retrying won't help. | | `404 Not Found` | Verify the organization, ledger, or account IDs in your request path. | ### Exponential backoff For transient errors, use exponential backoff with jitter to avoid overwhelming the server: ```python Python theme={null} import time import random import requests def create_transaction_with_retry(idempotency_key, payload, max_retries=5, base_delay=1.0): """Retries with the same idempotency key on every attempt.""" for attempt in range(max_retries): try: response = requests.post( f"{BASE_URL}/v1/organizations/{org_id}/ledgers/{ledger_id}/transactions/json", headers={ "X-Idempotency": idempotency_key, "X-TTL": "300", "Content-Type": "application/json", }, json=payload, ) response.raise_for_status() return response.json() except requests.exceptions.RequestException: if attempt == max_retries - 1: raise delay = base_delay * (2 ** attempt) + random.uniform(0, 1) time.sleep(delay) ``` A typical retry sequence with this pattern would be: 1s, 2s, 4s, 8s, 16s (plus random jitter on each attempt). ## Preventing entity duplication *** For some endpoints, you don’t need idempotency keys to avoid duplication. Midaz enforces **uniqueness constraints** on critical resources. If you attempt to create an entity that conflicts with an existing one, the system blocks the request and returns a `409 Conflict` with a descriptive error: | Resource | Unique field | Error code | | ------------ | ------------------------------------------ | ---------- | | Ledger | Name (within organization) | `0002` | | Asset | Name or code (within ledger) | `0003` | | Segment | Name (within ledger) | `0015` | | Account | Alias (within organization and ledger) | `0020` | | Account Type | Key value (within organization and ledger) | `0108` | This ensures that your data remains clean, unambiguous, and easy to manage, even when multiple services are operating in parallel or when retries occur automatically. Unlike idempotency keys, these constraints are permanent and don’t expire. ## Idempotency across Lerian products *** Multiple Lerian products support idempotency, each with its own header convention. Most products return the `X-Idempotency-Replayed` response header to indicate whether the response is a cached replay. Midaz always includes this header (`false` for new requests, `true` for replays), while other services only add it when the response is a replay — if the header is absent, the request was processed as new. Direct Pix does not return this header. | Product | Request header | Accepts `X-TTL` | `X-Idempotency-Replayed` | Scope | Covered endpoints | | ---------------- | ---------------------------------------------------- | ------------------ | ------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Midaz** | `X-Idempotency` | Yes (default 300s) | Always (`false`/`true`) | Per organization and ledger | Transaction creation, annotation, and revert (6 endpoints) | | **Matcher** | `X-Idempotency-Key` (also accepts `Idempotency-Key`) | No | On replay only | Per tenant, method, and path | All POST/PUT/PATCH endpoints (\~33 endpoints via global middleware). Callback endpoints use a failed-reacquire pattern: if a previous callback with the same key failed, Matcher automatically reacquires the lock and reprocesses the request. See [Webhooks and callbacks](/en/matcher/integrations/matcher-webhooks-callbacks#automatic-retry-for-failed-callbacks) for details. | | **TED** | `X-Idempotency-Key` | No | On replay only | Per idempotency key (no tenant scoping) | Message submit, return, and cancel (3 endpoints) | | **Indirect Pix** | `X-Idempotency` | Yes | On replay only | Per account | Cashout initiate, cashout process, and refund (3 endpoints) | | **Direct Pix** | `Idempotency-Key` | No | No | Per key | Payment creation and return initiation (2 endpoints) | | **Reporter** | `X-Idempotency` | No | On replay only | Per key | Report and template creation (2 endpoints) | Midaz always includes the `X-Idempotency-Replayed` header in the response (`false` for new requests, `true` for replays). Other services (Matcher, TED, Indirect Pix, and Reporter) only add this header when the response is a replay — if the header is absent, the request was processed as new. Direct Pix does not return this header; its idempotency middleware replays the full response transparently without a replay indicator. Fees Engine, Tracer, Auth, and CRM do not currently support idempotency headers. **Tracer** validations are idempotent by `requestId` instead — POST `/v1/validations` deduplicates on the body field and returns the cached result with HTTP 200 (HTTP 201 on first call). Auth token operations are inherently idempotent. CRM and onboarding entities rely on [uniqueness constraints](#preventing-entity-duplication) instead. ## FAQ *** Midaz automatically generates one by computing a SHA-256 hash of the request body. This means identical request bodies sent to the same organization and ledger are automatically deduplicated. You only need to provide a custom key if you want to control deduplication independently from the request body. Yes. Idempotency keys are scoped per organization and ledger, so the same key value used in different organizations or ledgers won’t conflict. However, within the same organization and ledger, each key must be unique per operation. In Midaz, idempotency keys are scoped per organization and ledger, not per endpoint. If you reuse the same key on a different endpoint within the same organization and ledger, you will receive the cached response from the original endpoint. Always use unique keys for each distinct operation. Only the TTL from the first request is used. Changing it later has no effect. Yes. Midaz replays the full response, including status code (`201 Created`), headers, and body, for completed requests. The default window is **300 seconds** (5 minutes). If the transaction fails due to validation errors or insufficient balance, Midaz deletes the idempotency key from cache. This allows you to fix the issue and retry with the same key. # BYOC configuration essentials Source: https://docs.lerian.studio/en/reference/byoc-configuration Shared environment variables used across Lerian Go services — the deployment-wide backbone operators set when running BYOC in their own cloud or on-prem. In a BYOC (bring your own cloud) deployment, you run Lerian products inside your own AWS, GCP, or on-prem infrastructure and own the data and the runtime. Each service is configured through environment variables, and most of them are service-specific. This page covers the **universal backbone** — the variables that behave the same way across Lerian Go services — so you can set the deployment-wide knobs once and then reach for each product's own page for the rest. This is the shared backbone, not the full list. Variable prefixes differ slightly between services (for example, a service with separate onboarding and transaction databases namespaces them), and every service adds its own keys. See [Per-product variables](#per-product-variables) for the exhaustive lists. ## Deployment mode and TLS `DEPLOYMENT_MODE` sets how strictly the service enforces TLS on its infrastructure connections, and its value is echoed in the [`/readyz`](/en/reference/health-and-readiness) response. | Variable | Description | Default | Allowed values | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | ----------------------- | | `DEPLOYMENT_MODE` | TLS-enforcement posture. `local` allows plaintext connections. `byoc` recommends TLS and warns on plaintext but still boots. `saas` makes TLS mandatory — the service refuses to start if any dependency connection lacks TLS. | `local` | `local`, `byoc`, `saas` | | `ALLOW_INSECURE_TLS` | Bypass per-connection TLS enforcement on infrastructure DSNs. Intended for local development only. | `false` | `true`, `false` | For a production BYOC deployment, set `DEPLOYMENT_MODE=byoc`, connect every datastore over TLS, and leave `ALLOW_INSECURE_TLS` unset (`false`). The `local` defaults ship plaintext connections and are not safe for production. ## Server | Variable | Description | Default | Allowed values | | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------- | | `SERVER_ADDRESS` | Main HTTP listen address (`host:port`, usually `:port`). The liveness, readiness, and version probes bind this same port. | Varies per service | `host:port` | | `LOG_LEVEL` | Log verbosity. Use `info` or higher in production. | `debug` | `debug`, `info`, `warn`, `error` | | `ENV_NAME` | Environment label. In SLC, it is normalized to an allowlist. | `development` | SLC: `production`, `staging`, `uat`, `development`, `local` | | `VERSION` | Service version tag surfaced by `/version`. | Per release | Semver string | Some services expose a numeric `SERVER_PORT` instead of, or alongside, `SERVER_ADDRESS`. Worker components without a primary HTTP API expose a dedicated health port (for example `HEALTH_PORT` or `WORKER_SERVER_PORT`). See [Default network ports](/en/reference/default-network-ports) and [Health and readiness](/en/reference/health-and-readiness). ## Datastores Every service that persists state connects to one or more datastores. The variable prefix depends on the store — and, in some services, on the logical database. The table below shows the common shape; consult each product's page for the exact names. | Variable | Description | Default | Allowed values | | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------ | | `POSTGRES_HOST` / `POSTGRES_PORT` | PostgreSQL host and port. | — / `5432` | Host / port | | `POSTGRES_USER` / `POSTGRES_PASSWORD` | Connection credentials. The password is a secret — inject it at deploy time, never commit it. | — | String / secret | | `POSTGRES_NAME` / `POSTGRES_DB` / `DB_NAME` | Database name. The exact key varies by service — `POSTGRES_NAME` (for example STA, SLC, SISBAJUD, Consignado), `POSTGRES_DB` (SPB, SPI, Boleto), or `DB_NAME` (Pix Indirect); Midaz namespaces it per logical database as `DB_{MODULE}_NAME`. Consult each product's page. | — | String | | `POSTGRES_SSLMODE` | libpq TLS mode. Use `require` or stronger in production. | `disable` | `disable`, `require`, `verify-ca`, `verify-full` | | `POSTGRES_REPLICA_*` | Optional read-replica connection (same shape as the primary). | — | Host / port / credentials | | `MONGO_URI` / `MONGO_HOST` / `MONGO_PORT` | MongoDB connection, as a full URI or discrete host and port. | — | Connection string / host / port | | `MONGO_TLS_CA_CERT` | Base64-encoded PEM CA certificate for TLS (for example, a managed MongoDB service). | — | Base64 PEM | | `REDIS_HOST` / `REDIS_PASSWORD` | Redis/Valkey endpoint and auth password (secret). | — | `host:port` / secret | | `REDIS_TLS` / `REDIS_CA_CERT` | Enable TLS to Redis and supply a base64-encoded PEM CA certificate. In SLC, `REDIS_TLS=true` requires a nonblank `REDIS_CA_CERT`. | `false` / — | `true`, `false` / base64 PEM | | `RABBITMQ_HOST` / `RABBITMQ_URI` | Broker host and connection scheme. Use `amqps` in production. | — / `amqp` | Host / `amqp`, `amqps` | | `RABBITMQ_DEFAULT_USER` / `RABBITMQ_DEFAULT_PASS` | Broker credentials (password is a secret). In SLC, when RabbitMQ is enabled and its URL has no credentials, explicitly declare both `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS`. | — | String / secret | Not every service uses every store, and prefixes vary: core products often namespace connections per logical database (for example `DB_ONBOARDING_*`, `DB_TRANSACTION_*`, `MONGO_CRM_*`), while plugins and rails use the flat `POSTGRES_*` shape above. In multi-tenant mode, static datastore credentials are ignored — connections are resolved per tenant (see below). ## Multi-tenancy Multi-tenancy is **off by default**. When you enable it, every datastore connection switches from static configuration to per-tenant resolution through Tenant Manager, and the service adds a per-tenant readiness probe at `GET /readyz/tenant/{id}`. | Variable | Description | Default | Allowed values | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- | --------------- | | `MULTI_TENANT_ENABLED` | Enable per-tenant connection resolution. When `false`, the service runs single-tenant with the static connections above. | `false` | `true`, `false` | | `MULTI_TENANT_URL` | Tenant Manager API URL. Required when enabled. | — | URL | | `MULTI_TENANT_SERVICE_API_KEY` | API key for Tenant Manager (secret). Required when enabled. | — | Secret | | `MULTI_TENANT_ALLOW_INSECURE_HTTP` | Allow a cleartext `http://` Tenant Manager URL. Never enable in production — credentials travel in plaintext. | `false` | `true`, `false` | | `MULTI_TENANT_REDIS_HOST` / `MULTI_TENANT_REDIS_PORT` | Optional Redis endpoint for tenant lifecycle events (event-driven discovery). | — / `6379` | Host / port | Additional per-tenant pool sizing, circuit-breaker, and cache-TTL knobs (`MULTI_TENANT_MAX_TENANT_POOLS`, `MULTI_TENANT_CIRCUIT_BREAKER_*`, `MULTI_TENANT_CACHE_TTL_SEC`, and others) exist per service. See the per-product pages. ## Runtime configuration | Variable | Description | Default | Allowed values | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------------- | | `SYSTEMPLANE_ENABLED` | Enable the systemplane runtime-configuration admin API on the main port. Off by default in most services; some — for example SPI — enable it by default. Check each service's page. | `false` (SPI: `true`) | `true`, `false` | When enabled, the service exposes an authenticated plane for reading and writing runtime configuration. See [Systemplane](/en/reference/systemplane/overview) for the API, namespaces, and required permissions. ## Streaming and outbox The event-publication path (a lib-streaming producer backed by a transactional outbox) is **off by default** in every service except the Fetcher worker, which sets `STREAMING_ENABLED=true` to emit job-completion events. | Variable | Description | Default | Allowed values | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | --------------------------------------- | | `STREAMING_ENABLED` | Enable the event producer. | `false` (Fetcher worker: `true`) | `true`, `false` | | `STREAMING_BROKERS` | Kafka/RedPanda bootstrap brokers, as a `host:port` comma-separated list. | — | `host:port` CSV | | `STREAMING_CLOUDEVENTS_SOURCE` | The CloudEvents `source` stamped on every event the service emits. It has no default: a producer that streams to a broker with this value empty stops at startup. Several products accept exactly one literal and refuse to boot on any other, so take the value from the product's own page. | — | The literal the product requires | | `STREAMING_COMPRESSION` | Producer compression codec. | `lz4` | `none`, `gzip`, `snappy`, `lz4`, `zstd` | | `STREAMING_REQUIRED_ACKS` | Producer acknowledgement level. | `all` | `all`, `leader`, `none` | | `OUTBOX_ENABLED` | Enable the transactional outbox dispatcher (where the service exposes it separately). | `false` | `true`, `false` | `STREAMING_SASL_*` and `STREAMING_TLS_*` secure the broker connection — set them when your broker requires authentication or TLS. ## Service discovery Consul service discovery is **off by default**. When enabled, the service registers itself and resolves peers through Consul instead of static addresses. | Variable | Description | Default | Allowed values | | --------------------------------------------- | ----------------------------------------------------------------------- | ------- | --------------- | | `SD_ENABLED` | Enable Consul service discovery. | `false` | `true`, `false` | | `SD_ADDRESS` | Consul agent address. Required when enabled. | — | `host:port` | | `SD_EXTERNAL_ADDRESS` / `SD_INTERNAL_ADDRESS` | Advertised addresses. Required when enabled. | — | `host:port` | | `SD_TLS` / `SD_TLS_SKIP_VERIFY` | Enable TLS to Consul, and (separately) bypass certificate verification. | `false` | `true`, `false` | | `SD_TOKEN` | Consul ACL token (secret). | — | Secret | Some services use legacy aliases (`SD_ADVERTISE_*`, `CONSUL_ADDR`) for the same behavior. ## Observability Telemetry is push-based (OTLP). Some services additionally expose a `/metrics` endpoint for Prometheus scraping — see [Health and readiness](/en/reference/health-and-readiness). | Variable | Description | Default | Allowed values | | -------------------------------------- | --------------------------------------------------------------- | --------------- | --------------- | | `ENABLE_TELEMETRY` | Enable OpenTelemetry instrumentation. | `false` | `true`, `false` | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint. Required when telemetry is enabled. | — | URL | | `OTEL_EXPORTER_OTLP_PORT` | OTLP port, when using a discrete host and port. | `4317` | Port | | `OTEL_RESOURCE_SERVICE_NAME` | Service name attached to exported telemetry. | Service default | String | | `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT` | Environment label attached to exported telemetry. | — | String | | `OTEL_INSECURE_EXPORTER` | Allow a plaintext OTLP connection. Leave `false` in production. | `false` | `true`, `false` | ## Plugin authentication Lerian services can authenticate protected routes — including the systemplane admin API — through Access Manager (Casdoor-backed). The authentication toggle, its variable name, and its default differ by service: most plugins and products use `PLUGIN_AUTH_ENABLED` (default `false`, off), while native rails such as SILOC and SPB use `AUTH_ENABLED` (default `true`, on — required in production and SaaS) paired with `AUTH_ADDRESS`. Always enable authentication in production, and check each product or rail's own environment-variables page for the authoritative toggle name, its default, and the routes it protects. | Variable | Description | Default | Allowed values | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | --------------- | | `PLUGIN_AUTH_ENABLED` / `AUTH_ENABLED` | Require Access Manager authentication on protected routes. The name and default vary by service — plugins use `PLUGIN_AUTH_ENABLED`, native rails use `AUTH_ENABLED`. | Plugins `false`, rails `true` | `true`, `false` | | `PLUGIN_AUTH_ADDRESS` / `PLUGIN_AUTH_HOST` / `AUTH_ADDRESS` | Access Manager service address. Required when enabled. | — | Address | ## Per-product variables The variables above are the shared baseline. Each product adds its own — datastore prefixes, integration URLs, worker tuning, and feature toggles. Use the per-product pages for the full, current list: The exhaustive, per-service variable list ships in each service's `.env.example` file. Treat it as the source of truth for a specific release, and never commit real secret values into it. # Default network ports Source: https://docs.lerian.studio/en/reference/default-network-ports Reference the default TCP ports used by Midaz and Lerian plugins so you can open the right firewall rules when deploying in self-hosted environments. These are the default internal ports. If you're using Docker Compose, Kubernetes, or a cloud provider, port mappings may differ. Always verify your configuration before exposing services publicly. ## Core services *** | Service | Port | Name | | :---------------- | :--- | :----------------------- | | Midaz Onboarding | 3000 | midaz\_onboarding | | Midaz Transaction | 3001 | midaz\_transaction | | Midaz Ledger | 3002 | midaz\_ledger | | Transaction gRPC | 3011 | midaz\_transaction\_grpc | | Midaz Console | 8081 | midaz-console | | Matcher | 8080 | matcher | ## Plugin services *** Each plugin runs as an independent service and may expose its own API or background processes. | Plugin | Port | Name | | :------------------------ | :--- | :--------------- | | Access Manager - Auth | 4000 | plugin-auth | | Access Manager - Identity | 4001 | plugin-identity | | Fees Engine | 4002 | plugin-fees | | CRM | 4003 | plugin-crm | | Reporter Manager | 4005 | reporter-manager | For better security, expose only necessary ports (like Console and MCP) and restrict others using firewall rules or internal networking policies. ## Customization *** You can change the default ports using environment variables, Docker Compose files, or Helm values, depending on your setup. If you override a port, make sure to update any references across your infrastructure, load balancers, and API consumers. # Error handling Source: https://docs.lerian.studio/en/reference/error-handling Learn how to interpret, classify, and resolve error responses returned by Lerian APIs, including HTTP status codes and structured error payload fields. When integrating with Lerian APIs, robust error handling is essential for a seamless user experience and resilient application performance. This guide explains how errors are structured, how to classify them, and how to respond to different error categories. ## Error response model *** All Lerian APIs return a structured error object for every error. The format is consistent across all services: ```json theme={null} { "code": "", "title": "", "message": "" } ``` **Field definitions:** * `code`: A unique, stable identifier for the error. Use this field for programmatic error handling. * `title`: A brief summary of the issue. * `message`: Detailed, human-readable guidance for resolving the error. Always use the `code` field to identify errors programmatically. Titles and messages may evolve to improve clarity, but error codes remain stable. ### Field-level error details When an error relates to specific fields in the request payload, the response includes a `fields` object with granular details: ```json Missing required fields theme={null} { "code": "CRM-0003", "title": "Missing Fields in Request", "message": "Your request is missing one or more required fields. Please refer to the documentation to ensure all necessary fields are included in your request.", "fields": { "document": "document is a required field" } } ``` ```json Invalid field values theme={null} { "code": "CRM-0047", "title": "Bad Request", "message": "The server could not understand the request due to malformed syntax. Please check the listed fields and try again.", "fields": { "legalName": "legalName is a required field.", "parentOrganizationId": "parentOrganizationId must be a valid UUID" } } ``` ```json Unexpected fields theme={null} { "code": "CRM-0053", "title": "Unexpected Fields in the Request", "message": "The request body contains more fields than expected. Please send only the allowed fields as per the documentation. The unexpected fields are listed in the fields object.", "fields": { "extraField": "extraField is not allowed" } } ``` ## Error code structure *** Every error code follows a standardized format that identifies both the service and the specific error: ``` - ``` * **PREFIX** (3 letters): Identifies the service or plugin that produced the error. * **NNNN** (4 digits): A unique number within that service. ### Service prefixes | Prefix | Service | | ------- | ------------------------------- | | **AUT** | Access Manager (authentication) | | **IDE** | Access Manager (identity) | | **CRM** | CRM | | **FEE** | Fees Engine | | **Pix** | Pix | | **BTF** | Bank Transfer (TED) | | **TPL** | Reporter | | **TRC** | Tracer | Midaz core uses numeric-only codes (e.g., `0002`, `0009`) without a prefix. All other services include their 3-letter prefix. ### Number ranges Error codes are organized into ranges that indicate the error's origin: | Range | Category | Description | | ------------- | --------------------- | --------------------------------------------------------------- | | `0001`–`0099` | System and middleware | Authentication, authorization, headers, rate limiting | | `0100`–`0999` | Service-specific | Validation, business logic, and domain errors within the plugin | | `1000`–`1999` | External integration | Errors originating from external providers or upstream services | This structure allows you to quickly identify whether an error comes from your request (low numbers), the service's business logic (mid-range), or an external dependency (1000+). ## Error classification *** Understanding the type of error helps you decide how to respond. Lerian errors fall into three categories: ### Validation errors Errors caused by incorrect or missing input in the request. **Characteristics:** * HTTP status `400` (Bad Request) * Include a `fields` object when specific fields are at fault * Always preventable by validating input before sending **Examples:** Missing required fields, invalid UUIDs, unsupported enum values, fields exceeding maximum length. **What to do:** Check the `fields` object for specifics. Correct the input and retry. ### Business logic errors Errors caused by operations that violate domain rules or resource state constraints. **Characteristics:** * HTTP status `404` (Not Found), `409` (Conflict), or `422` (Unprocessable Entity) * Indicate that the request is syntactically valid but cannot be processed **Examples:** Resource not found, duplicate name conflicts, invalid status transitions (e.g., activating an already active rule), insufficient balance. **What to do:** Verify the resource exists and is in the expected state. Check the error message for the specific constraint that was violated. ### System errors Errors caused by infrastructure issues, timeouts, or unexpected failures. **Characteristics:** * HTTP status `500` (Internal Server Error), `502` (Bad Gateway), `503` (Service Unavailable), or `504` (Gateway Timeout) * Not caused by your input — the same request may succeed later **Examples:** Internal server error, service temporarily unavailable, gateway timeout. **What to do:** Retry with exponential backoff (see guidance below). If the error persists, contact support. ## HTTP status codes *** Lerian APIs use a focused set of HTTP status codes: | Code | Meaning | Category | | ----- | --------------------- | ------------------------------------------------------- | | `400` | Bad Request | Validation error — fix the request | | `401` | Unauthorized | Missing or invalid authentication credentials | | `403` | Forbidden | Valid credentials but insufficient permissions | | `404` | Not Found | The requested resource does not exist | | `409` | Conflict | The operation conflicts with the current resource state | | `422` | Unprocessable Entity | Valid syntax but violates business rules | | `429` | Too Many Requests | Rate limit exceeded — wait and retry | | `500` | Internal Server Error | Unexpected server failure — retry later | | `502` | Bad Gateway | Upstream service returned an invalid response | | `503` | Service Unavailable | Service temporarily unavailable — retry later | | `504` | Gateway Timeout | Request timed out — retry later | ## Retry guidance *** Not all errors should be retried. The following table helps you decide: | Error type | Retryable | Recommended action | | ------------------------- | --------- | ----------------------------------------------------------- | | `400` validation errors | No | Fix the request payload | | `401` / `403` auth errors | No | Check credentials and permissions | | `404` not found | No | Verify the resource ID | | `409` conflicts | Sometimes | Check current state, then retry if the conflict is resolved | | `422` business logic | No | Adjust the operation to comply with business rules | | `429` rate limit | Yes | Wait for the retry window, then retry | | `500` internal errors | Yes | Retry with exponential backoff | | `502` / `503` / `504` | Yes | Retry with exponential backoff | ### Exponential backoff strategy For retryable errors, use exponential backoff to avoid overwhelming the service: 1. **First retry**: Wait 1 second 2. **Second retry**: Wait 2 seconds 3. **Third retry**: Wait 4 seconds 4. **Maximum retries**: Stop after 3–5 attempts 5. **Jitter**: Add a small random delay (0–500ms) to each wait to prevent thundering herd Never retry `400`, `401`, `403`, or `422` errors automatically. These indicate issues with your request that must be fixed before retrying. ## Troubleshooting by category *** ### Missing or invalid fields (400) Most `400` errors include a `fields` object that tells you exactly which fields need attention. **Common causes:** * Required field omitted from the request body * Field value does not match the expected type (e.g., string instead of UUID) * Field value exceeds maximum length * Unexpected extra fields in the request body **Resolution steps:** 1. Read the `fields` object in the error response 2. Compare your request against the API reference for that endpoint 3. Verify field names use `lowerCamelCase` (not `snake_case`) 4. Verify dates use ISO 8601 format with `Z` suffix 5. Verify UUIDs are valid v4 format ### Authentication and authorization (401/403) **Common causes:** * Missing `Authorization` header * Expired or revoked token * Token does not grant access to the requested endpoint **Resolution steps:** 1. Confirm that Access Manager is enabled in your environment 2. Verify the token is present in the `Authorization` header 3. Request a new token if the current one has expired 4. Check that the token's scope includes the required permissions ### Resource not found (404) **Common causes:** * Incorrect resource ID in the URL path * Resource was soft-deleted * Resource belongs to a different organization or ledger **Resolution steps:** 1. Verify the ID format (must be a valid UUID) 2. List resources to confirm the ID exists 3. Check that you are using the correct `organizationId` and `ledgerId` path parameters ### Conflict errors (409) **Common causes:** * Creating a resource with a name that already exists (e.g., duplicate ledger name, duplicate rule name) * Attempting an operation that has already been completed (e.g., duplicate transaction) **Resolution steps:** 1. Read the error message to identify which field caused the conflict 2. Use a different value (e.g., rename) or retrieve the existing resource instead 3. For idempotent operations, verify the existing resource matches your intent ### Rate limiting (429) **Common causes:** * Too many requests in a short period **Resolution steps:** 1. Implement exponential backoff with jitter 2. Reduce the frequency of API calls 3. Batch operations where possible ### Server and timeout errors (500/502/503/504) **Common causes:** * Temporary service disruption * High load on the platform * Upstream dependency unavailable **Resolution steps:** 1. Retry with exponential backoff (1s, 2s, 4s) 2. If the error persists after 3–5 retries, contact support 3. Log the full error response (including `code`) for support escalation ## Service-specific error lists *** Each Lerian service publishes a complete list of its error codes. Use these references to look up specific error codes: | Service | Error list | | ------------------- | --------------------------------------------------------------------------------------- | | Midaz | [Midaz error list](/en/reference/midaz/error-list) | | Access Manager | [Access Manager error list](/en/reference/access-manager/access-manager-error-list) | | CRM | [CRM error list](/en/reference/midaz/crm/crm-error-list) | | Fees Engine | [Fees Engine error list](/en/reference/midaz/plugins/fees-engine/fee-engine-error-list) | | Bank Transfer (TED) | [TED error list](/en/reference/midaz/plugins/ted/ted-error-list) | | Reporter | [Reporter error list](/en/reference/reporter/reporter-error-list) | | Tracer | [Tracer error list](/en/reference/tracer/tracer-error-list) | ## Best practices *** ### 1. Use error codes for programmatic handling Error codes are stable identifiers designed for automation. Map specific codes to resolution paths in your integration: ``` if error.code == "TRC-0100": # Rule not found — verify rule ID elif error.code.startswith("TRC-01"): # Rule-related error — check rule configuration elif error.code.startswith("TRC-0"): # Tracer validation error — check request format ``` ### 2. Log errors with context Include the full error response, the request that triggered it, and the timestamp. This makes support escalation faster and debugging more effective. ### 3. Handle field-level errors When the response includes a `fields` object, surface those specific messages to your users rather than showing a generic error. ### 4. Implement circuit breakers for integrations If you receive repeated `500`, `502`, or `503` errors, use a circuit breaker pattern to temporarily stop calling the failing service and prevent cascading failures. ### 5. Stay updated Review the error list pages periodically. New error codes may be added as services evolve. # Consignado events Source: https://docs.lerian.studio/en/reference/events/consignado Reference the domain events emitted and consumed by Lerian Consignado — Dataprev, including payload structure, CloudEvents topics, and delivery semantics. The Lerian Consignado — Dataprev business surface is entirely event-driven. Averbação, detecting an auction (leilão) win, margin reads, and reconciliation all happen by exchanging events over Kafka — none of them has an HTTP endpoint. The only HTTP surface Consignado exposes is credential custody (certificate and OAuth material), documented in the [Consignado API reference](/en/reference/rails/consignado/get-credential-status). Money amounts and rates cross the wire as decimal **strings**, never floats. Every event carries its schema version in the `ce-schemaversion` header and travels in the shared [CloudEvents envelope](/en/reference/events/overview). Every fact carries version `1.0.0` except `consignado_proposal.accepted`, which carries `2.0.0`. Consignado's `ce-source` is `consignado-gw`, which also names its topic namespace (see [Topic naming](/en/reference/events/overview#topic-naming)): emitted events land on `consignado-gw..`, with a `.v2` suffix on the one fact whose schema major is 2. Every fact is **outbox-backed**: it is written in the same database transaction as the state change it reports and relayed to the broker afterwards. ## Events emitted Facts Consignado emits after acting on a command or picking up an input from the rail. Optional fields are marked `?` — an absent key means the rail sent nothing, where an empty string would read as a blank the rail sent. | Event (`ce-type`) | Topic | Fires when | Key payload | | ----------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.consignado_solicitacao.received` | `consignado-gw.consignado_solicitacao.received` | The auction (leilão) discovery poller picked a worker loan request off the rail — relayed so the lender can decide whether to bid. | `id_solicitacao`, `codigo_solicitante`, `cpf`, `matricula`, `nome_trabalhador`, `data_nascimento`, `inscricao_empregador`, `numero_inscricao_empregador`, `data_admissao`, `valor_liberado`, `nro_parcelas`, `margem_disponivel`, `elegivel_emprestimo`?, `data_hora_validade_solicitacao`, `pessoa_exposta_politicamente`?, `garantias` (`tem_garantias` plus three FGTS collateral values), `discovered_at` | | `studio.lerian.consignado_margin.fetched` | `consignado-gw.consignado_margin.fetched` | The rail answered a margin read — Consignado relays the margin Dataprev returned. | `request_ref`, `cpf`, `matricula`, `available_margin`, `remuneracao_disponivel`, `valor_total_vencimentos`?, `elegivel`?, `motivo_inelegibilidade`?, `tipo_bloqueio`?, `qtd_emprestimos_ativos_suspensos`?, `data_desligamento`?, `codigo_motivo_desligamento`?, `percentual_verba_rescisoria_disponivel`?, `as_of` | | `studio.lerian.consignado_afastamento.reported` | `consignado-gw.consignado_afastamento.reported` | A margin read carried a worker-absence alert — one event per alert element, covering both afastamento and desligamento shapes. | `cpf`, `matricula`, `tipo_alerta_codigo`, `codigo_motivo_afastamento`?, `data_referencia`, `data_afastamento`?, `data_termino_afastamento`?, `codigo_motivo_desligamento`?, `data_desligamento`?, `data_aviso_previo`?, `data_fim_aviso_previo`? | | `studio.lerian.consignado_proposal.accepted` | `consignado-gw.consignado_proposal.accepted.v2` | A confirmed averbação named one of this tenant's proposals — the post-averbação witness reads the contract back from the rail and relays what the rail registered, verbatim, so the lender can verify booked terms against an independent source. This fact carries schema `2.0.0` and the `.v2` topic suffix. `matricula` identifies the worker's employment link (with `cpf`); it replaces the former `vinculo_ref`, which named no Dataprev field and always travelled empty. | `proposal_ref`, `id_solicitacao`, `numero_proposta`, `numero_contrato`, `cpf`, `matricula`, `numero_inscricao_empregador`, `principal_amount`, `liberated_amount`, `installment_amount`, `installment_count`, `monthly_rate`, `annual_rate`, `cet_monthly`, `cet_annual`, `iof_amount`, `situacao_emprestimo`?, `situacao_descricao`?, `garantia`? (nested block: `tem_garantias` plus three optional FGTS collateral values; an absent block means the rail was silent), `accepted_at`, plus optional rail-read detail such as worker and employer names, exclusion and portability history, and the FGTS collateral block state | | `studio.lerian.consignado_averbacao.confirmed` | `consignado-gw.consignado_averbacao.confirmed` | The rail accepted the averbação (Dataprev success code). | `contract_id`, `numero_contrato`, `dataprev_success_code`, `averbado_at` | | `studio.lerian.consignado_averbacao.rejected` | `consignado-gw.consignado_averbacao.rejected` | The rail rejected the averbação. | `contract_id`, `numero_contrato`, `error_code`, `error_message` | | `studio.lerian.consignado_contract.registered` | `consignado-gw.consignado_contract.registered` | A terminal averbação completed its booking composition — the complete post-rail booking handoff for lenders and funds. This is the only fact that books a contract downstream; the averbação acknowledgement and the proposal witness must not create or advance a booked contract. | `contract_id`, `numero_contrato`, `numero_proposta`?, `identity` (`cpf`, `matricula`, employer inscription), `declared_terms` (client-declared amounts, rates, dates, and the guarantee gate), `rail_terms` (post-rail registered terms with an optional three-state `garantia` block), `margin_snapshot`, `signature`, `artifacts` digests and blob refs, `registration` (`averbado_at`, `response_code`, attachment hash), `fund_ref`? | | `studio.lerian.consignado_exclusao.confirmed` | `consignado-gw.consignado_exclusao.confirmed` | The rail confirmed an averbação exclusion (cancellation). Both exclusion facts share one payload shape; `fact_id` is deterministic per (tenant, request, outcome), so a subscriber can dedupe replays. | `fact_id`, `tenant_id`, `request_ref`, `settlement_id`, `loan_account_id`, `contract_id`, `status`, `provider_reference`?, `outcome_at` | | `studio.lerian.consignado_exclusao.rejected` | `consignado-gw.consignado_exclusao.rejected` | The rail rejected an averbação exclusion. | `fact_id`, `tenant_id`, `request_ref`, `settlement_id`, `loan_account_id`, `contract_id`, `status`, `reason_code`?, `message`?, `outcome_at` | | `studio.lerian.consignado_reconciliation.received` | `consignado-gw.consignado_reconciliation.received` | The reconciliation poller picked up an input (escrituração, CEF D+2 repasse, or portal CSV) — one event per record: per contract row for escrituração, per registro for repasse. `records` carries that single entry and is omitted for the portal CSV source. | `competencia`, `source_kind`, `object_ref`, `records`. Each record keys on `numero_contrato` and may additionally carry `cpf`? and `matricula`? so a repasse that matches nothing in the consumer's book still names the worker it moved money for. | | `studio.lerian.consignado_employment_status.reported` | `consignado-gw.consignado_employment_status.reported` | The rail reported a change in the worker's employment status. | `report_ref`, `record_id`, `numero_contrato`, `status`, `effective_at`, `reason_code`? | | `studio.lerian.consignado_redirecionamento.confirmed` | `consignado-gw.consignado_redirecionamento.confirmed` | The rail confirmed a payroll-collection redirection to an eligible active vínculo. `numero_contrato_novo` is optional: it travels only when the rail names a new contract. | `request_ref`, `numero_contrato`, `source_vinculo_ref`, `target_vinculo_ref`, `target_matricula`, `target_cnpj`, `target_esocial_category`, `disruption_status`, `effective_at`, `provider_reference`, `numero_contrato_novo`?, `outcome_at` | | `studio.lerian.consignado_redirecionamento.rejected` | `consignado-gw.consignado_redirecionamento.rejected` | The rail rejected a payroll-collection redirection. | `request_ref`, `numero_contrato`, `source_vinculo_ref`, `target_vinculo_ref`, `target_matricula`, `target_cnpj`, `target_esocial_category`, `disruption_status`, `effective_at`, `reason_code`, `outcome_at` | ## Commands consumed Commands Consignado consumes from Lender. It subscribes by topic, under the producer's (`lender`) namespace; the producing service sets the `ce-type` header, so it is not listed here. The averbação command subscription is active only in deployments where averbação is enabled; the margin command is always subscribed. No producer emits the margin command yet: Lender declares `lender.consignado_margin.requested` as a contract reservation (see [Lender events](/en/reference/events/lender)), so the margin round trip, including the `consignado_margin.fetched` and `consignado_afastamento.reported` facts it produces, carries no live traffic until that command ships. | Command | Topic | Key decoded fields | | --------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `consignado.margin.requested` | `lender.consignado_margin.requested` | `contract_ref`, `cpf`, `cnpj`, `matricula`, `esocial_category`, `codigo_inscricao_empregador`, `numero_inscricao_empregador`, plus the digital-consent triple `canal_autorizacao_digital`, `data_hora_autorizacao_digital`, `nsu_autorizacao_digital` — all required on the wire | | `consignado.averbacao.requested` | `lender.consignado_averbacao.requested` | `contract_id`, `numero_contrato`, `cpf`, `matricula`, the full accepted financial terms (`principal_amount`, `liberated_amount`, `installment_amount`, `installment_count`, rates, `iof_amount`, `first_deduction_competencia`), the FGTS collateral block (`tem_garantias` plus values), the signed CCB, and signature evidence | | `consignado.exclusao.requested` | `lender.consignado_exclusao.requested` | `request_ref`, `settlement_id`, `loan_account_id`, `contract_id`, `numero_contrato`, `settled_at` | | `consignado.redirecionamento.requested` | `lender.consignado_redirecionamento.requested` | `request_ref`, `numero_contrato`, `source_vinculo_ref`, `target_vinculo_ref`, `target_matricula`, `target_cnpj`, `target_esocial_category`, `disruption_status`, `effective_at` | # Fetcher events Source: https://docs.lerian.studio/en/reference/events/fetcher Reference the job terminal events emitted by Lerian Fetcher — job.completed and job.failed — with payload structure and delivery semantics. Fetcher's worker emits a terminal event for every extraction job: one **`job.completed`** or one **`job.failed`**, exactly once per job outcome. These notifications are a mandatory product contract — the worker refuses to boot with streaming disabled rather than silently swallowing them. Events travel in the shared [CloudEvents envelope](/en/reference/events/overview), published over **RabbitMQ**: each event routes to the exchange named by `RABBITMQ_JOB_EVENTS_EXCHANGE` with the event key as the AMQP routing key (`job.completed`, `job.failed`). Both events are **outbox-backed**: the outbox row is written durably and a relay publishes it, retrying through broker outages; a repairer re-emits terminal events that never reached the broker. Delivery is at-least-once — deduplicate on `ce-id`, which is deterministic per job outcome: `fetcher.job..`, so every re-emission of the same fact carries the same id. `ce-subject` is the job id; single-tenant deployments stamp `ce-tenantid` as `single-tenant`. Unlike most Lerian payloads, Fetcher's job events use **camelCase** keys — they mirror Fetcher's REST surface. ## Job events | Event (`ce-type`) | Fires when | Key payload | | ----------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.job.completed` | An extraction job finishes successfully. | `jobId`, `status` (`completed`), `metadata` (includes the requesting `source`), `result`? (`path`?, `sizeBytes`?, `rowCount`?, `format`?, `hmac`?, `integrity`?, `protection`?), `executionTimeMs`?, `completedAt`? | | `studio.lerian.job.failed` | An extraction job fails. | `jobId`, `status` (`failed`), `metadata` (includes the requesting `source` and an `error` object with failure details), `executionTimeMs`?, `completedAt`? | The `result` block describes the produced artifact: where it was written, its size and row count, the output format, and — when result protection is enabled — the integrity (HMAC) and protection descriptors a consumer uses to verify the artifact before trusting it. ## Events consumed Fetcher consumes no platform events. Its inbound job requests arrive on an internal RabbitMQ work queue from embedding products (such as Matcher), which is not part of the public event contract. # Lender events Source: https://docs.lerian.studio/en/reference/events/lender Reference the domain events emitted and consumed by Lerian Lender — loan lifecycle, collections, BR jurisdiction, and consignado commands — with payloads and delivery semantics. Lender emits domain events as **CloudEvents 1.0** messages in binary content mode over Kafka, published through `lib-streaming`. Every event travels in the [shared envelope](/en/reference/events/overview): `ce-type` names the event as `studio.lerian..`, `ce-subject` carries the aggregate id, `ce-tenantid` the owning tenant, and `ce-schemaversion` the payload version — `2.0.0` for the `loan_application.*` events, whose topics carry the `.v2` suffix, and `1.0.0` for every other event below. `ce-source` comes from `STREAMING_CLOUDEVENTS_SOURCE`, and Lender **enforces the value `lender`** when streaming is enabled — booting with any other value fails. Topics derive from the source (see [Topic naming](/en/reference/events/overview#topic-naming)), so every emitted event lands on `lender..`. Every event in Lender's catalog is **outbox-backed**: the event row is written in the same database transaction as the state change it reports, and a relay publishes committed rows to Kafka, retrying through broker outages. The catalog does not allow this policy to be weakened per deployment. Money amounts and rates cross the wire as decimal **strings**, never floats. Lender serves its full event catalog at `GET /api/v1/streaming/manifest`. ## Loan lifecycle events | Event (`ce-type`) | Topic | Fires when | Key payload | | --------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.loan_application.submitted` | `lender.loan_application.submitted.v2` | An application is validated and persisted, pending approval. | `loanApplicationId`, `loanProductVersionId`, `borrowerId`, `assignedOfficerId`, `status`, `requestedPrincipalAmount`, `requestedInterestRate`, `requestedInstallments`, `expectedDisbursementDate`, `previewProfileVersion`, `previewJurisdictionCode`, `previewScheduleSnapshotId`, `createdAt`, `updatedAt` | | `studio.lerian.loan_application.approved` | `lender.loan_application.approved.v2` | A pending application is approved. | `loanApplicationId`, `loanProductVersionId`, `borrowerId`, `assignedOfficerId`, `status`, `approvedAmount`, `approvalDecisionId`, `approvalDecisionAt`, `approvedBy`, `requestedPrincipalAmount`, `previewProfileVersion`, `previewJurisdictionCode`, `updatedAt` | | `studio.lerian.loan_application.rejected` | `lender.loan_application.rejected.v2` | A pending application is rejected. | As `approved`, with `rejectionDecisionId`, `rejectionDecisionAt`, `rejectedBy` | | `studio.lerian.loan_application.withdrawn` | `lender.loan_application.withdrawn.v2` | A pending application is withdrawn. | As `approved`, with `withdrawalDecisionId`, `withdrawalDecisionAt`, `withdrawnBy` | | `studio.lerian.loan_application.disbursed` | `lender.loan_application.disbursed.v2` | An approved application is disbursed and the active loan is created. | `loanApplicationId`, `loanProductVersionId`, `borrowerId`, `assignedOfficerId`, `status`, `loanAccountId`, `disbursementEventId`, `disbursementTransactionId`, `grossRequestedAmount`, `netDeliveredAmount`, `disbursedAt`, `profileVersion`, `jurisdictionExtensions`?, `previewJurisdictionCode`, `updatedAt` | | `studio.lerian.loan_product.created` | `lender.loan_product.created` | A loan product is persisted as a draft. | `loan_product_id`, `name`, `loan_type`, `status`, `jurisdiction_code`, `current_version_id`?, `created_at` | | `studio.lerian.loan_product.activated` | `lender.loan_product.activated` | A product transitions to active, pinned to a version snapshot. | `loan_product_id`, `current_version_id`, `status`, `name`, `loan_type`, `jurisdiction_code`, `created_at` | | `studio.lerian.loan_product_version.created` | `lender.loan_product_version.created` | Immutable product-version terms are appended. | `loan_product_version_id`, `loan_product_id`, `jurisdiction_code`, `jurisdictionExtensions`?, `rate_mode`, `floating_rate_table_id`?, `floating_spread_bps`, `fixed_annual_rate_bps`, `requires_floating_rate`, `created_at` | | `studio.lerian.loan_charge.applied` | `lender.loan_charge.applied` | A product-version charge template is persisted as an immutable applied charge on a loan account. | `request_id`, `loan_account_id`, `source_product_version_id`, `charge_template_id`, `charge_code`, `charge_type`, `amount`, `rate`, `currency`, `account_created_at`, `assessed_at` | | `studio.lerian.accounting_profile.configured` | `lender.accounting_profile.configured` | A tenant's product-version accounting profile and posting rules are durably configured. | `profile_id`, `loan_product_version_id`, `accounting_mode`, `posting_rules` (each entry: `event_type` and `legs` with `account`, `role`?, `side`, `component`?, `optional`), `created_at` | ## Servicing events | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------------------------ | ----------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.repayment.recorded` | `lender.repayment.recorded` | A repayment is durably recorded with its cash allocation. | `transaction_id`, `loan_account_id`, `request_id`, `paid_amount`, `overpayment_amount`, `effective_date`, `created_at`, `allocation_count`, `allocations` (each entry: `installment_number`, `due_date`, `principal_amount`, `interest_amount`, `fees_amount`, `penalties_amount`, `total_amount`, `fully_paid`) | | `studio.lerian.repayment_reversal.recorded` | `lender.repayment_reversal.recorded` | A repayment reversal is recorded as a compensating transaction with lineage. | As `repayment.recorded`, plus `original_transaction_id`, `profile_version`, `jurisdiction_code` | | `studio.lerian.loan_schedule.prepayment_applied` | `lender.loan_schedule.prepayment_applied` | A prepayment produces a successor schedule version. | `loan_account_id`, `schedule_version_id`, `previous_schedule_version_id`, `version_number`, `reason`, `trigger_transaction_id`, `request_id`, `payload_hash`, `prepayment_amount`, `effective_date`, `business_date`, `profile_version`, `jurisdiction_code`, `created_at`, `installment_count` | | `studio.lerian.loan_schedule.rescheduled` | `lender.loan_schedule.rescheduled` | A reschedule produces a successor schedule version. | As `prepayment_applied` minus `prepayment_amount`, plus `first_rescheduled_due_date` | ## Collection events The four collection-payment events share one payload schema; optional fields fill in per flow. | Event (`ce-type`) | Topic | Fires when | Key payload | | -------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.collection_payment.applied` | `lender.collection_payment.applied` | A verified collection payment is conserved and applied to one loan financial path. | `application_id`, `notification_id`, `instrument_id`, `provider`, `provider_account_id`, `provider_payment_id`, `received_amount`, `applied_amount`, `unapplied_amount`, `refunded_amount`?, `currency`, `applied_transaction_id`?, `repair_reason`?, `refund_request_id`?, `refund_transaction_id`? | | `studio.lerian.collection_payment.unapplied` | `lender.collection_payment.unapplied` | A payment is conserved as unapplied cash with a bounded repair reason. | Same schema | | `studio.lerian.collection_payment.reapplied` | `lender.collection_payment.reapplied` | Unapplied cash is released into receivables. | Same schema | | `studio.lerian.collection_payment.refunded` | `lender.collection_payment.refunded` | Unapplied cash is refunded through a provider request. | Same schema | ## BR jurisdiction events | Event (`ce-type`) | Topic | Fires when | Key payload | | --------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.loan_account.pdd_stage_transitioned` | `lender.loan_account.pdd_stage_transitioned` | A BR PDD delinquency stage transition or cure is persisted, with accrual eligibility. | `loan_account_id`, `transition_id`, `from_stage`, `to_stage`, `accrual_state`, `reason`?, `effective_at`, `business_date`, `triggered_by`?, `profile_version`?, `jurisdiction_code`?, `created_at` | | `studio.lerian.prepayment_quote.created` | `lender.prepayment_quote.created` | An immutable BR prepayment quote is created, with rebate and IOF reconciliation facts. | `quote_id`, `loan_account_id`, `quote_type`, `principal_outstanding`, `interest_rebate`, `charge_rebate`, `iof_reconciliation`, `gross_amount`, `net_settlement_amount`, `rebate_mandatory`, `expires_at`, `currency`, `sla_due_at`, `statement_available_at`, `instrument_id`, `instrument_type`, `instrument_requested_at`, `profile_version`?, `jurisdiction_code`?, `consumer_protection_regime`?, `created_at` | | `studio.lerian.prepayment_settlement.recorded` | `lender.prepayment_settlement.recorded` | An accepted prepayment quote is settled; the final breakdown is persisted. `final_interest_amount` is a disclosure-only forgiven-interest figure — never sum it into cash movements. | `settlement_id`, `quote_id`, `loan_account_id`, `transaction_id`, `schedule_version_id`?, `settlement_type`, `principal_outstanding`, `interest_rebate`, `charge_rebate`, `iof_reconciliation`, `gross_amount`, `net_settlement_amount`, `final_principal_amount`, `final_interest_amount`, `final_charge_amount`, `final_iof_amount`, `profile_version`?, `jurisdiction_code`?, `consumer_protection_regime`?, `accepted_at`, `created_at` | | `studio.lerian.payroll_deduction.refund_required` | `lender.payroll_deduction.refund_required` | Confirmed post-payoff payroll cash is recorded as requiring a borrower refund. | `receipt_id`, `account_id`, `product_version_id`, `payoff_effective_at`, `receipt_settled_at`, `amount`, `currency`, `required_at` | | `studio.lerian.guarantee_recovery.cash_allocated` | `lender.guarantee_recovery.cash_allocated` | One confirmed guarantee-recovery cash receipt is allocated with cash conservation. | `receipt_id`, `loan_account_id`, `disruption_ref`, `source_sequence`, `cash_source`, `amount`, `repayment_applied`, `prepayment_applied`, `unapplied`, `currency`, `settled_at` | ## Consignado commands emitted Commands Lender sends to the Consignado rail. They keep Lender's namespace — Consignado subscribes to these `lender.*` topics (see [Consignado events](/en/reference/events/consignado)). | Command (`ce-type`) | Topic | Fires when | Key payload | | ----------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.consignado_averbacao.requested` | `lender.consignado_averbacao.requested` | Active contracting asks the rail to averbar a signed contract. | `contract_id`, `numero_contrato`, `cpf`, `matricula`, worker and employer identification, the full accepted financial terms (`principal_amount`, `liberated_amount`, `installment_amount`, `installment_count`, rates, `iof_amount`, `first_deduction_competencia`), the FGTS collateral block, the signed CCB, and signature evidence | | `studio.lerian.consignado_exclusao.requested` | `lender.consignado_exclusao.requested` | A full payoff triggers exclusion of the averbação. | `request_ref`, `settlement_id`, `loan_account_id`, `contract_id`, `numero_contrato`, `settled_at` | | `studio.lerian.consignado_redirecionamento.requested` | `lender.consignado_redirecionamento.requested` | Employment-disruption resolution requests a payroll-collection redirection to an eligible vínculo. | `request_ref`, `numero_contrato`, `source_vinculo_ref`, `target_vinculo_ref`, `target_matricula`, `target_cnpj`, `target_esocial_category`, `disruption_status`, `effective_at` | `studio.lerian.consignado_margin.requested` (topic `lender.consignado_margin.requested`) is declared in the catalog and in the manifest, but no Lender flow emits it yet — treat it as a contract reservation, not live traffic. Consignado's fact events (`studio.lerian.consignado_proposal.accepted`, `studio.lerian.consignado_averbacao.confirmed`, and the rest) also appear in Lender's manifest for contract documentation, but their producer is the Consignado rail — see the [Consignado events](/en/reference/events/consignado) page for those payloads. ## Events consumed Consumers are opt-in per deployment: each has an enable flag (default off) and fails at boot when enabled without a reachable broker. | Topic (producer) | What Lender does with it | | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `consignado-gw.consignado_averbacao.confirmed` (Consignado) | Confirms the averbação on the application and converges disbursement. | | `consignado-gw.consignado_averbacao.rejected` (Consignado) | Applies the averbação rejection to the application and contract. | | `consignado-gw.consignado_employment_status.reported` (Consignado) | Feeds employment-disruption processing. | | `consignado-gw.consignado_exclusao.confirmed` and `.rejected` (Consignado) | Applies the terminal exclusion outcome to the exclusion flow; a repair command can re-emit `studio.lerian.consignado_exclusao.requested`. | | `consignado-gw.consignado_redirecionamento.confirmed` and `.rejected` (Consignado) | Applies the redirection outcome to employment-disruption state. | | `consignado-gw.consignado_reconciliation.received` (Consignado) | Drives the reconciliation orchestrator for a competência (escrituração and repasse ingestion). | | `matcher.match_run.completed` (Matcher) | Translates a completed reconciliation match run into PDD stage transitions, arrears and collection issuance, and settle or refund posting intents. | Two Consignado facts are documented contracts, not live Lender consumers. `consignado_proposal.accepted` now travels on `consignado-gw.consignado_proposal.accepted.v2` with schema `2.0.0`, as a post-averbação witness rather than a booking input (see [Consignado events](/en/reference/events/consignado)); Lender's handler and manifest still pin the unsuffixed `consignado-gw.consignado_proposal.accepted` topic with schema `1.0.0`, which no producer emits, and the consumer is **not wired into any deployment**, so this post-averbação witness flow is not live end to end. `consignado-gw.consignado_contract.registered`, the only fact that books a contract downstream, has a durable-inbox handler implemented, but its consumer cannot be enabled yet: Lender fails at boot until the booking processor ships. Check the streaming manifest of your deployment before depending on either fact. # Matcher events Source: https://docs.lerian.studio/en/reference/events/matcher Reference the domain events emitted by Lerian Matcher — reconciliation contexts, match runs, exceptions, disputes, and governance — with payloads and delivery semantics. Matcher emits domain events as **CloudEvents 1.0** messages in binary content mode over Kafka, published through `lib-streaming`. Every event travels in the [shared envelope](/en/reference/events/overview): `ce-type` names the event as `studio.lerian..`, `ce-subject` carries the aggregate id, `ce-tenantid` the owning tenant, and `ce-schemaversion` the payload version — `1.0.0` for every event below. `ce-source` comes from `STREAMING_CLOUDEVENTS_SOURCE` and is required when streaming is enabled; deployments conventionally set `matcher`, so topics land on `matcher..` (see [Topic naming](/en/reference/events/overview#topic-naming)). Money amounts — fee values, adjustment amounts — cross the wire as decimal **strings**, never floats. Matcher serves its full event catalog at `GET /system/matcher/streaming/manifest`. This page covers the Kafka streaming plane. Matcher's exception **webhook** dispatch — HTTP callbacks for exception routing — is a separate surface, documented in [Webhooks and callbacks](/en/matcher/integrations/matcher-webhooks-callbacks). ## Delivery policies Matcher's catalog uses two delivery policies: * **Outbox-backed** events are written to the outbox in the same database transaction as the state change; a relay publishes committed rows and retries through broker outages. These are the audit-grade facts (matching outcomes, exception resolutions, disputes, governance). This policy cannot be weakened per deployment. * **Direct** events publish after the transaction commits, best-effort, falling back to the outbox only when the broker circuit is open. These are configuration and operational lifecycle signals. Each table below states the policy for its events. ## Matching events Outbox-backed: `transaction.matched`, `transaction.pending_review`. Direct: the rest. | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.transaction.matched` | `matcher.transaction.matched` | A transaction line reaches terminal MATCHED in a match-run commit — one event per matched line. | `transaction_id`, `context_id`, `match_run_id`, `previous_status`, `status`, `match_group_id`?, `candidate_match_group_id`?, `source_id`?, `matched_at`? | | `studio.lerian.transaction.pending_review` | `matcher.transaction.pending_review` | A candidate match needs human review (non-automatic rule). | Same schema, with `pending_review_at`? | | `studio.lerian.transaction.ignored` | `matcher.transaction.ignored` | A transaction is excluded from matching. | `transaction_id`, `ingestion_job_id`, `context_id`, `source_id`, `previous_status`, `status`, `extraction_status`, `updated_at` | | `studio.lerian.match_run.completed` | `matcher.match_run.completed` | A match run finishes. Lender consumes this event to translate reconciliation verdicts into loan servicing actions. | `match_run_id`, `context_id`, `mode`, `status`, `stats`, `started_at`, `completed_at`? | | `studio.lerian.match_run.failed` | `matcher.match_run.failed` | A match run fails. | As `completed`, plus `failure_reason` | | `studio.lerian.match_group.confirmed` | `matcher.match_group.confirmed` | A match group is confirmed. | `match_group_id`, `match_run_id`, `context_id`, `rule_id`, `transaction_ids`, `confidence`, `status`, `confirmed_at`? | | `studio.lerian.match_group.unmatched` | `matcher.match_group.unmatched` | A confirmed match group is undone. | As `confirmed`, plus `previous_status`, `reason`, `unmatched_at` | | `studio.lerian.fee_variance.created` | `matcher.fee_variance.created` | A fee-aware rule detects a variance between expected and actual fees. | `fee_variance_id`, `context_id`, `match_run_id`, `match_group_id`, `transaction_id`, `fee_schedule_id`, `fee_schedule_name_snapshot`, `currency`, `expected_fee`, `actual_fee`, `delta`, `variance_type`, `created_at` | ## Exception and dispute events Outbox-backed except `exception.assigned` and the comment events, which are direct. | Event (`ce-type`) | Topic | Fires when | Key payload | | ----------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `studio.lerian.exception.assigned` | `matcher.exception.assigned` | An exception is assigned to an operator. | `exception_id`, `status`, `version`, `assigned_at` | | `studio.lerian.exception.resolved` | `matcher.exception.resolved` | An exception is resolved. | `exception_id`, `status`, `version`, `resolution_type`?, `transaction_id`?, `resolved_at` | | `studio.lerian.exception.force_match_resolved` | `matcher.exception.force_match_resolved` | An exception is resolved by force-match with an override reason. | Base fields plus `resolution_type` (`FORCE_MATCH`), `override_reason_code`, `resolved_at` | | `studio.lerian.exception.adjust_entry_resolved` | `matcher.exception.adjust_entry_resolved` | An exception is resolved by an adjustment entry. | Base fields plus `resolution_type` (`ADJUST_ENTRY`), `reason_code`, `amount`, `currency`, `resolved_at` | | `studio.lerian.exception.dispatched` | `matcher.exception.dispatched` | An exception is dispatched to an external system. | `exception_id`, `target_system`, `queue`, `external_reference`, `acknowledged`, `dispatched_at` | | `studio.lerian.exception.callback_processed` | `matcher.exception.callback_processed` | An external system's callback is applied to an exception. | Base fields plus `external_system`, `external_issue_id`, `callback_type`, `processed_at` | | `studio.lerian.exception_comment.added` | `matcher.exception_comment.added` | A comment is added to an exception thread. | `comment_id`, `exception_id`, `created_at` | | `studio.lerian.exception_comment.deleted` | `matcher.exception_comment.deleted` | A comment is deleted. | `comment_id`, `exception_id`, `actor`, `deleted_at` | | `studio.lerian.dispute.opened` | `matcher.dispute.opened` | A dispute is opened on an exception. | `dispute_id`, `exception_id`, `state`, `resolution`?, `category`, `opened_at` | | `studio.lerian.dispute.won` | `matcher.dispute.won` | A dispute closes as won. | As `opened`, plus `closed_at` | | `studio.lerian.dispute.lost` | `matcher.dispute.lost` | A dispute closes as lost. | As `opened`, plus `closed_at` | | `studio.lerian.evidence.submitted` | `matcher.evidence.submitted` | Evidence is attached to a dispute. | `evidence_id`, `dispute_id`, `exception_id`, `has_file`, `submitted_at` | ## Configuration and data-intake events All direct. | Event (`ce-type`) | Topic | Fires when | Key payload | | ---------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.reconciliation_context.created` | `matcher.reconciliation_context.created` | A reconciliation context is created. | `context_id`, `name`, `context_type`, `interval`, `status`, `auto_match_on_upload`, `created_at`, `tenant_id` | | `studio.lerian.reconciliation_context.updated` | `matcher.reconciliation_context.updated` | A context's metadata or lifecycle status changes. | Same schema, with `updated_at` | | `studio.lerian.reconciliation_source.created` | `matcher.reconciliation_source.created` | A source (input side) is created in a context. | `context_id`, `source_id`, `name`, `source_type`, `side`, `created_at` | | `studio.lerian.match_rule.created` | `matcher.match_rule.created` | A match rule is created. | `context_id`, `rule_id`, `rule_type`, `priority`, `config_hash`, `created_at` | | `studio.lerian.match_rule.reordered` | `matcher.match_rule.reordered` | Rule priorities are reordered. | `context_id`, `ordered_rule_ids`, `priority_version`, `reordered_at` | | `studio.lerian.fetcher_connection.synced` | `matcher.fetcher_connection.synced` | A Fetcher connection and its discovered schema snapshot are synchronized. | `connection_id`, `fetcher_connection_id`, `config_name`, `database_type`, `status`, `schema_discovered`, `last_seen_at`, `updated_at` | | `studio.lerian.fetcher_connection.unreachable` | `matcher.fetcher_connection.unreachable` | A Fetcher connection becomes unreachable. | Same schema, plus `previous_status` | | `studio.lerian.extraction_request.created` | `matcher.extraction_request.created` | An extraction request is created against a connection. | `extraction_request_id`, `connection_id`, `status`, `table_count`, `has_filters`, `start_date`, `end_date`, `created_at` | | `studio.lerian.ingestion.completed` | `matcher.ingestion.completed` | An ingestion job completes. | `job_id`, `context_id`, `source_id`, `status`, `total_rows`, `failed_rows`, `transaction_count`, `date_range_start`, `date_range_end`, `completed_at` | | `studio.lerian.ingestion.failed` | `matcher.ingestion.failed` | An ingestion job fails. | `job_id`, `context_id`, `source_id`, `status`, `total_rows`, `failed_rows`, `error_code`, `failed_at` | ## Governance and reporting events Outbox-backed except the export-job family, which is direct. | Event (`ce-type`) | Topic | Fires when | Key payload | | ---------------------------------------- | ---------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.audit_log.created` | `matcher.audit_log.created` | A hash-chained audit log entry is persisted. | `audit_log_id`, `tenant_id`, `entity_type`, `entity_id`, `action`, `tenant_seq`, `hash_version`, `record_hash`, `created_at` | | `studio.lerian.archive_metadata.created` | `matcher.archive_metadata.created` | The archival worker registers an archive partition. | `archive_metadata_id`, `tenant_id`, `partition_name`, `date_range_start`, `date_range_end`, `status`, `created_at`, `updated_at`, plus `checksum`?, `row_count`?, `compressed_size_bytes`?, `storage_class`?, `archived_at`? | | `studio.lerian.archive.uploaded` | `matcher.archive.uploaded` | An archive is uploaded to storage. | Same schema | | `studio.lerian.archive.completed` | `matcher.archive.completed` | An archival cycle completes. | Same schema | | `studio.lerian.actor.pseudonymized` | `matcher.actor.pseudonymized` | An actor's personal data is pseudonymized (GDPR/LGPD). | `actor_id`, `pseudonymized`, `display_name_status`, `email_status`, `updated_at`, `tenant_id` | | `studio.lerian.export_job.created` | `matcher.export_job.created` | An export job is created. | `export_job_id`, `tenant_id`, `context_id`, `report_type`, `format`, `status`, `schema_version`, `created_at`, `expires_at`, `updated_at` | | `studio.lerian.export_job.succeeded` | `matcher.export_job.succeeded` | An export job finishes writing its artifact. | Same schema, plus `file_name`?, `sha256`?, `records_written`?, `bytes_written`?, `attempts`?, `finished_at`? | | `studio.lerian.export_job.failed` | `matcher.export_job.failed` | An export job fails. | Same schema, plus `error_code`, `attempts`?, `finished_at`? | | `studio.lerian.export_job.expired` | `matcher.export_job.expired` | An export artifact expires and is cleaned up. | Same schema, plus `expired_at`? | ## Declared but not yet emitted The catalog and manifest declare seven additional events that **no code path emits today**: `reconciliation_context.deleted` and the `extraction_request` lifecycle family (`submitted`, `completed`, `failed`, `cancelled`, `bridged`, `bridge_failed`). They are contract reservations — do not build consumers that depend on receiving them. ## Events consumed Matcher consumes **no Kafka streaming events**. Its integration inputs arrive over HTTP: file uploads, Fetcher extractions, and — when enabled — Streaming Hub webhook deliveries on `POST /v1/discovery/hub/events` (HMAC-signed, deduplicated by event id, disabled by default). # Midaz events Source: https://docs.lerian.studio/en/reference/events/midaz Reference the domain events emitted by Midaz ledger, Fees, and CRM services — event topics, CloudEvents payloads, and versioning for downstream consumers. Midaz emits domain events as **CloudEvents 1.0** messages in binary content mode over Kafka, published through `lib-streaming`. Every event travels in the [shared envelope](/en/reference/events/overview): `ce-type` names the event as `studio.lerian..`, `ce-subject` carries the aggregate id, `ce-tenantid` the owning tenant (absent in single-tenant scope), and `ce-schemaversion` the payload version — `1.0.0` for every event below. `ce-source` is deployment-configured through the `STREAMING_CLOUDEVENTS_SOURCE` environment variable and has **no default**: when streaming is enabled, the ledger fails to boot if it is unset. Topics do not derive from the source — Midaz routes each event to an explicit topic that folds the owning service segment into the first token (`lerian.streaming.ledger_.` for the ledger core; the CRM and Fees capabilities below use `crm_` and `fee_`) and normalizes hyphens to underscores in the topic tail. The `ce-type` keeps its hyphens (for example, `balance.config-changed` lands on topic `lerian.streaming.ledger_balance.config_changed` but keeps `ce-type` `studio.lerian.balance.config-changed`). Money amounts — balances, transaction and operation amounts — cross the wire as decimal **strings**, never floats. ## Ledger events Facts the ledger emits as organizations, accounts, balances, and transactions change. ### Organization | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------------ | ---------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------- | | `studio.lerian.organization.created` | `lerian.streaming.ledger_organization.created` | An organization is created. | `id`, `legalName`, `doingBusinessAs`, `legalDocument`, `address`, `status`, `createdAt` | | `studio.lerian.organization.updated` | `lerian.streaming.ledger_organization.updated` | An organization's mutable fields change. | `id`, `parentOrganizationId`, `legalName`, `doingBusinessAs`, `address`, `status`, `updatedAt` | | `studio.lerian.organization.deleted` | `lerian.streaming.ledger_organization.deleted` | An organization is deleted. | `id`, `deletedAt` | ### Ledger | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------ | ---------------------------------------- | ------------------------------------------- | ----------------------------------------------------- | | `studio.lerian.ledger.created` | `lerian.streaming.ledger_ledger.created` | A ledger is created within an organization. | `id`, `organizationId`, `name`, `status`, `createdAt` | | `studio.lerian.ledger.updated` | `lerian.streaming.ledger_ledger.updated` | A ledger's name or status changes. | `id`, `organizationId`, `name`, `status`, `updatedAt` | | `studio.lerian.ledger.deleted` | `lerian.streaming.ledger_ledger.deleted` | A ledger is deleted. | `id`, `organizationId`, `deletedAt` | ### Account | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------- | ----------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.account.created` | `lerian.streaming.ledger_account.created` | An account is opened in a ledger. | `id`, `organizationId`, `ledgerId`, `name`, `assetCode`, `type`, `alias`, `portfolioId`, `segmentId`, `status`, `createdAt` | | `studio.lerian.account.updated` | `lerian.streaming.ledger_account.updated` | An account's mutable fields change. | `id`, `organizationId`, `ledgerId`, `name`, `segmentId`, `entityId`, `status`, `blocked`, `updatedAt` | | `studio.lerian.account.deleted` | `lerian.streaming.ledger_account.deleted` | An account is deleted. | `id`, `organizationId`, `ledgerId`, `portfolioId`, `deletedAt` | ### Asset | Event (`ce-type`) | Topic | Fires when | Key payload | | ----------------------------- | --------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------- | | `studio.lerian.asset.created` | `lerian.streaming.ledger_asset.created` | An asset is registered in a ledger. | `id`, `organizationId`, `ledgerId`, `name`, `type`, `code`, `status`, `createdAt` | | `studio.lerian.asset.updated` | `lerian.streaming.ledger_asset.updated` | An asset's mutable fields change. | `id`, `organizationId`, `ledgerId`, `name`, `type`, `code`, `status`, `updatedAt` | | `studio.lerian.asset.deleted` | `lerian.streaming.ledger_asset.deleted` | An asset is deleted. | `id`, `organizationId`, `ledgerId`, `deletedAt` | ### Portfolio | Event (`ce-type`) | Topic | Fires when | Key payload | | --------------------------------- | ------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------- | | `studio.lerian.portfolio.created` | `lerian.streaming.ledger_portfolio.created` | A portfolio is created. | `id`, `organizationId`, `ledgerId`, `name`, `entityId`, `status`, `createdAt` | | `studio.lerian.portfolio.updated` | `lerian.streaming.ledger_portfolio.updated` | A portfolio's mutable fields change. | `id`, `organizationId`, `ledgerId`, `name`, `entityId`, `status`, `updatedAt` | | `studio.lerian.portfolio.deleted` | `lerian.streaming.ledger_portfolio.deleted` | A portfolio is deleted. | `id`, `organizationId`, `ledgerId`, `deletedAt` | ### Segment | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------- | ----------------------------------------- | ---------------------------------- | ----------------------------------------------------------------- | | `studio.lerian.segment.created` | `lerian.streaming.ledger_segment.created` | A segment is created. | `id`, `organizationId`, `ledgerId`, `name`, `status`, `createdAt` | | `studio.lerian.segment.updated` | `lerian.streaming.ledger_segment.updated` | A segment's mutable fields change. | `id`, `organizationId`, `ledgerId`, `name`, `status`, `updatedAt` | | `studio.lerian.segment.deleted` | `lerian.streaming.ledger_segment.deleted` | A segment is deleted. | `id`, `organizationId`, `ledgerId`, `deletedAt` | ### Operation route | Event (`ce-type`) | Topic | Fires when | Key payload | | --------------------------------------- | ------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `studio.lerian.operation-route.created` | `lerian.streaming.ledger_operation_route.created` | An operation route is created. | `id`, `organizationId`, `ledgerId`, `title`, `code`, `operationType`, `account`, `accountingEntries`, `createdAt` | | `studio.lerian.operation-route.updated` | `lerian.streaming.ledger_operation_route.updated` | An operation route's mutable fields change. | `id`, `organizationId`, `ledgerId`, `title`, `code`, `operationType`, `account`, `accountingEntries`, `updatedAt` | | `studio.lerian.operation-route.deleted` | `lerian.streaming.ledger_operation_route.deleted` | An operation route is deleted. | `id`, `organizationId`, `ledgerId`, `deletedAt` | ### Transaction route | Event (`ce-type`) | Topic | Fires when | Key payload | | ----------------------------------------- | --------------------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- | | `studio.lerian.transaction-route.created` | `lerian.streaming.ledger_transaction_route.created` | A transaction route is created. | `id`, `organizationId`, `ledgerId`, `title`, `operationRouteIds`, `createdAt` | | `studio.lerian.transaction-route.updated` | `lerian.streaming.ledger_transaction_route.updated` | A transaction route's mutable fields change. | `id`, `organizationId`, `ledgerId`, `title`, `operationRouteIds`, `updatedAt` | | `studio.lerian.transaction-route.deleted` | `lerian.streaming.ledger_transaction_route.deleted` | A transaction route is deleted. | `id`, `organizationId`, `ledgerId`, `deletedAt` | ### Balance | Event (`ce-type`) | Topic | Fires when | Key payload | | ----------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `studio.lerian.balance.created` | `lerian.streaming.ledger_balance.created` | A balance is created during account setup. | `id`, `organizationId`, `ledgerId`, `accountId`, `alias`, `key`, `assetCode`, `accountType`, `available`, `onHold`, `allowSending`, `allowReceiving`, `createdAt` | | `studio.lerian.balance.changed` | `lerian.streaming.ledger_balance.changed` | A committed transaction moves a balance — one event per balance-affecting operation; values reflect the state after the operation. | `organizationId`, `ledgerId`, `accountId`, `balanceId`, `assetCode`, `available`, `onHold`, `version`, `reason`, `direction`, `amount`, `transactionId`, `operationId`, `occurredAt` | | `studio.lerian.balance.config-changed` | `lerian.streaming.ledger_balance.config_changed` | A balance's settings change, or overdraft is enabled (companion balance materialized). `changeType` discriminates `settings_updated` from `overdraft_enabled`. | `id`, `organizationId`, `ledgerId`, `accountId`, `allowSending`, `allowReceiving`, `direction`, `settings`, `changeType`, `updatedAt` | | `studio.lerian.balance.deleted` | `lerian.streaming.ledger_balance.deleted` | A balance is deleted. | `id`, `organizationId`, `ledgerId`, `accountId`, `deletedAt` | | `studio.lerian.balance.overdraft-drawn` | `lerian.streaming.ledger_balance.overdraft_drawn` | A debit consumes overdraft beyond available funds (`action` = `drawn`). | `balanceId`, `accountId`, `organizationId`, `ledgerId`, `assetCode`, `transactionId`, `operationId`, `action`, `amount`, `overdraftBalance`, `overdraftLimit`, `occurredAt` | | `studio.lerian.balance.overdraft-repaid` | `lerian.streaming.ledger_balance.overdraft_repaid` | Overdraft usage decreases but is not fully cleared (`action` = `repaid`). | `balanceId`, `accountId`, `organizationId`, `ledgerId`, `assetCode`, `transactionId`, `operationId`, `action`, `amount`, `overdraftBalance`, `overdraftLimit`, `occurredAt` | | `studio.lerian.balance.overdraft-cleared` | `lerian.streaming.ledger_balance.overdraft_cleared` | Overdraft is fully repaid — terminal signal (`action` = `cleared`). | `balanceId`, `accountId`, `organizationId`, `ledgerId`, `assetCode`, `transactionId`, `operationId`, `action`, `amount`, `overdraftBalance`, `overdraftLimit`, `occurredAt` | ### Transaction The four lifecycle events share one payload schema; optional fields fill in per flow — `route` and `routeId` when the transaction was routed, `parentTransactionId` only on `reverted`. | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.transaction.posted` | `lerian.streaming.ledger_transaction.posted` | A newly created transaction is committed to storage with all its operations persisted (no parent). | `id`, `organizationId`, `ledgerId`, `status`, `amount`, `assetCode`, `source`, `destination`, `route`, `operations`, `metadata`, `createdAt` | | `studio.lerian.transaction.committed` | `lerian.streaming.ledger_transaction.committed` | A pending transaction transitions from `PENDING` to `APPROVED`. | `id`, `organizationId`, `ledgerId`, `status`, `amount`, `assetCode`, `source`, `destination`, `operations`, `metadata` | | `studio.lerian.transaction.canceled` | `lerian.streaming.ledger_transaction.canceled` | A pending transaction transitions from `PENDING` to `CANCELED`. | `id`, `organizationId`, `ledgerId`, `status`, `amount`, `assetCode`, `source`, `destination`, `operations`, `metadata` | | `studio.lerian.transaction.reverted` | `lerian.streaming.ledger_transaction.reverted` | A revert flow creates a child transaction; `parentTransactionId` correlates it to the original. | `id`, `parentTransactionId`, `organizationId`, `ledgerId`, `status`, `amount`, `assetCode`, `source`, `destination`, `operations`, `metadata` | ## Fees events Midaz's Fees capability manages fee and billing packages and records when fees are applied to a transaction. Its events are emitted by the consolidated ledger service, so they share the ledger's `ce-source` — but they keep their own topic segment, landing under `lerian.streaming.fee_*`. Fees resources carry the `fee-` domain prefix in their keys and `ce-type`s; the topic fold strips that prefix before adding the `fee_` service segment, so `fee-packages` lands on `fee_packages` — never `fee_fee_packages` — unlike CRM, whose resource names carry no domain prefix. | Event (`ce-type`) | Topic | Fires when | Key payload | | -------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `studio.lerian.fee-packages.created` | `lerian.streaming.fee_packages.created` | A fee package is created. | `id`, `organizationId`, `ledgerId`, `segmentId`, `transactionRoute`, `enable`, `createdAt`, `updatedAt` | | `studio.lerian.fee-packages.updated` | `lerian.streaming.fee_packages.updated` | A fee package's mutable fields change. | `id`, `organizationId`, `ledgerId`, `segmentId`, `transactionRoute`, `enable`, `createdAt`, `updatedAt` | | `studio.lerian.fee-packages.deleted` | `lerian.streaming.fee_packages.deleted` | A fee package is deleted. | `id`, `organizationId`, `ledgerId`, `deletedAt` | | `studio.lerian.fee-billing-packages.created` | `lerian.streaming.fee_billing_packages.created` | A billing package is created. | `id`, `organizationId`, `ledgerId`, `type`, `pricingModel`, `countMode`, `enable`, `createdAt`, `updatedAt` | | `studio.lerian.fee-billing-packages.updated` | `lerian.streaming.fee_billing_packages.updated` | A billing package's mutable fields change. | `id`, `organizationId`, `ledgerId`, `type`, `pricingModel`, `countMode`, `enable`, `createdAt`, `updatedAt` | | `studio.lerian.fee-billing-packages.deleted` | `lerian.streaming.fee_billing_packages.deleted` | A billing package is deleted. | `id`, `organizationId`, `ledgerId`, `deletedAt` | | `studio.lerian.fee-charge.applied` | `lerian.streaming.fee_charge.applied` | A fee package is applied to a committed transaction. | `transactionId`, `organizationId`, `ledgerId`, `feePackageId`, `appliedAt` | ## CRM events Midaz's CRM capability manages account holders and their instruments. Its events are emitted by the consolidated ledger service: they share the ledger's `ce-source` but keep their own topic segment, landing under `lerian.streaming.crm_*`, and they follow the same envelope and versioning as every other Midaz event. ### Holder | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------ | ------------------------------------- | --------------------------------- | --------------------------------------------------------- | | `studio.lerian.holder.created` | `lerian.streaming.crm_holder.created` | A holder is created. | `id`, `organizationId`, `type`, `externalId`, `createdAt` | | `studio.lerian.holder.updated` | `lerian.streaming.crm_holder.updated` | A holder's mutable fields change. | `id`, `organizationId`, `type`, `externalId`, `updatedAt` | | `studio.lerian.holder.deleted` | `lerian.streaming.crm_holder.deleted` | A holder is deleted. | `id`, `organizationId`, `deletionType`, `deletedAt` | ### Instrument An instrument is a regulated entity, so document, banking details, and regulatory fields are PII and never cross the wire — only stable identifiers, the org/holder/ledger/account scope, the type classification, and a reduced related-party list (each entry carries just `relatedPartyId` and a non-PII `role`). | Event (`ce-type`) | Topic | Fires when | Key payload | | ------------------------------------------------ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `studio.lerian.instrument.created` | `lerian.streaming.crm_instrument.created` | An instrument is created for a holder and account. | `id`, `holderId`, `organizationId`, `ledgerId`, `accountId`, `type`, `relatedParties`, `createdAt` | | `studio.lerian.instrument.updated` | `lerian.streaming.crm_instrument.updated` | An instrument's mutable fields change. | `id`, `holderId`, `organizationId`, `ledgerId`, `accountId`, `type`, `relatedParties`, `updatedAt` | | `studio.lerian.instrument.deleted` | `lerian.streaming.crm_instrument.deleted` | An instrument is deleted. | `id`, `holderId`, `organizationId`, `deletionType`, `deletedAt` | | `studio.lerian.instrument.related-party-deleted` | `lerian.streaming.crm_instrument.related_party_deleted` | A single related party is removed from an instrument (`ce-subject` is the instrument id). | `instrumentId`, `holderId`, `organizationId`, `relatedPartyId`, `deletedAt` | # Event streaming overview Source: https://docs.lerian.studio/en/reference/events/overview Shared event streaming contract for Lerian products — the CloudEvents envelope, topic and type conventions, schema versioning, and delivery guarantees. Lerian products emit domain events — past-tense business facts such as an account being created or a credential being issued — onto a shared streaming backbone. Any service or downstream subscriber consumes them without coupling to the producer's internal APIs. This page describes the wire contract every event on the platform follows. The per-product pages list the concrete events each system emits and consumes. [Streaming Hub](/en/streaming-hub/what-is-streaming-hub) delivers these same events to your own infrastructure — webhooks, SQS, RabbitMQ, EventBridge, or pull — with managed retries. ## Transport Events travel as **CloudEvents 1.0** messages in **binary content mode**. Binary mode puts the CloudEvents context attributes in transport headers, each prefixed with `ce-`, and the event body in the message value as JSON. A consumer reads routing and identity from the headers without deserializing the payload. Most producers — Midaz, Tracer, Lender, Matcher, Consignado — publish over **Kafka**. Two publish the same envelope over **RabbitMQ**: [Reporter](/en/reference/events/reporter) routes each event to a configured exchange with the event key as the routing key, and [Fetcher](/en/reference/events/fetcher) does the same for its job terminal events. The envelope, typing, versioning, and delivery semantics below apply identically on both transports. ## The envelope Every record carries these CloudEvents headers. | Header | Present | Carries | | -------------------- | -------- | ---------------------------------------------------- | | `ce-specversion` | Always | CloudEvents spec version — `1.0`. | | `ce-id` | Always | Unique event id (UUIDv7). Deduplicate on this value. | | `ce-source` | Always | The producing service. | | `ce-type` | Always | Event type — `studio.lerian..`. | | `ce-time` | Always | Emission timestamp (RFC 3339). | | `ce-resourcetype` | Always | The resource — for example `account`. | | `ce-eventtype` | Always | The event — for example `created`. | | `ce-schemaversion` | Always | Payload schema version. | | `ce-subject` | When set | The aggregate id the event concerns. | | `ce-tenantid` | When set | The owning tenant; omitted for single-tenant scope. | | `ce-datacontenttype` | When set | Body media type — `application/json`. | ## Event type The `ce-type` header names the event as `studio.lerian..`. An account creation in the ledger is `studio.lerian.account.created`. The two segments also appear on their own in `ce-resourcetype` and `ce-eventtype`, so a consumer filters on either the full type or its parts. ## Topic naming Kafka topic names are **not shared across producers** — there is no single platform-wide prefix. Each producer owns its own topic namespace, and a name follows one of two patterns depending on how the producer routes its events. The per-product pages list the exact topic for every event; the rules below let you predict the shape and know which producer a record came from. ### Explicit topics (Midaz) Midaz routes each event to an explicit destination it owns: * The **ledger core** publishes under `lerian.streaming.ledger_.`. * The **CRM and Fees capabilities** — emitted by the same consolidated ledger service, sharing its `ce-source` — keep their own segments: `lerian.streaming.crm_.` and `lerian.streaming.fee_.`. * **Tracer** publishes under `lerian.streaming.tracer_.`. Midaz normalizes hyphens to underscores in the topic tail so every segment matches `[a-z0-9_]`: `balance.config-changed` lands on `lerian.streaming.ledger_balance.config_changed`, while its `ce-type` keeps the hyphen (`studio.lerian.balance.config-changed`). The topic tail is the only place the underscore form appears — the event identity carried in `ce-type`, `ce-resourcetype`, and `ce-eventtype` is unchanged. ### Source-derived topics (Lender, Matcher, Consignado, cross-product commands) Other producers derive the topic from their CloudEvents source: ``` .. ``` Lender emits under `lender..`, Matcher under `matcher..`, and Consignado under `consignado-gw..`. A command one product sends to another keeps the **producer's** namespace, not the consumer's — a command Consignado consumes from Lender arrives on `lender..`. The source segment is lowercased and any character outside `[a-z0-9._-]` is folded to a hyphen before it is used, so pick a source value that stays unique after that normalization. A derived topic carries no version suffix for a `1.x` schema; a **major** schema bump (`2.0.0` and above) appends `.v` to the topic — for example `...event.v2` — so a breaking payload change moves consumers to a new topic instead of reinterpreting the old one. Midaz's explicit topics are fixed literals and never carry a version suffix — a breaking schema change on an explicit topic is signalled only by `ce-schemaversion`, not by the topic name. ## Source `ce-source` identifies the producing service. It is **deployment-configured** through the `STREAMING_CLOUDEVENTS_SOURCE` environment variable. The ledger (including CRM and Fees), Consignado, Matcher, Reporter, and Fetcher **require** it: with streaming enabled, they fail to boot when it is unset rather than emitting under a guessed source. Lender goes further and enforces the exact value `lender`. Tracer instead ships an in-code default (`lerian.midaz.tracer`) and treats the variable as an override. Set a stable, descriptive value per deployment. `ce-source` records where a record originated, for auditing and consumer-side routing. For producers that derive their topics from it (see [Topic naming](#topic-naming)), the source also determines the topic namespace, so its value is part of the wire contract, not just metadata. Midaz routes to explicit topics instead, so its source appears on `ce-source` for attribution but does not shape the topic name. ## Subject and tenant `ce-subject` carries the id of the aggregate the event is about — the account, transaction, or credential the fact concerns. `ce-tenantid` carries the owning tenant in multi-tenant deployments; it is omitted for single-tenant business events, so a consumer treats an absent tenant id as valid single-tenant scope rather than an error. ## Schema versioning Each event declares its own payload schema version in `ce-schemaversion`, independent of other events on the same source. The default is `1.0.0`. A minor bump is additive and backward compatible; a major bump is a breaking change. The version lives in the header, never in the topic name, so a consumer that reads payloads as a [tolerant reader](/en/reference/tolerant-reader) — ignoring unknown fields — is unaffected by an additive change. ## Delivery guarantees Delivery is **at-least-once**. A consumer commits its position only after it finishes handling a record, so a crash mid-handling replays the record rather than dropping it — which means the same event can arrive more than once. Deduplicate on `ce-id` and keep handlers idempotent. On the producer side, the delivery policy belongs to each event definition, not to the platform. A product that declares an event **outbox-backed** writes it to its outbox in the same database transaction as the state change that produced it. The event and the fact it reports commit or roll back together. A relay then publishes committed outbox rows to the producer's transport (Kafka or RabbitMQ) and retries through broker outages. Lender, Consignado, and Fetcher declare every event in their catalogs this way; Matcher and Reporter split their catalogs — audit-grade facts are outbox-backed, operational signals publish directly with an outbox fallback. Read the product's own event page for the policy its catalog carries, and where the product exposes a streaming manifest, read the manifest at startup. ## Per-product catalogs | Producer | Page | Transport | Streaming manifest | | ------------------------- | ---------------------------------------------------- | --------- | ------------------------------------------------------------------------- | | Midaz (ledger, CRM, Fees) | [Midaz events](/en/reference/events/midaz) | Kafka | — | | Tracer | [Tracer events](/en/reference/events/tracer) | Kafka | — | | Lender | [Lender events](/en/reference/events/lender) | Kafka | `GET /api/v1/streaming/manifest` | | Matcher | [Matcher events](/en/reference/events/matcher) | Kafka | `GET /system/matcher/streaming/manifest` | | Consignado | [Consignado events](/en/reference/events/consignado) | Kafka | — | | Reporter | [Reporter events](/en/reference/events/reporter) | RabbitMQ | [`GET /v1/streaming/events`](/en/reference/reporter/get-streaming-events) | | Fetcher | [Fetcher events](/en/reference/events/fetcher) | RabbitMQ | — | The Brazil rails (STA, CCS, SLC, SPB, SPI, SILOC, SISBAJUD, bank transfer, and the Pix services) also publish events on this same contract; see each rail's own documentation area for its catalog. # Reporter events Source: https://docs.lerian.studio/en/reference/events/reporter Reference the domain events emitted by Lerian Reporter — template, report, and deadline lifecycle — with full payloads, topics, and delivery semantics. Reporter emits domain events as **CloudEvents 1.0** messages in binary content mode. Unlike Midaz and the other Kafka producers, Reporter publishes over **RabbitMQ**: every event is routed to the exchange named by `RABBITMQ_REPORT_EVENTS_EXCHANGE` with the event key as the AMQP routing key (`report.finished` routes as `report.finished`). The envelope is identical to the rest of the platform — see the [shared envelope](/en/reference/events/overview) — and the `topic` value in the manifest is a logical identifier derived from the source, not a broker destination. Every event carries `ce-schemaversion` `1.0.0`. Payload keys are `snake_case`. Fields marked `?` are optional or `null`-able. Emission is post-commit and never fails the underlying work. **Critical** events are outbox-backed — written to a durable outbox and replayed through broker outages; **Important** events publish directly, falling back to the outbox when the broker circuit opens. For concepts, the manifest operation, and consumer guidance, read the [Reporter events guide](/en/reporter/reporter-events); the machine-readable catalog is served at [`GET /v1/streaming/events`](/en/reference/reporter/get-streaming-events). ## Template events All Important. | Event (`ce-type`) | Fires when | Key payload | | -------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.template.created` | A template is uploaded and persisted. | `template_id`, `output_format`, `description`, `file_name`, `mapped_datasource_keys`, `mapped_fields_count`, `created_at` | | `studio.lerian.template.updated` | A template's file or metadata changes. | `template_id`, `output_format`, `description`, `updated_fields`, `file_replaced`, `updated_at` | | `studio.lerian.template.deleted` | A template is deleted, cascading its deadlines. | `template_id`, `hard_delete`, `cascaded_deadlines_deleted`, `deleted_at` | ## Report events `report.requested` is Important; the three terminal events are Critical. | Event (`ce-type`) | Fires when | Key payload | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.report.requested` | A report request is accepted and durably queued (status `Processing`), before the worker picks it up. | `report_id`, `template_id`, `output_format`, `status`, `request_id`, `filter_datasource_count`, `template_description` | | `studio.lerian.report.finished` | Every data section succeeded and the artifact is stored. | `report_id`, `template_id`, `output_format`, `status`, `artifact_object_key`, `artifact_content_type`, `artifact_ttl`?, `completed_at`, `duration_ms`, `section_count` | | `studio.lerian.report.partial` | At least one section failed but an artifact exists. `section_failures` carries canonical error codes only. | `report_id`, `template_id`, `output_format`, `status`, `artifact_object_key`, `artifact_content_type`, `section_failures`, `completed_at`, `duration_ms`, `section_count`, `failed_section_count` | | `studio.lerian.report.errored` | The report ends in error — either the worker dispatch failed or generation failed. `error_code` is a fixed vocabulary (`report_generation_failed`, `report_generation_timeout`, `report_generation_canceled`); raw error text never travels. | `report_id`, `template_id`?, `output_format`?, `status`, `error_code`, `error_summary`, `completed_at`, `duration_ms`? | `report.errored` can originate from the manager (dispatch failure) or the worker (generation failure). Both share the deterministic `ce-id` `reporter.report.error.`, so duplicates collapse at the consumer. ## Deadline events `deadline.delivered` and `deadline.delivery_reverted` are Critical; the rest are Important. | Event (`ce-type`) | Fires when | Key payload | | ------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.deadline.created` | A regulatory deadline is created. | `deadline_id`, `name`, `type`, `template_id`?, `template_name`, `due_date`, `frequency`, `months_of_year`, `active`, `notify_days_before`, `color`, `status_snapshot`, `created_at` | | `studio.lerian.deadline.updated` | A deadline changes. | `deadline_id`, `updated_fields`, `template_id`?, `template_name`, `due_date`, `frequency`, `months_of_year`, `active`, `status_snapshot`, `updated_at` | | `studio.lerian.deadline.deleted` | A deadline is soft-deleted. | `deadline_id`, `soft_delete`, `deleted_at` | | `studio.lerian.deadline.delivered` | A deadline is marked delivered. `next_occurrence` is `null` for one-off deadlines. | `deadline_id`, `template_id`?, `template_name`, `type`, `due_date`, `delivered_at`?, `status_snapshot`, `frequency`, `next_occurrence`?, `updated_at` | | `studio.lerian.deadline.delivery_reverted` | A delivery mark is cleared. | `deadline_id`, `template_id`?, `template_name`, `type`, `due_date`, `reverted_at`, `previous_delivered_at`?, `status_snapshot`, `frequency`, `updated_at` | ## Events consumed Reporter consumes no platform events — this channel is publish-only. The manager-to-worker report generation traffic runs on an internal RabbitMQ work queue that is not part of the public event contract. # Tracer events Source: https://docs.lerian.studio/en/reference/events/tracer Reference the domain events emitted by Tracer for spending rules and limits, including payload structure, topics, and downstream consumption patterns. Tracer emits domain events as **CloudEvents 1.0** messages in binary content mode over Kafka, published through `lib-streaming`. Every event travels in the [shared envelope](/en/reference/events/overview): `ce-type` names the event as `studio.lerian..`, `ce-subject` carries the aggregate id (the rule or limit), `ce-tenantid` the owning tenant (absent in single-tenant scope), and `ce-schemaversion` the payload version — `1.0.0` for every event below. `ce-source` defaults to `lerian.midaz.tracer`; the `STREAMING_CLOUDEVENTS_SOURCE` environment variable overrides it when set — unlike the ledger and Consignado, Tracer never fails to boot over an unset source. Topics do not derive from the source — Tracer routes each event to an explicit topic of the form `lerian.streaming.tracer_.`, folding the producing-service segment into the topic name like every other Midaz service (for example, `lerian.streaming.tracer_rule.created`). The rule and limit payloads carry only identity, status, and lifecycle timestamps. The rule expression, name, description, and the limit's amounts are configuration detail and never cross the wire. ## Rule events Facts Tracer emits as a spending rule moves through its lifecycle. | Event (`ce-type`) | Topic | Fires when | Key payload | | -------------------------------- | ------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | `studio.lerian.rule.created` | `lerian.streaming.tracer_rule.created` | A rule is created. | `id`, `status`, `action`, `scopes`, `createdAt`, `updatedAt` | | `studio.lerian.rule.updated` | `lerian.streaming.tracer_rule.updated` | A rule's mutable fields change. | `id`, `status`, `action`, `scopes`, `createdAt`, `updatedAt` | | `studio.lerian.rule.activated` | `lerian.streaming.tracer_rule.activated` | A rule transitions to active and starts evaluating traffic. | `id`, `status`, `activatedAt`, `updatedAt` | | `studio.lerian.rule.deactivated` | `lerian.streaming.tracer_rule.deactivated` | A rule transitions to inactive and stops evaluating. | `id`, `status`, `deactivatedAt`, `updatedAt` | | `studio.lerian.rule.drafted` | `lerian.streaming.tracer_rule.drafted` | A rule is saved as a draft (status `DRAFT`), not yet active. | `id`, `status`, `updatedAt` | | `studio.lerian.rule.deleted` | `lerian.streaming.tracer_rule.deleted` | A rule is deleted. | `id`, `deletedAt` | ## Limit events Facts Tracer emits as a spending limit moves through its lifecycle. | Event (`ce-type`) | Topic | Fires when | Key payload | | --------------------------------- | ------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `studio.lerian.limit.created` | `lerian.streaming.tracer_limit.created` | A limit is created. | `id`, `status`, `limitType`, `currency`, `scopes`, `activeTimeStart`, `activeTimeEnd`, `customStartDate`, `customEndDate`, `resetAt`, `createdAt`, `updatedAt` | | `studio.lerian.limit.updated` | `lerian.streaming.tracer_limit.updated` | A limit's mutable fields change. | `id`, `status`, `limitType`, `currency`, `scopes`, `activeTimeStart`, `activeTimeEnd`, `customStartDate`, `customEndDate`, `resetAt`, `createdAt`, `updatedAt` | | `studio.lerian.limit.activated` | `lerian.streaming.tracer_limit.activated` | A limit transitions to active. | `id`, `status`, `updatedAt` | | `studio.lerian.limit.deactivated` | `lerian.streaming.tracer_limit.deactivated` | A limit transitions to inactive. | `id`, `status`, `updatedAt` | | `studio.lerian.limit.drafted` | `lerian.streaming.tracer_limit.drafted` | A limit is saved as a draft (status `DRAFT`), not yet active. | `id`, `status`, `updatedAt` | | `studio.lerian.limit.deleted` | `lerian.streaming.tracer_limit.deleted` | A limit is deleted. | `id`, `deletedAt` | # Health and readiness Source: https://docs.lerian.studio/en/reference/health-and-readiness Reference the /health, /readyz, and /version probe contract exposed by every Lerian service, with response shapes and startup and shutdown behavior. Most deployable Lerian services expose the same three operational HTTP endpoints — `/health`, `/readyz`, and `/version` — on their main application port. Orchestrators such as Kubernetes use them to decide when a service is alive, when it can receive traffic, and which build is running. This is the standard probe contract, not a guarantee for every service: some components — workers and sidecars — expose only a subset. The [per-service coverage table](#per-service-coverage) below is authoritative for exceptions. ## The probe endpoints | Endpoint | Method | Purpose | Status | Auth | | ---------- | ------ | ------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------- | | `/health` | GET | **Liveness.** Confirms the process is up. Returns `200` with body `healthy`. | `200` | Public (before auth) | | `/readyz` | GET | **Readiness.** Confirms every dependency is reachable. Returns `200` when ready, `503` when any dependency is down or degraded. | `200` / `503` | Public (before auth) | | `/version` | GET | **Build info.** Returns the running version and build metadata. | `200` | Public (before auth) | The spellings are exactly `/health` and `/readyz` — not `/healthz` or `/livez`. All three probes are registered on the main application port, **before** the authentication middleware (they are public probes), and are excluded from access logs and request tracing. ## The `/readyz` response body `/readyz` returns a JSON document describing overall readiness and each dependency check. ```json theme={null} { "status": "healthy", "checks": { "postgres": { "status": "up", "latency_ms": 2, "tls": true }, "redis": { "status": "skipped", "reason": "not configured" }, "rabbitmq": { "status": "degraded", "breaker_state": "half-open", "latency_ms": 12 } }, "version": "1.2.3", "deployment_mode": "byoc" } ``` | Field | Meaning | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `status` | Overall readiness: `healthy` or `unhealthy`. | | `checks` | One entry per dependency (Postgres, MongoDB, Redis, RabbitMQ, systemplane, and others). | | `checks..status` | Per-dependency result: `up`, `down`, `degraded`, `skipped`, or `n/a`. | | `checks..latency_ms` | Round-trip probe latency in milliseconds, when measured. | | `checks..tls` | Whether the connection to that dependency uses TLS. | | `checks..breaker_state` | Circuit-breaker state, when a breaker fronts the dependency. | | `checks..error` / `.reason` | Failure detail or the reason a check was skipped. Sanitized in `saas` and `byoc`; verbose in `local`. | | `version` | Running service version. | | `deployment_mode` | The active [`DEPLOYMENT_MODE`](/en/reference/byoc-configuration#deployment-mode-and-tls): `local`, `saas`, or `byoc`. | **Status vocabulary** is a closed set: * Overall `status`: `healthy` or `unhealthy`. * Per-check `status`: `up`, `down`, `degraded`, `skipped`, `n/a`. `/readyz` returns HTTP `200` only when overall status is `healthy`. If **any** check is `down` or `degraded`, it returns HTTP `503`. ## Startup and shutdown behavior The probes are wired so an orchestrator never routes traffic to a service that cannot serve it. * **Startup self-probe.** `/readyz` returns `503` ("server not ready") until the listener is up and dependencies are reachable — so a starting pod is not added to a load balancer prematurely, even if `/health` already answers `200`. * **Graceful drain.** On `SIGTERM`, the service flips `/readyz` to `503` for a drain window (about 12 seconds) while `/health` stays `200`. Orchestrators stop routing new traffic during the window, then the process exits once in-flight work drains. The window is tunable via `READYZ_DRAIN_DELAY_SEC` (some services use `READYZ_DRAIN_GRACE_SECONDS`). * **Deployment mode.** The active `DEPLOYMENT_MODE` is echoed in the `/readyz` body. In `saas` mode, a dependency reached without TLS fails the readiness (and boot) check; in `byoc` it is recommended but not enforced. ## Multi-tenant readiness When [multi-tenancy](/en/reference/byoc-configuration#multi-tenancy) is enabled, the service adds an auth-gated per-tenant readiness probe: ``` GET /readyz/tenant/{id} ``` It runs the readiness checks against a single tenant's resolved connections. The global `/readyz` reports tenant-scoped checks as `n/a` and points to the per-tenant route. ## Per-service coverage Every service below exposes `/health` (liveness) and `/readyz` (readiness) on its main port. The table lists default ports, the multi-tenant probe where it applies, and the two path deviations. | Product / service | Main port | Liveness | Readiness | Multi-tenant readyz | Notes | | ---------------------------------- | ------------------------------ | ------------------------- | -------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Midaz ledger | 3002 | `/health` | `/readyz` | — | Also `/version`. No `/metrics` (OTLP push). | | Tracer | 4020 | `/health` | `/readyz` | — | `/version`, `/metrics`. Optional reservation gRPC seam (off by default). | | Reporter (API) | 4005 | `/health` | `/readyz` | — | `/version`. Runs when `RUN_MODE=api`. | | Reporter (worker) | 4006 | `/health` | `/readyz` | — | Dedicated worker health server (`RUN_MODE=worker`). | | Flowker (API) | 4021 | `/health` | `/readyz` | — | `/metrics`, `/version`. | | Flowker (worker) | 4022 | `/health` | `/readyz` | — | Worker health port. | | Flowker (XSD-validator sidecar) | 8081 | `/health` | — | — | Liveness only. | | Fetcher (manager) | 4006 | `/health` | `/readyz` | Yes | `/metrics`. | | Fetcher (worker) | 4007 | `/health` | `/readyz` | Yes | `/metrics`. Dedicated worker health server. | | Matcher | 4018 | `/health` | `/readyz` | — | `/version`. Exposes the [systemplane admin API](/en/reference/systemplane/overview). | | Lender | 8080 | `/health` | `/readyz` | — | `/version`. Webhook receivers expose their own `/health` and `/readyz`. Systemplane admin API. | | Access Manager | 4000 (auth), 8000 (authorizer) | `/health` | `/readyz` | — | Two servers, each with its own probes. | | Fees | 4002 | `/health` | `/readyz` | — | Part of Midaz; deployable independently. | | Pix — Direct, via JD | 8080 | `/health` | `/readyz` | Yes | `/metrics`, `/version`. Systemplane admin API. | | Pix — Indirect, via BTG | 4014 | `/health` | `/readyz` (+ `/ready`) | — | **Deviation:** the API additionally answers `/ready` alongside `/readyz`. Multi-component (API plus workers). | | Pix switch | Varies (dedicated component) | `/health` | `/readyz` | — | Per adapter; systemplane runs as a separate deployable component. | | TED — via JD | 4027 | `/health`, `/health/live` | `/readyz`, `/health/ready` | — | **Deviation:** additionally exposes `/health/live` and `/health/ready`. Systemplane admin API. | | Boleto and bill payments — via BTG | 8080 | `/health` | `/readyz` | Yes | Multi-component. | | CCS | 4030 (HTTP), 7001 (gRPC) | `/health` | `/readyz` | Yes | `/metrics`, `/version`. Systemplane admin API. | | STA | 4028 | `/health` | `/readyz` | Yes | `/metrics`, `/version`. Systemplane admin API. | | SISBAJUD | 4029 | `/health` | `/readyz` | Yes | `/metrics`, `/version`. Systemplane admin API. | | SLC | 4111 | `/health` | `/readyz` | Yes | `/metrics`, `/version`. Systemplane admin API. | | SPI | 8080 | `/health` | `/readyz` | — | Systemplane admin API. | | SPB | Varies | `/health` | `/readyz` | — | Systemplane admin API. | | SILOC | Varies | `/health` | `/readyz` | — | — | | Consignado | 8080 | `/health` | `/readyz` | Yes | `/metrics`, `/version`. Systemplane admin API. | | SPB file exchange (BC Correios) | 9090 | `/health` (Live) | `/readyz` (Ready) | — | **Deviation:** `/health` maps to the liveness (Live) handler and `/readyz` to the readiness (Ready) handler. Systemplane admin API (catalog). | **Multi-tenant readyz** is marked `Yes` where the service registers `GET /readyz/tenant/{id}`; it appears when multi-tenancy is enabled. A `—` means no dedicated per-tenant probe is registered for that service. **Ports** are compose/`.env.example` defaults and can be overridden per deployment — see [Default network ports](/en/reference/default-network-ports). `Varies` marks a service whose default port depends on deployment configuration. # Cancel a Pending Transaction Source: https://docs.lerian.studio/en/reference/midaz/cancel-a-pending-transaction en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/cancel Use this endpoint to cancel a pending transaction that was previously created and placed on hold. Only transactions in PENDING status can be cancelled; cancelling a transaction in any other status returns 422 (ErrCommitTransactionNotPending, code 0099). # Commit a Pending Transaction Source: https://docs.lerian.studio/en/reference/midaz/commit-a-pending-transaction en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/commit Use this endpoint to commit a pending transaction that was previously created and placed on hold. Only transactions in PENDING status can be committed; committing a transaction in any other status returns 422 (ErrCommitTransactionNotPending, code 0099). # Create a Balance Source: https://docs.lerian.studio/en/reference/midaz/create-a-balance en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id}/balances Use this endpoint to add new balances to an account. # Create a Block Transaction Source: https://docs.lerian.studio/en/reference/midaz/create-a-block-transaction en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/block Use this endpoint to create a transaction whose resulting operations are typed `BLOCK`. Midaz is agnostic about the business reason for blocking funds — use the `metadata` field to record it. This endpoint always creates an immediately-posted, non-pending transaction: the `pending` field of the request body is ignored (overridden to `false`) — block transactions are never pending. The endpoint accepts the same body as the JSON create endpoint. # Create a Holder Source: https://docs.lerian.studio/en/reference/midaz/create-a-holder en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/holders # Create a Ledger Source: https://docs.lerian.studio/en/reference/midaz/create-a-ledger en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers Use this endpoint to create a Ledger. # Create a Metadata Index Source: https://docs.lerian.studio/en/reference/midaz/create-a-metadata-index en/openapi/v3-current/ledger.yaml post /v1/settings/metadata-indexes/entities/{entity_name} Use this endpoint to create a metadata index for a specific entity type. Metadata indexes improve query performance when filtering or searching by custom metadata fields. # Create a Portfolio Source: https://docs.lerian.studio/en/reference/midaz/create-a-portfolio en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/portfolios Use this endpoint to create a Portfolio. # Create a Segment Source: https://docs.lerian.studio/en/reference/midaz/create-a-segment en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/segments Use this endpoint to create a Segment. # Create a Transaction Annotation Source: https://docs.lerian.studio/en/reference/midaz/create-a-transaction-annotation en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/annotation Use this endpoint to create a transaction with no financial impact, to include operations in the ledger, but without impacting account balances. **Note:** the `pending` field is accepted in the request body but ignored for annotations — an annotation's status is always set to `NOTED`. # Create a Transaction using JSON Source: https://docs.lerian.studio/en/reference/midaz/create-a-transaction-using-json en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/json Use this endpoint to create a transaction using JSON. **Transaction lifecycle:** the transaction is persisted with an initial status of `CREATED` and is promoted to `APPROVED` asynchronously after balance processing completes. The `CREATED` state is transient; clients should treat it as in-progress and not assume final settlement until the status reaches `APPROVED`. # Create an Account Source: https://docs.lerian.studio/en/reference/midaz/create-an-account en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts Use this endpoint to create an Account in Midaz. You can create as many accounts as necessary to structure your ledger, but each request creates only one account at a time. **Important:** Before creating an Account, ensure the corresponding Asset already exists. Use the [Create an Asset](/en/reference/midaz/create-an-asset) endpoint to register it if needed. If you have previously defined Account Types, make sure they are properly configured before account creation. We recommend either recreating existing accounts or updating them to include the correct type before enabling the Account Type validation feature — this helps prevent validation errors and ensures compliance with your accounting structure. **Note:** the `type` value `external` is reserved for system-managed external accounts and is rejected on input. # Create an Account Type Source: https://docs.lerian.studio/en/reference/midaz/create-an-account-type en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/account-types Use this endpoint to create an Account Type. You can create multiple Account Types, but they must be added one at a time. # Create an Asset Source: https://docs.lerian.studio/en/reference/midaz/create-an-asset en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/assets Use this endpoint to create the Asset that will be used with the transactions. You can create multiple Assets. However, the Assets must be created one at a time, with each Asset requiring specific decimal place settings and other individual parameters to be defined during the creation process. **Asset code:** must consist of **uppercase letters only** (A-Z). This applies to all asset types, not just currencies; codes containing digits or symbols are rejected. **Side effect:** creating an Asset automatically provisions a dedicated external account (alias `@external/`, type `external`) together with its default balance. These system-managed resources back external (off-ledger) movements for the asset. # Create an Inflow Transaction Source: https://docs.lerian.studio/en/reference/midaz/create-an-inflow-transaction en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/inflow Use this endpoint to register incoming funds from external sources into the ledger. You only need to provide the destination accounts—based on the selected asset, the system automatically assigns the appropriate external account as the transaction source, simplifying the inflow process. # Create an Operation Route Source: https://docs.lerian.studio/en/reference/midaz/create-an-operation-route en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/operation-routes Use this endpoint to create an Operation Route. # Create an Organization Source: https://docs.lerian.studio/en/reference/midaz/create-an-organization en/openapi/v3-current/ledger.yaml post /v1/organizations Use this endpoint to create an Organization. You can create multiple Organizations, but they must be added one at a time. # Create an Outflow Transaction Source: https://docs.lerian.studio/en/reference/midaz/create-an-outflow-transaction en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/outflow Use this endpoint to initiate an outflow transaction, abstracting the process of moving funds from internal ledger accounts to the external world.

Instead of requiring full transaction details, you only need to provide the source accounts. Based on the asset provided, the system automatically includes the corresponding external account in the distribute section of the transaction.

# Create an Unblock Transaction Source: https://docs.lerian.studio/en/reference/midaz/create-an-unblock-transaction en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/unblock Use this endpoint to create a transaction whose resulting operations are typed `UNBLOCK`. Midaz is agnostic about the business reason for unblocking funds — use the `metadata` field to record it. This endpoint always creates an immediately-posted, non-pending transaction: the `pending` field of the request body is ignored (overridden to `false`) — unblock transactions are never pending. The endpoint accepts the same body as the JSON create endpoint. # Create a Transaction Route Source: https://docs.lerian.studio/en/reference/midaz/create-transaction-route en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transaction-routes Use this endpoint to create an Transaction Route. # Delete a Balance Source: https://docs.lerian.studio/en/reference/midaz/delete-a-balance en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/balances/{balance_id} Use this endpoint to delete a balance. # Delete a Ledger Source: https://docs.lerian.studio/en/reference/midaz/delete-a-ledger en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id} Use this endpoint to delete a Ledger. The Ledger will remain in our records and appear in the [List Ledgers](/en/reference/midaz/list-ledgers) and [Retrieve a Ledger](/en/reference/midaz/retrieve-a-ledger) endpoint responses, with the `deletedAt` field showing the deletion date. # Delete a Metadata Index Source: https://docs.lerian.studio/en/reference/midaz/delete-a-metadata-index en/openapi/v3-current/ledger.yaml delete /v1/settings/metadata-indexes/entities/{entity_name}/key/{index_key} Use this endpoint to delete a metadata index by specifying the entity name and the index key. The index will be permanently removed. # Delete a Portfolio Source: https://docs.lerian.studio/en/reference/midaz/delete-a-portfolio en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/portfolios/{portfolio_id} Use this endpoint to delete a Portfolio. # Delete a Segment Source: https://docs.lerian.studio/en/reference/midaz/delete-a-segment en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/segments/{segment_id} Use this endpoint to delete a specific Segment. # Delete a Transaction Route Source: https://docs.lerian.studio/en/reference/midaz/delete-a-transaction-route en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/transaction-routes/{transaction_route_id} Use this endpoint to delete a specific Transaction Route. # Delete an Account Source: https://docs.lerian.studio/en/reference/midaz/delete-an-account en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id} Use this endpoint to delete a specific Account. > **Important:** You cannot delete an account with a remaining balance. First, transfer the amount to another account before deactivating it. # Delete an Account Type Source: https://docs.lerian.studio/en/reference/midaz/delete-an-account-type en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/account-types/{account_type_id} Use this endpoint to delete a specific Account Type. # Delete an Asset Source: https://docs.lerian.studio/en/reference/midaz/delete-an-asset en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/assets/{asset_id} Use this endpoint to delete an Asset. # Delete an Operation Route Source: https://docs.lerian.studio/en/reference/midaz/delete-an-operation-route en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/ledgers/{ledger_id}/operation-routes/{operation_route_id} Use this endpoint to delete a specific Operation Route. **Important:** You cannot delete an operation route that is associated with an active transaction route. # Delete an Organization Source: https://docs.lerian.studio/en/reference/midaz/delete-an-organization en/openapi/v3-current/ledger.yaml delete /v1/organizations/{id} Use this endpoint to delete an Organization. The Organization will remain in our records and appear in the [List Organizations](/en/reference/midaz/list-organizations) and [Retrieve an Organization](/en/reference/midaz/retrieve-an-organization) endpoint responses, with the `deletedAt` field showing the deletion date. # List Account Types Source: https://docs.lerian.studio/en/reference/midaz/list-account-types en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/account-types Use this endpoint to retrieve the details of all the Account Types you have created. # List Accounts Source: https://docs.lerian.studio/en/reference/midaz/list-accounts en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts Use this endpoint to list all accounts in your Ledger. # List Assets Source: https://docs.lerian.studio/en/reference/midaz/list-assets en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/assets Use this endpoint to retrieve the details of all Assets in your Ledger. # List Balances Source: https://docs.lerian.studio/en/reference/midaz/list-balances en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/balances Use this endpoint to list all balances. # List Holders Source: https://docs.lerian.studio/en/reference/midaz/list-holders en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/holders # List Ledgers Source: https://docs.lerian.studio/en/reference/midaz/list-ledgers en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers Use this endpoint to retrieve the details of all the Ledgers in an Organization. # List Metadata Indexes Source: https://docs.lerian.studio/en/reference/midaz/list-metadata-indexes en/openapi/v3-current/ledger.yaml get /v1/settings/metadata-indexes Use this endpoint to retrieve all metadata indexes. You can optionally filter the results by entity name. # List Operation Routes Source: https://docs.lerian.studio/en/reference/midaz/list-operation-routes en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/operation-routes Use this endpoint to retrieve the details of all the Operation Routes you have created. # List Operations by Account Source: https://docs.lerian.studio/en/reference/midaz/list-operations-by-account en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id}/operations Use this endpoint to retrieve all Operations from a specific Account. # List Organizations Source: https://docs.lerian.studio/en/reference/midaz/list-organizations en/openapi/v3-current/ledger.yaml get /v1/organizations Use this endpoint to retrieve the details of all the Organizations you have created. **Note:** the `metadata` filter is mutually exclusive with the `legal_name` and `doing_business_as` filters. Combining `metadata` with either of them returns a `400` error. # List Portfolios Source: https://docs.lerian.studio/en/reference/midaz/list-portfolios en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/portfolios Use this endpoint to retrieve the details of all Portfolios in your Ledger. If the filters in the request do not meet the endpoint's criteria, an empty response will be returned. # List Segments Source: https://docs.lerian.studio/en/reference/midaz/list-segments en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/segments Use this endpoint to retrieve the details of all Segments in your Ledger. # List Transaction Routes Source: https://docs.lerian.studio/en/reference/midaz/list-transaction-routes en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/transaction-routes Use this endpoint to retrieve the details of all the Transaction Routes you have created. # List Transactions Source: https://docs.lerian.studio/en/reference/midaz/list-transactions en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions Use this endpoint to retrieve the information of all Transactions created. **Note:** list items do **not** include the `operations` array. Only the [Retrieve a Transaction](/en/reference/midaz/retrieve-a-transaction) endpoint hydrates the full `operations` list for an individual transaction. # Retrieve a Balance Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-balance en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/balances/{balance_id} Use this endpoint to retrieve a specific balance by its identifier. # Retrieve Balances by Account Alias Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-balance-by-account-alias en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/alias/{alias}/balances Use this endpoint to retrieve all balances for an account identified by its alias. # Retrieve the Balance of an External Account Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-balance-of-an-external-account en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/external/{code}/balances Use this endpoint to retrieve all balances of an External Account identified by its asset code. # Retrieve Holder details Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-holder en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/holders/{holder_id} # Retrieve a Ledger Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-ledger en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id} Use this endpoint to retrieve the details of a specific Ledger by its identifier. If a ledger has been deleted, the information of the ledger will be displayed, including the date when the deletion occurred in the `deletedAt` field. # Retrieve a Portfolio Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-portfolio en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/portfolios/{portfolio_id} Use this endpoint to retrieve the details of a specific Portfolio by its identifier. # Retrieve a Segment Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-segment en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/segments/{segment_id} Use this endpoint to retrieve the details of a specific Segment by its identifier. # Retrieve a Transaction Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-transaction en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id} Use this endpoint to retrieve the details of a specific Transaction by its identifier. # Retrieve a Transaction Route Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-transaction-route en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/transaction-routes/{transaction_route_id} Use this endpoint to retrieve the details of a specific Transaction Route by its identifier. # Retrieve an Account Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-account en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id} Use this endpoint to retrieve the details of a specific Account by its identifier. # Retrieve an Account by Alias Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-account-by-alias en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/alias/{alias} Use this endpoint to retrieve the details of a specific Account by its alias. # Retrieve an Account Type Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-account-type en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/account-types/{account_type_id} Use this endpoint to retrieve the details of a specific Account Type by its identifier. # Retrieve an Asset Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-asset en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/assets/{asset_id} Use this endpoint to retrieve the details of a specific Asset by its identifier. # Retrieve an External Account Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-external-account en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/external/{code} Use this endpoint to retrieve the details of a specific External Account by its asset code. # Retrieve an Operation Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-operation-by-account en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id}/operations/{operation_id} Use this endpoint to retrieve the details of a specific Operation created from an Account by its identifier. # Retrieve an Operation Route Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-operation-route en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/operation-routes/{operation_route_id} Use this endpoint to retrieve the details of a specific Operation Route by its identifier. # Retrieve an Organization Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-organization en/openapi/v3-current/ledger.yaml get /v1/organizations/{id} Use this endpoint to retrieve the details of a specific Organization by its identifier. # Retrieve Balance History Source: https://docs.lerian.studio/en/reference/midaz/retrieve-balance-history en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/balances/{balance_id}/history Use this endpoint to retrieve the state of a specific balance at a given point in time (point-in-time query). # Retrieve Balance History by Account Source: https://docs.lerian.studio/en/reference/midaz/retrieve-balance-history-by-account en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id}/balances/history Use this endpoint to retrieve the state of all balances for a specific account at a given point in time (point-in-time query). # Retrieve Balances by Account Source: https://docs.lerian.studio/en/reference/midaz/retrieve-balances-by-account en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id}/balances Use this endpoint to retrieve all balances from a specific account. # Retrieve Ledger Settings Source: https://docs.lerian.studio/en/reference/midaz/retrieve-ledger-settings en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/settings Use this endpoint to retrieve the current settings of a specific Ledger. Settings control validation behavior for transactions processed within the Ledger. # Revert a Transaction Source: https://docs.lerian.studio/en/reference/midaz/revert-a-transaction en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/revert Use this endpoint to reverse an existing transaction by automatically generating a new one in the opposite direction of the original. The new transaction is linked to the original using the `parentTransactionId` field, ensuring full traceability between both operations. **Important**: Only transactions with status **APPROVED** can be reverted. If the original transaction already has a parent (i.e., was already reverted) the request will be rejected. On success this endpoint creates the reversing transaction and returns 201 Created with the created transaction payload. # Update a Balance Source: https://docs.lerian.studio/en/reference/midaz/update-a-balance en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/balances/{balance_id} Use this endpoint to update the information of an existing balance. # Update a Holder Source: https://docs.lerian.studio/en/reference/midaz/update-a-holder en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/holders/{holder_id} # Update a Ledger Source: https://docs.lerian.studio/en/reference/midaz/update-a-ledger en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id} Use this endpoint to update the details of a Ledger. # Update a Portfolio Source: https://docs.lerian.studio/en/reference/midaz/update-a-portfolio en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/portfolios/{portfolio_id} Use this endpoint to update the details of a Portfolio. # Update a Segment Source: https://docs.lerian.studio/en/reference/midaz/update-a-segment en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/segments/{segment_id} Use this endpoint to update the details of a Segment. # Update a Transaction Source: https://docs.lerian.studio/en/reference/midaz/update-a-transaction en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id} Use this endpoint to update the details of a Transaction. # Update a Transaction Route Source: https://docs.lerian.studio/en/reference/midaz/update-a-transaction-route en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/transaction-routes/{transaction_route_id} Use this endpoint to update the details of a Transaction Route. # Update an Account Source: https://docs.lerian.studio/en/reference/midaz/update-an-account en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id} Use this endpoint to update the details of an Account. **Merge semantics:** this endpoint follows [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) JSON Merge Patch. Sending an explicit `null` for `segmentId`, `entityId`, or `portfolioId` clears the field; omitting a field leaves its current value unchanged. **Note:** accounts of type `external` are immutable and managed by the system. Attempting to update one returns a `400`/`403` error. # Update an Account Type Source: https://docs.lerian.studio/en/reference/midaz/update-an-account-type en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/account-types/{account_type_id} Use this endpoint to update the details of an Account Type. # Update an Asset Source: https://docs.lerian.studio/en/reference/midaz/update-an-asset en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/assets/{asset_id} Use this endpoint to update the details of an Asset. # Update an Operation Source: https://docs.lerian.studio/en/reference/midaz/update-an-operation en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/{transaction_id}/operations/{operation_id} Use this endpoint to update the details of an Operation. # Update an Operation Route Source: https://docs.lerian.studio/en/reference/midaz/update-an-operation-route en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/operation-routes/{operation_route_id} Use this endpoint to update the details of an Operation Route. # Update an Organization Source: https://docs.lerian.studio/en/reference/midaz/update-an-organization en/openapi/v3-current/ledger.yaml patch /v1/organizations/{id} Use this endpoint to update the details of an Organization. **Note:** an Organization cannot be its own parent. If `parentOrganizationId` equals the Organization's own `id`, the request returns a `400` error. # Update Ledger Settings Source: https://docs.lerian.studio/en/reference/midaz/update-ledger-settings en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/ledgers/{ledger_id}/settings Use this endpoint to partially update the settings of a specific Ledger. Uses deep merge — only the fields you include in the request body are changed. # Pagination Source: https://docs.lerian.studio/en/reference/pagination Page-based pagination across Lerian list endpoints, with page and limit query parameters and a consistent paginated response payload for every resource. Efficiently managing large datasets is crucial for maintaining API performance and scalability. Our APIs employ a robust pagination mechanism to deliver data in smaller, manageable portions. This improves response times and client-side processing, particularly for applications that load results incrementally. ## Pagination in our APIs *** Instead of returning the entire dataset in a single response, which can strain resources, clients can request specific subsets of data. This improves response times and enables smoother data handling, particularly for applications that load results incrementally. We support two query parameters for pagination: * `page` (integer): Specifies the page number to retrieve. The default value is `1`. * Negative values are invalid and will result in an error. * `limit` (integer): Defines the maximum number of items per page. The default value is `10`. For example, to retrieve the first page of organizations with a maximum of 10 items per page: ```http HTTP theme={null} GET /v1/organizations?page=1&limit=10 ``` ### Pagination response The pagination response includes the following structure: * `items`: An array of entities retrieved for the current page. Each object contains detailed information about the resource requested, as shown in the example below. * `page`: The current page number, starting from `1` (default). * `limit`: The maximum number of items included in the response, as defined in the request or default configuration. Below is an example response for a paginated request: ```jsonon JSON expandable theme={null} { "items": [ { "id": "0193696f-8eff-7926-a77a-0b424cf7156b", "parentOrganizationId": null, "legalName": "Collins Inc", "doingBusinessAs": "The ledger.io", "legalDocument": "78425230000190", "address": { "line1": "Avenida Paulista, 1234", "line2": "CJ 203", "zipCode": "01310916", "city": "New Lyla", "state": "CH", "country": "FO" }, "status": { "code": "ACTIVE", "description": "Ledger Test" }, "createdAt": "2024-11-26T17:05:39.071587Z", "updatedAt": "2024-11-26T17:05:39.071587Z", "deletedAt": null, "metadata": { "bitcoinn": "3QH2XV7JxMRKXDqh87JG7LWuf7AeGfy", "boolean": true, "chave": "metadata_chave", "double": 10.5, "int": 1 } } ], "page": 1, "limit": 1 } ``` ### Maximum page size By default, API endpoints that support pagination accept a maximum of **100 items per page** (`limit=100`). This restriction helps ensure optimal performance and avoids excessive payload sizes. If your use case requires retrieving a larger number of items per request, you can override this limit by setting the `MAX_PAGINATION_LIMIT` environment variable in your deployment configuration ( `.env` file). Increasing the pagination limit may result in slower response times depending on the volume of data and infrastructure conditions. Be sure to test thoroughly in staging environments before applying the change in production. To update this configuration in Kubernetes: ```bash bash theme={null} kubectl edit configmap midaz-ledger -n midaz # Set the new value for MAX_PAGINATION_LIMIT kubectl rollout restart deployments midaz-ledger -n midaz ``` On a Helm-managed install, set `ledger.configmap.MAX_PAGINATION_LIMIT` in your `values.yaml` and run `helm upgrade`. The next chart upgrade replaces a direct edit to the ConfigMap. Midaz and all its plugin services inherit this behavior. ## Cursor-based pagination *** Some endpoints use cursor-based pagination instead of page numbers. This approach is more efficient for large or frequently changing datasets and guarantees consistent results even when data is modified between requests. Cursor-based endpoints accept the following query parameters: * `cursor` (string): An encoded token from a previous response (`next_cursor` or `prev_cursor`) to navigate forward or backward. Omit this parameter to start from the beginning. * `limit` (integer): The maximum number of items per page. The default value is `10`. * `sort_order` (string): The direction used to sort results. Accepted values: `asc` (ascending, default) or `desc` (descending). When paginating with a `cursor` from a previous response, you cannot change `sort_order` mid-pagination. Sorting parameters must be set on the initial request and remain consistent throughout the pagination sequence. For example, to retrieve balances using cursor pagination: ```http HTTP theme={null} GET /v1/organizations/{organization_id}/ledgers/{ledger_id}/balances?limit=10 ``` ### Cursor pagination response The response includes the following structure: * `items`: An array of entities for the current page. * `next_cursor`: An encoded token to retrieve the next page. If absent or empty, there are no more results. * `prev_cursor`: An encoded token to retrieve the previous page. If absent or empty, you are on the first page. * `limit`: The maximum number of items included in the response. To navigate to the next page, pass the `next_cursor` value as the `cursor` query parameter: ```http HTTP theme={null} GET /v1/organizations/{organization_id}/ledgers/{ledger_id}/balances?cursor=eyJpZCI6IjAxOTNiN...&limit=10 ``` Below is an example response for a cursor-paginated request: ```json JSON theme={null} { "items": [ { "id": "019c96a0-0c0d-7915-84b9-e497bfee9916", "organizationId": "019c96a0-0a98-7287-9a31-786e0809c769", "ledgerId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", "accountId": "019c96a0-0c0c-7221-8cf3-13313fb60081", "alias": "customer-brl-1", "key": "default", "assetCode": "BRL", "available": "1000", "onHold": "0" } ], "next_cursor": "eyJpZCI6IjAxOTNiNTZmLWJhY2YtNzQ0MS05NDU4LTEyZTE5MjVlOGI4NCIsInBvaW50c19uZXh0Ijp0cnVlfQ==", "prev_cursor": "eyJpZCI6IjAxOTNiNTZmLWJhY2YtNzQ0MS05NDU4LTEyZTE5MjVlOGI4NCIsInBvaW50c19uZXh0IjpmYWxzZX0=", "limit": 10 } ``` ### Which endpoints use cursor pagination? The following endpoints use cursor-based pagination: * **List Transactions** — `GET /v1/organizations/{id}/ledgers/{id}/transactions` * **List Balances** — `GET /v1/organizations/{id}/ledgers/{id}/balances` * **List Operations by Account** — `GET /v1/organizations/{id}/ledgers/{id}/accounts/{id}/operations` * **List Account Types** — `GET /v1/organizations/{id}/ledgers/{id}/account-types` * **List Operation Routes** — `GET /v1/organizations/{id}/ledgers/{id}/operation-routes` * **List Transaction Routes** — `GET /v1/organizations/{id}/ledgers/{id}/transaction-routes` All other list endpoints use the standard page-based pagination described above. # Postman integration Source: https://docs.lerian.studio/en/reference/postman-integration Import a ready-made Lerian Postman collection and run authenticated requests against your API instance to test endpoints and speed up integration work. Lerian’s Postman collections are a quick way to get hands-on with our APIs. You can test requests, receive real responses, and explore how the APIs fit together. Whether you’re building or debugging, they help you move faster and more confidently. When a Lerian project includes a Postman collection, it is usually stored in a `postman` folder inside the project directory. To learn more about importing collections, check the [Postman’s documentation](https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/) . ## Importing the Postman collection *** To import the Postman collection, follow these steps: Open Postman. Click **"Import"** in the top-left corner. In the **Import** modal, select the **"File"** tab. Click **"Choose Files"**, then navigate to the `postman` folder inside the project directory you are using. Postman will process the file and add the collection to **"Collections"** in your workspace. Open the collection and verify that all API requests and folders are correctly imported. ## Tokens and authentication *** If you are using **Midaz** in the [**Community model**](/en/midaz/what-is-midaz), you do not need to authenticate or access tokens; you're good to go. For the [**Enterprise model**](/en/midaz/what-is-midaz), you can enable authentication and token management using the [Access Manager](/en/platform/access-manager/access-manager) plugin. Access Manager is available as a part of 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). # Quick start guide Source: https://docs.lerian.studio/en/reference/quick-start-guide Set up Midaz for the first time, from environment installation and configuration to creating your first ledger structure and posting a test transaction. **This guide is intended for developers** setting up Midaz for the first time. If you're looking for a business-level overview of how Midaz works, see [What is Midaz?](/en/midaz/what-is-midaz). For deployment options, see [SaaS and BYOC](/en/deployment-models). Starting with Midaz is straightforward. Follow these steps to build your foundation and explore the possibilities. ## Step 1 - Set up your environment *** To set up your Midaz environment, follow the steps in the [Installing Midaz monorepo](/en/midaz/midaz-setup) page. * This will install Midaz Ledger. ### Authentication and authorization Midaz does not include a built-in authentication or authorization system. This gives you the flexibility to integrate with your existing identity provider or security stack. If you need a ready-to-use solution, you can enable **Access Manager**, an optional feature developed by Lerian. It handles both authentication and authorization, making it easier to control who can access what across your entire Lerian ecosystem — including Midaz and all other Lerian products. Access Manager supports common identity standards like OAuth2 and OIDC and integrates natively with all Lerian products. For more details, refer to the [Access Manager](/en/platform/access-manager/access-manager) guide. ## Step 2 - Create your Midaz structure *** Start by creating the data structure that powers your workflows. ### Step 2.1 - Create an Organization The first step is to create your Organization within Midaz. * [**To create an Organization, use the Create an Organization endpoint.**](/en/reference/midaz/create-an-organization) If your organization has multiple branches, you can create various Organizations and designate a **Parent Organization**. For example, if Company XYZ has branches in New York and Brazil, you can create an Organization for Company XYZ and set it as the parent of XYZ New York and XYZ Brazil. For a complete list of endpoints to manage your Organization, expand the section below: * Retrieve all Organizations using the [List Organizations](/en/reference/midaz/list-organizations) endpoint. * Retrieve a specific Organization with the [Retrieve an Organization](/en/reference/midaz/retrieve-an-organization) * Update Organization details using the [Update an Organization](/en/reference/midaz/update-an-organization) endpoint. * Delete an Organization in Homologation using the [Delete an Organization](/en/reference/midaz/delete-an-organization) endpoint. * For Organizations in Production, deletion requires contacting Lerian support for security purposes. ### Step 2.2 - Create a Ledger Each organization must have **at least** on Ledger. However, you can have multiple Ledgers within each Organization if needed. * [**To create a Ledger, use the Create a Ledger endpoint.**](/en/reference/midaz/create-a-ledger) For a complete list of endpoints to manage your Ledger, expand the section below: * Retrieve all Ledgers using the [List Ledgers](/en/reference/midaz/list-ledgers) endpoint. * Retrieve a specific Ledger with the [Retrieve a Ledger](/en/reference/midaz/retrieve-a-ledger) endpoint. * Update Ledger details using the [Update a Ledger](/en/reference/midaz/update-a-ledger) endpoint. * Delete a Ledger in Homologation using the [Delete a Ledger](/en/reference/midaz/delete-a-ledger) endpoint. * For Ledgers in Production, deletion requires contacting Lerian support for security purposes. ### Step 2.3 - Create the Assets Your Organization can have multiple Assets. However, each Asset must be created individually, allowing for specific configurations like decimal places. * [**To create an Asset, use the Create an Asset endpoint.**](/en/reference/midaz/create-an-asset) When you create an Asset, Midaz automatically sets up an External Account if one doesn’t already exist. This ensures seamless backing for all transactions flowing in and out of the Ledger. For a complete list of endpoints to manage your Assets, expand the section below: * Retrieve all Assets using the [List Assets](/en/reference/midaz/list-assets) endpoint. * Retrieve a specific Asset with the [Retrieve an Asset](/en/reference/midaz/retrieve-an-asset) endpoint. * Update Asset details using the [Update an Asset](/en/reference/midaz/update-an-asset) endpoint. * Delete an Asset with the [Delete an Asset](/en/reference/midaz/delete-an-asset) endpoint. ### Step 2.4 - Create a Segment (optional) If you want, you can use the **Segment ID** as a custom grouping label that enables clustering accounts according to user-defined policies, supporting efficient account segmentation and management. * [**To create a Product, use the Create a Segment endpoint.**](/en/reference/midaz/create-a-segment) For a complete list of endpoints to manage your Segment, expand the section below: * Retrieve all Segments using the [List Segments](/en/reference/midaz/list-segments) endpoint. * Retrieve a specific Segment with the [Retrieve a Segment](/en/reference/midaz/retrieve-a-segment) endpoint. * Update Segment details using the [Update a Segment](/en/reference/midaz/update-a-segment) endpoint. * Delete a Segment using the [Delete a Segment](/en/reference/midaz/delete-a-segment) endpoint. ### Step 2.5 - Create a Portfolio Create a Portfolio to group your accounts of different Assets. * [**To create a Portfolio, use the Create a Portfolio endpoint.**](/en/reference/midaz/create-a-portfolio) For a complete list of endpoints to manage your Portfolios, expand the section below: * Retrieve all Portfolios using the [List Portfolios](/en/reference/midaz/list-portfolios) endpoint. * Retrieve a specific Portfolio with the [Retrieve a Portfolio](/en/reference/midaz/retrieve-a-portfolio) endpoint. * Update Portfolio details using the [Update a Portfolio](/en/reference/midaz/update-a-portfolio) endpoint. * Delete a Portfolio with the [Delete a Portfolio](/en/reference/midaz/delete-a-portfolio) endpoint. ### Step 2.6 - Create the Accounts You can now add the accounts related to the Assets you want to use with the Ledger. You will have to create one account for each Asset. * [**To create an account, use the Create an Account endpoint.**](/en/reference/midaz/create-an-account) For a complete list of endpoints to manage your Accounts, expand the section below: * Retrieve all Accounts using the [List Accounts](/en/reference/midaz/list-accounts) endpoint. * Retrieve a specific Account with the [Retrieve an Account](/en/reference/midaz/retrieve-an-account) endpoint. * Update Account details using the [Update an Account](/en/reference/midaz/update-an-account) endpoint. * Delete an Account with the [Delete an Account](/en/reference/midaz/delete-an-account) endpoint. ## Step 3 - Explore our APIs *** Dive into the [API reference](/en/reference/introduction) to learn more about how Midaz can elevate your operations. From metadata handling to idempotency, every detail is crafted for simplicity and efficiency. ## Need help? *** You're not alone. We’re here to support you every step of the way. * Explore our [Documentation hub](/en/welcome-to-our-guides) for in-depth guides and resources. * Join the [Discord community](https://discord.lerian.studio/). * [Contact our support team](https://lerian.studio/support) for personalized assistance. # Reliability and support Source: https://docs.lerian.studio/en/reference/stability-quality Error handling, testing practices, and support channels that keep Lerian integrations stable, predictable, and easy to maintain across releases. This section helps you keep your integrations stable, reliable, and easy to maintain. It covers how to handle errors, validate quality, and get help when something goes wrong. ## Available pages ***
Learn how Lerian services communicate errors and how to design resilient integrations. Explore recommended testing strategies, tools, and validation flows. Diagnose issues, review logs, and reach out to our support team.
# Testing and quality assurance Source: https://docs.lerian.studio/en/reference/testing-quality-assurance Unit and integration test policies, plus the CI checks and quality gates that guard every code change in the Midaz codebase before it reaches production. Midaz is dedicated to maintaining high standards of software quality. To achieve this, we use both unit and integration tests throughout our codebase. These tests are integral to our development process, ensuring that each component functions correctly and integrates seamlessly. Here, you will find details of the testing process and policies ensuring that new features are accompanied by corresponding unit tests to maintain comprehensive test coverage. ## Running tests *** To execute the tests, use the following command in your terminal: ```bash command theme={null} make test ``` This command runs the predefined test suite, which includes both unit and integration tests. ## Continuous integration *** * **GitHub actions**: Our tests are integrated with GitHub Actions, automatically running as part of our continuous integration pipeline. This ensures that all code commits meet our quality standards before merging. ## Unit testing policy *** * **New features**: As part of our testing policy, any new feature introduced in Midaz must include corresponding unit tests. This practice helps ensure that new code does not negatively impact existing functionality. * **Test coverage**: We aim for comprehensive test coverage as a benchmark for software quality. ## Test outputs *** * **Terminal outputs**: Test results appear directly in the terminal, providing developers with immediate feedback to quickly identify and resolve issues. * **Automated test pipeline**: Tests also run automatically as part of our continuous delivery pipeline, supporting consistent quality and reliability across the software. # Tolerant reader Source: https://docs.lerian.studio/en/reference/tolerant-reader The tolerant reader integration pattern lets your client code absorb Lerian API changes — new fields, reordering, or additions — without breaking. As our public API evolves to meet new requirements and enhance functionality, integrators, and clients need to adapt seamlessly. A key pattern supporting resilience to such changes is the **Tolerant Reader**. By adopting the Tolerant Reader pattern, integrations can better handle new information without frequent updates. This approach reduces maintenance needs and enhances resilience, creating a smoother integration experience. ## What is a tolerant reader? *** The Tolerant Reader pattern is an integration design approach where a client consuming data is built to handle potential changes—such as added fields or minor structure modifications—gracefully. This pattern promotes forward compatibility, reducing the need for immediate updates whenever the data or service evolves. ## Real-world scenario *** Consider a request to `GET /organizations/{organization_id}`. Initially, the response might be: ```jsonon JSON theme={null} { "id": "cc15194a-6bc9-4ebb-b15d-43411a54ba4b", "parentOrganizationId": null, "legalName": "Empresa Teste Ltda", "legalDocument": "86820799000188", "metadata": null, "status": { "code": "ACTIVE", "description": null }, "createdAt": "2024-02-08T16:59:31+0300", "updatedAt": "2024-02-08T16:59:31+0300", "deletedAt": null } ``` Later, fields like `doingBusinessAs` and `address` may be added: ```jsonon JSON expandable theme={null} { "id": "cc15194a-6bc9-4ebb-b15d-43411a54ba4b", "parentOrganizationId": null, "legalName": "Empresa Teste Ltda", "doingBusinessAs": "Empresa Teste", "legalDocument": "86820799000188", "address": { "line1": "Rua Botucatu, 10", "line2": "Casa B", "zipCode": "04023060", "city": "São Paulo", "state": "SP", "country": "BR" }, "metadata": null, "status": { "code": "ACTIVE", "description": null }, "createdAt": "2024-02-08T16:59:31+0300", "updatedAt": "2024-02-08T16:59:31+0300", "deletedAt": null } ``` A client that follows the Tolerant Reader pattern would handle the additional fields without issue, ensuring continued functionality. ## Implementing a tolerant reader *** Below is an example Python code snippet that processes additional fields gracefully, accounting for changes in the payload. ```python Python expandable theme={null} def fetch_organization_info(organization_id): response = requests.get(f"{host}/organization/{organization_id}") org_info = response.json() # Fetch required fields legal_name = org_info.get("legalName", "Unknown") legal_document = org_info.get("legalDocument", "No document provided") # Handle optional fields doing_business_as = org_info.get("doingBusinessAs") # Nested address structure address_info = org_info.get("address", {}) line1 = address_info.get("line1", "Address not provided") city = address_info.get("city", "City not provided") state = address_info.get("state", "State not provided") country = address_info.get("country", "Country not provided") # Display information print(f"Legal Name: {legal_name}, Legal Document: {legal_document}") print(f"Doing Business As: {doing_business_as}") print(f"Address: {line1}, {city}, {state}, {country}") # Example usage fetch_organization_info("cc15194a-6bc9-4ebb-b15d-43411a54ba4b") ``` In this example, `get` methods are used to safely access values. This allows for flexibility if new fields, such as `phoneNumber`, are introduced, as the code remains functional even if a field is absent. # Troubleshooting and support Source: https://docs.lerian.studio/en/reference/troubleshooting-support Troubleshoot common Midaz issues and reach the right support channels for a mission-critical service kept current with community fixes and improvements. If you have any suggestions, such as enhancements, ideas, questions, or anything else that could improve Midaz, please reach out to us using the channels listed below. ## Troubleshooting *** Before reaching out for support, try these quick checks for common issues: ### Midaz isn’t starting * Ensure all dependencies (Docker, Kubernetes, Helm) are installed and running. * Check if the required environment variables are set. * Use `docker compose logs` or `kubectl logs` to inspect container output for errors. ### API not responding * Verify you’re hitting the correct endpoint and using the right credentials. * Use tools like Postman or `curl` to test requests manually. * Refer to the [API Reference](/en/reference/introduction) to confirm the expected payload. Logs are your best friend; if something breaks, start there. ## Community support *** If you use the **Community** version and are looking for help or want to get involved, we offer open channels where you can ask questions, report bugs, and share ideas. * **GitHub Discussions**: Use the [Discussions tab](https://github.com/LerianStudio/midaz-console/discussions) to talk with other users, propose improvements, or raise issues. * **Discord**: Join our [community on Discord](https://discord.gg/DnhqKwkGv3) for real-time support and updates. Look for the **#midaz** channel. Whether you're a first-time contributor or an experienced dev, your input matters. Let’s build something better together. If you're an **Enterprise** customer, use the [**Lerian Support Portal**](/en/midaz/lerian-support-portal) to get guaranteed response times and access to our dedicated support team. # Create connection Source: https://docs.lerian.studio/en/reference/fetcher/connections/create-connection en/openapi/v3-current/connections.yaml post /v1/management/connections Creates a database connection for the product named by the X-Product-Name header. # List connections Source: https://docs.lerian.studio/en/reference/fetcher/connections/list-connections en/openapi/v3-current/connections.yaml get /v1/management/connections Returns a filtered, paginated list of database connections visible to the optional product. Dynamic metadata filters use query parameters such as metadata.region=br. # Get a connection Source: https://docs.lerian.studio/en/reference/fetcher/connections/retrieve-connection en/openapi/v3-current/connections.yaml get /v1/management/connections/{id} Returns one database connection by identifier. # Calculate billing Source: https://docs.lerian.studio/en/reference/midaz/calculate-billing en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/billing/calculate # Create a BillingPackage Source: https://docs.lerian.studio/en/reference/midaz/create-a-billing-package en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/billing-packages # Create a fee estimate calculation Source: https://docs.lerian.studio/en/reference/midaz/create-a-fee-estimate en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/estimates # Create a Package Source: https://docs.lerian.studio/en/reference/midaz/create-a-package en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/packages # Create an Instrument Account Source: https://docs.lerian.studio/en/reference/midaz/create-an-instrument en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/holders/{holder_id}/instruments # Create or Update an AssetRate Source: https://docs.lerian.studio/en/reference/midaz/create-or-update-an-asset-rate en/openapi/v3-current/ledger.yaml put /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates # SoftDelete a BillingPackage by ID Source: https://docs.lerian.studio/en/reference/midaz/delete-a-billing-package en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/billing-packages/{billing_package_id} # Delete a Holder Source: https://docs.lerian.studio/en/reference/midaz/delete-a-holder en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/holders/{holder_id} # SoftDelete a Package by ID Source: https://docs.lerian.studio/en/reference/midaz/delete-a-package en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/packages/{package_id} # Delete a Related Party Source: https://docs.lerian.studio/en/reference/midaz/delete-a-related-party en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}/related-parties/{related_party_id} # Delete an Instrument Source: https://docs.lerian.studio/en/reference/midaz/delete-an-instrument en/openapi/v3-current/ledger.yaml delete /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} # Midaz error list Source: https://docs.lerian.studio/en/reference/midaz/error-list Midaz APIs return a structured error object with a stable code, HTTP status, and message so you can quickly diagnose ledger issues and remediate them. The Midaz API returns errors as an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details object, served with the `application/problem+json` content type: ```json theme={null} { "type": "https://errors.lerian.studio/v1/", "title": "", "status": , "detail": "", "code": "" } ``` **Field definitions** * `type`: A URI that identifies the error type, built as `https://errors.lerian.studio/v1/` followed by the error code. * `title`: A brief summary of the issue. * `status`: The HTTP status code of the response. * `detail`: Detailed guidance for resolving the error. The table below lists this content in the `message` column. * `code`: A unique, stable identifier for the error. * `entityType`: The entity the error relates to (for example, `Organization`). Only present when applicable. For server-side errors (HTTP 5xx), `title` and `detail` are sanitized to generic values so internal causes never leak. Use `code` and `type` to identify the error. **Field-level error details** For issues specific to individual fields, an `errors` array lists each field with its problem. Examples: ```json Missing Required Fields theme={null} { "type": "https://errors.lerian.studio/v1/0009", "title": "Missing Fields in Request", "status": 400, "detail": "Your request is missing one or more required fields: [legalName]. Please refer to the documentation to ensure all necessary fields are included in your request.", "code": "0009", "entityType": "Organization", "errors": [ { "location": "legalName", "message": "legalName is a required field" } ] } ``` ```json Invalid Field Values theme={null} { "type": "https://errors.lerian.studio/v1/0047", "title": "Bad Request", "status": 400, "detail": "The server could not understand the request due to malformed syntax. Please check the listed fields and try again.", "code": "0047", "errors": [ { "location": "legalName", "message": "legalName is a required field." }, { "location": "parentOrganizationId", "message": "parentOrganizationId must be a valid UUID" } ] } ``` ```json Unexpected Fields theme={null} { "type": "https://errors.lerian.studio/v1/0053", "title": "Unexpected Fields in the Request", "status": 400, "detail": "The request body contains more fields than expected. Please send only the allowed fields as per the documentation. The unexpected fields are listed in the fields object.", "code": "0053", "errors": [ { "location": "unknownField", "message": "unexpected field", "value": "..." } ] } ``` ## Midaz errors *** Refer to the following table for error codes and their descriptions: | `code` | `title` | `message` | | ------ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0001 | Duplicate Ledger Error | A ledger with the specified name already exists in this division. Please rename the ledger or choose a different division. | | 0002 | Ledger Name Conflict | A ledger named \{\{name}} already exists in your organization. Please rename the ledger, or if you want to use the same name, consider creating a new ledger for a different division. | | 0003 | Asset Name or Code Duplicate | An asset with the same name or code already exists in your ledger. Please modify the name or code of your new asset. | | 0004 | Code Uppercase Requirement | The code must be in uppercase. Please ensure that the code is in uppercase format and try again. | | 0005 | Currency Code Standard Compliance | Currency-type assets must comply with the ISO-4217 standard. Please use a currency code that conforms to ISO-4217 guidelines. | | 0006 | Unmodifiable Field Error | Your request includes a field that cannot be modified. Please review your request and try again, removing any uneditable fields. | | 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. | | 0008 | Action Not Permitted | This action is not permitted in the current environment. Contact an administrator if you believe this is an error. | | 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. | | 0010 | Account Type Immutable | The account type specified cannot be modified. Please ensure the correct account type is being used and try again. | | 0011 | Inactive Account Type Error | The account type specified cannot be set to INACTIVE. Please ensure the correct account type is being used and try again. | | 0012 | Account Balance Deletion Error | An account or sub-account cannot be deleted if it has a remaining balance. Please ensure all remaining balances are transferred before attempting to delete. | | 0013 | Resource Already Deleted | The resource you are trying to delete has already been deleted. Ensure you are using the correct ID and try again. | | 0014 | Segment ID Inactive | The segment with ID \{\{id}} is currently inactive. Please activate the segment before proceeding. | | 0015 | Duplicate Segment Name Error | A segment with the name \{\{name}} already exists for this ledger ID \{\{ledgerId}}. Please try again with a different ledger or name. | | 0017 | Invalid Script Format Error | The script provided in your request is invalid or in an unsupported format. Please verify the script format and try again. | | 0018 | Insufficient Funds Error | The transaction could not be completed due to insufficient funds in the account. Please add sufficient funds to your account and try again. | | 0019 | Account Ineligibility Error | One or more accounts listed in the transaction are not eligible to participate. Please review the account statuses and try again. | | 0020 | Alias Unavailability Error | The alias \{\{alias}} is already in use. Please choose a different alias and try again. | | 0021 | Parent Transaction ID Not Found | The parentTransactionId \{\{parentTransactionId}} does not correspond to any existing transaction. Please review the ID and try again. | | 0022 | Immutable Field Error | The \{\{field}} field cannot be modified. Please remove this field from your request and try again. | | 0023 | Transaction Timing Restriction | You can only perform another transaction using \{\{assetCode}} of \{\{amount}} from \{\{source}} to \{\{destination}} after \{\{timestampUnlock}}. Please wait until the specified time to try again. | | 0024 | Account Status Transaction Restriction | The current statuses of the source and/or destination accounts do not permit transactions. Change the account status(es) and try again. | | 0025 | Insufficient Account Balance Error | The account \{\{accountId}} does not have sufficient balance. Please try again with an amount that is less than or equal to the available balance. | | 0026 | Transaction Method Restriction | Transactions involving \{\{assetCode}} are not permitted for the specified source and/or destination. Please try again using accounts that allow transactions with \{\{assetCode}}. | | 0027 | Duplicate Transaction Template Code Error | A transaction template with the specified code already exists for your ledger. Please use a different code and try again. | | 0028 | Duplicate Asset Pair Error | A pair for the specified assets already exists. Please update the existing entry instead of creating a new one. | | 0029 | Invalid Parent Account ID | The specified parent account ID does not exist. Please verify the ID is correct and try your request again. | | 0030 | Mismatched Asset Code | The parent account ID is associated with a different asset code than the one specified. Please make sure the asset code matches the parent account. | | 0031 | Chart Type Not Found | The specified chart type does not exist. Please provide a valid chart type and refer to the documentation. | | 0032 | Invalid Country Code | The provided country code in the 'address.country' field does not conform to the ISO-3166 alpha-2 standard. Please provide a valid alpha-2 country code. | | 0033 | Invalid Code Format | The 'code' field must be alphanumeric, in upper case, and must contain at least one letter. Please provide a valid code. | | 0034 | Asset Code Not Found | The provided asset code does not exist in our records. Please verify the asset code and try again. | | 0035 | Portfolio ID Not Found | The provided portfolio ID does not exist in our records. Please verify the portfolio ID and try again. | | 0036 | Segment ID Not Found | The provided segment ID does not exist in our records. Please verify the segment ID and try again. | | 0037 | Ledger ID Not Found | The provided ledger ID does not exist in our records. Please verify the ledger ID and try again. | | 0038 | Organization ID Not Found | The provided organization ID does not exist in our records. Please verify the organization ID and try again. | | 0039 | Parent Organization ID Not Found | The provided parent organization ID does not exist in our records. Please verify the parent organization ID and try again. | | 0040 | Invalid Type | The provided 'type' is not valid. Accepted types are currency, fiat, crypto, commodity, or others. Please provide a valid type. | | 0041 | Token Missing | A valid token must be provided in the request header. Please include a token and try again. | | 0042 | Invalid Token | The provided token is expired, invalid or malformed. Please provide a valid token and try again. | | 0043 | Insufficient Privileges | You do not have the necessary permissions to perform this action. Please contact your administrator if you believe this is an error. | | 0044 | Permission Enforcement Error | The enforcer is not configured properly. Please contact your administrator if you believe this is an error. | | 0045 | JWK Fetch Error | The JWK keys could not be fetched from the source. Please verify the source environment variable configuration and try again. | | 0046 | Internal Server Error | The server encountered an unexpected error. Please try again later or contact support. | | 0047 | Bad Request | The server could not understand the request due to malformed syntax. Please check the listed fields and try again. | | 0050 | Metadata Key Length Exceeded | The metadata key \{\{key}} exceeds the maximum allowed length. Please use a shorter key. | | 0051 | Metadata Value Length Exceeded | The metadata value \{\{key}} exceeds the maximum allowed length. Please use a shorter value. | | 0052 | Account ID Not Found | The provided account ID does not exist in our records. Please verify the account ID and try again. | | 0053 | Unexpected Fields in the Request | The request body contains more fields than expected. Please send only the allowed fields as per the documentation. The unexpected fields are listed in the fields object. | | 0054 | IDs Not Found for Accounts | No accounts were found for the provided IDs. Please verify the IDs and try again. | | 0055 | Asset ID Not Found | The specified asset ID does not exist in the provided ledger. Please verify the asset ID and try again. | | 0056 | No Assets Found | No assets were found for the specified ledger. Please create an asset or verify the ledger exists. | | 0057 | No Segments Found | No segments were found for the specified ledger. Please create a segment or verify the ledger exists. | | 0058 | No Portfolios Found | No portfolios were found for the specified ledger. Please create a portfolio or verify the ledger exists. | | 0059 | No Organizations Found | No organizations were found in the search. Please review the search criteria and try again. | | 0060 | No Ledgers Found | No ledgers were found in the search. Please review the search criteria and try again. | | 0061 | Balance Update Failed | The balance could not be updated for the specified account ID. Please verify the account ID and try again. | | 0062 | No Account IDs Provided | No account IDs were provided for the balance update. Please provide valid account IDs and try again. | | 0063 | Failed To Retrieve Accounts By Aliases | The accounts could not be retrieved using the specified aliases. Please verify the aliases for accuracy and try again. | | 0064 | No Accounts Found | No accounts were found in the search. Please review the search criteria and try again. | | 0065 | Invalid Path Parameter | The provided path parameter \{\{parameter\_name}} is not in the expected format. Please ensure the parameter adheres to the required format and try again. | | 0066 | Invalid Account Type | The provided 'type' is not valid. Accepted types are: deposit, savings, loans, marketplace, cerditCard or external. Please provide a valid type. | | 0067 | Invalid Metadata Nesting | The metadata object cannot contain nested values. Please ensure that the value \{\{value}} is not nested and try again. | | 0068 | Operation ID Not Found | The provided operation ID does not exist in our records. Please verify the operation ID and try again. | | 0069 | No Operations Found | No operations were found for the given query parameters. Please adjust your filters and try again. | | 0070 | Transaction ID Not Found | The provided transaction ID does not exist in our records. Please verify the transaction ID and try again. | | 0071 | No Transactions Found | No transactions were found for the given query parameters. Please adjust your filters and try again. | | 0072 | Invalid Transaction Type | Only one transaction type (amount, share, or remaining) must be specified for each entry. Please review your input and try again. | | 0073 | Transaction Value Mismatch | The values for the source, the destination, or both do not match the specified transaction amount. Please verify the values and try again. | | 0074 | External Account Modification Prohibited | Accounts of type 'external' cannot be deleted or modified as they are used for traceability with external systems. Please review your request and ensure operations are only performed on internal accounts. | | 0075 | Audit Record Not Retrieved | The record could not be retrieved for audit. Please verify that the submitted data is correct and try again. | | 0076 | Audit Tree Record Not Found | The record does not exist in the audit tree. Please ensure the audit tree is available and try again. | | 0077 | Invalid Date Format Error | The initialDate, finalDate, or both are in the incorrect format. Please use the yyyy-mm-dd format and try again. | | 0078 | Invalid Final Date Error | The finalDate cannot be earlier than the initialDate. Please verify the dates and try again. | | 0079 | Date Range Exceeds Limit Error | The range between 'initialDate' and 'finalDate' exceeds the permitted limit of \{\{limit}} months. Please adjust the dates and try again. | | 0080 | Pagination Limit Exceeded | The pagination limit exceeds the maximum allowed of \{\{pageLimit}} items per page. Please verify the limit and try again. | | 0081 | Invalid Sort Order | The 'sort\_order' field must be 'asc' or 'desc'. Please provide a valid sort order and try again. | | 0082 | Invalid Query Parameter | One or more query parameters are in an incorrect format. Please check the following parameters '\{\{parameter}}' and ensure they meet the required format before trying again. | | 0083 | Invalid Date Range Error | Both 'initialDate' and 'finalDate' fields are required and must be in the 'yyyy-mm-dd' format. Please provide valid dates and try again. | | 0084 | Duplicate Idempotency Key | The idempotency key '\{\{key}}' is already in use. Please provide a unique key and try again. | | 0085 | Account Alias Not Found | The provided account Alias does not exist in our records. Please verify the account Alias and try again. | | 0086 | Race condition detected | A race condition was detected while processing your request. Please try again | | 0087 | Transaction Revert already exist | Transaction revert already exists. Please try again. | | 0088 | Transaction is already a reversal | Transaction is already a reversal. Please try again | | 0089 | Transaction can't be reverted | Transaction can't be reverted. Please try again | | 0090 | Transaction Ambiguous Account | Transaction cannot use the same account in sources and destinations. | | 0091 | ID cannot be used as the parent ID | The provided ID cannot be used as the parent ID. Please choose a different one. | | 0092 | No Balances Found | No balances were found for the given query parameters. Please adjust your filters and try again. | | 0093 | Balance Cannot Be Deleted | Balance cannot be deleted because it still has funds in it or has active operations. | | 0094 | Invalid Request Body | The request body is invalid or could not be parsed. Please check JSON structure and field types. | | 0095 | Message Broker Unavailable | The server encountered an unexpected error while connecting to Message Broker. Please try again later or contact support. | | 0096 | Invalid Account Alias | The alias contains invalid characters. Please verify the alias value and try again. | | 0097 | Overflow Error | The request could not be completed due to an overflow. Please check the values and try again. | | 0098 | Invalid Pending Transaction | External accounts cannot be used for pending transactions in source operations. Please check the accounts and try again. | | 0099 | Transaction Status Invalid | Transaction cannot be reverted because its status is not APPROVED | | 0100 | Operation Route Title Already Exists | An operation route with the specified title already exists. Please use a different title and try again. | | 0101 | Operation Route Not Found | The provided operation route does not exist in our records. Please verify the operation route and try again. | | 0102 | No Operation Routes Found | No operation routes were found for the given query parameters. Please adjust your filters and try again. | | 0103 | Invalid Operation Route Type | The provided operation route type is not valid. Accepted types are 'source' or 'destination'. Please provide a valid operation type. | | 0104 | Missing Operation Routes in Request | Your request must include at least one operation route of each type (debit and credit). Please refer to the documentation to ensure these fields are properly populated. | | 0105 | Transaction Route Not Found | The provided transaction route does not exist in our records. Please verify the transaction route and try again. | | 0106 | No Transaction Routes Found | No transaction routes were found for the given query parameters. Please adjust your filters and try again. | | 0107 | Operation Route Linked to Transaction Routes | The operation route cannot be deleted because it is linked to one or more transaction routes. Please remove the operation route from all transaction routes before attempting to delete it. | | 0108 | Duplicate Account Type Key Value Error | An account type with the specified key value already exists for this organization and ledger. Please use a different key value or update the existing account type. | | 0109 | Account Type Not Found Error | The account type you are trying to access does not exist or has been removed. | | 0110 | No Account Types Found | No account types were found for the specified ledger. Please create an account type or verify the ledger exists. | | 0111 | Invalid Account Rule Type | The provided 'account.ruleType' is not valid. Accepted types are 'alias' or 'account\_type'. Please provide a valid rule type. | | 0112 | Invalid Account Rule Value | The provided 'account.validIf' is not valid. Please provide a string for 'alias' or an array of strings for 'account\_type'. | | 0113 | Corrupted Account Rule | The account rule data in the operation route is internally inconsistent (unknown rule type or malformed validIf value). This indicates a data integrity issue — please verify the operation route configuration. | | 0114 | Transaction Route Not Informed | The transaction route is not informed. Please inform the transaction route for this transaction. | | 0115 | Invalid Transaction Route ID | The provided transaction route ID is not a valid UUID format. Please provide a valid UUID for the transaction route. | | 0116 | Accounting Route Count Mismatch | The operation routes count does not match the transaction route cache. Please verify your route configuration. | | 0117 | Accounting Route Not Found | The operation route ID was not found in the transaction route cache. Please verify the route configuration. | | 0118 | Accounting Alias Validation Failed | The operation alias does not match the expected alias defined in the accounting route rule. | | 0119 | Accounting Account Type Validation Failed | The account type does not match any of the expected account types defined in the accounting route rule. | | 0120 | Invalid Characters | The field 'keyValue' contains invalid characters. Use only letters, numbers, underscores and hyphens. | | 0121 | Invalid Future Date Error | The transactionDate cannot be a future date. Please provide a valid date. | | 0122 | Invalid Field for Pending Transaction Error | Pending transactions do not support the transactionDate field. To proceed, please remove it from your request. | | 0123 | Duplicated Alias Key Value Error | An account alias with the specified key value already exists for this organization and ledger. Please use a different key value. | | 0124 | Additional Balance Creation Not Allowed | Additional balances are not allowed for external account type. | | 0125 | Invalid Transaction Value | Negative or zero transaction values are not allowed. The send\_value must be greater than zero. | | 0126 | Default Balance Not Found | Default balance must be created first for this account. | | 0127 | Account Creation Failed | The account could not be created because the default balance could not be created. Please try again. | | 0128 | Transaction Backup Cache Failed | The server encountered an unexpected error while adding the transaction to the backup cache. Please try again later or contact support. | | 0129 | Transaction Backup Cache Marshal Failed | The server encountered an unexpected error while serializing the transaction for the backup cache. Please try again later or contact support. | | 0131 | Invalid Datetime Format Error | The parameter is in the incorrect format. Please use the correct datetime format and try again. | | 0132 | Metadata Index Already Exists | A metadata index with the same key already exists for this entity. Please use a different key from the existing index. | | 0133 | Metadata Index Not Found | The specified metadata index does not exist. Please verify the index name and try again. | | 0134 | Invalid Metadata Key Format | The metadata key format is invalid. Keys must start with a letter and contain only alphanumeric characters and underscores. | | 0135 | Metadata Index Limit Exceeded | The maximum number of metadata indexes has been reached for this entity. Please delete unused indexes before creating new ones. | | 0136 | Metadata Index Creation Failed | The metadata index could not be created. Please try again later or contact support. | | 0137 | Metadata Index Deletion Forbidden | System indexes cannot be deleted. Please ensure you are deleting a custom metadata index. | | 0138 | Invalid Entity Name | The provided entity name is not valid. | | 0139 | Transaction Backup Cache Retrieval Failed | The transaction could not be retrieved from the backup cache internal function. Please ensure the transaction exists in the cache before processing balances. | | 0140 | Invalid Timestamp | The provided timestamp is invalid. Timestamps cannot be in the future. | | 0141 | No Balance Data at Date | No balance data is available at the specified date. | | 0142 | Missing Required Query Parameter | The required query parameter is missing from the request. | | 0143 | Payload Too Large | The request payload exceeds the maximum allowed size of 64KB. | | 0144 | JSON Nesting Depth Exceeded | The JSON payload exceeds the maximum allowed nesting depth of 10 levels. Please flatten your data structure. | | 0145 | JSON Key Count Exceeded | The JSON payload exceeds the maximum allowed number of keys (100). Please reduce the number of keys in your payload. | | 0146 | Tenant Not Provisioned | The tenant has not been provisioned. Please ensure the tenant is properly configured. | | 0147 | Unknown Settings Field | The settings contain an unknown field. Only known settings fields are allowed. | | 0148 | Invalid Settings Field Type | A settings field has an invalid type. Please verify the expected type for the field. | | 0149 | Settings Field at Root Level | A settings field must be nested under the correct parent key. Please verify the expected structure. | | 0150 | Route Not Bidirectional | The operation route does not allow bidirectional transactions. Only routes with operation type 'bidirectional' can be reverted. | | 0151 | Missing Counterpart | The route requires at least one debit and one credit operation (counterpart validation). | | 0152 | Direction Route Mismatch | The operation direction is not compatible with the route operation type. | | 0153 | No Source for Action | The action requires at least one source operation route. Please add a source route for this action. | | 0154 | No Destination for Action | The action requires at least one destination operation route. Please add a destination route for this action. | | 0155 | Invalid Route Action | The action is not a valid route action. Please provide a valid action value. | | 0157 | No Routes for Action | No routes found for the action. Please configure operation routes for this action in the transaction route. | | 0158 | Too Many Operation Routes | The number of operation routes exceeds the maximum allowed. Please reduce the number of operation routes and try again. | | 0159 | Service Suspended | The service is suspended for this tenant. | | 0160 | Tenant Not Found | The specified tenant was not found. | | 0161 | Tenant Service Unavailable | The tenant service is unavailable. Please try again later. | | 0162 | Scenario Not Allowed For Direction | The accounting scenario is not allowed for the specified operation direction. | | 0163 | Reserve Group Incomplete | The reserve group (hold, commit, cancel) must be complete. | | 0164 | Direct Scenario Required | The direct scenario is required when other scenarios are present. | | 0165 | Revert Only Bidirectional | The revert scenario is only allowed for bidirectional operation routes. | | 0166 | Accounting Entry Field Required | A required field is missing in the accounting entry. | | 0167 | Overdraft Limit Exceeded | The transaction would exceed the balance's configured overdraft limit. Reduce the transaction amount or increase the overdraft limit. | | 0168 | Direct Operation on Internal Balance | A user-initiated operation targeted a balance with internal scope. Internal balances (e.g., overdraft companions) cannot be operated on directly. | | 0169 | Deletion of Internal Balance | A delete request targeted a balance with internal scope. Internal balances are system-managed and cannot be deleted. | | 0170 | Reserved Balance Key | The balance key provided is reserved for system use (e.g., `overdraft`). Please choose a different key. | | 0171 | Invalid Balance Direction | The balance direction provided is not valid. Accepted values are `credit` and `debit`. | | 0172 | Invalid Balance Settings | The balance settings payload failed validation. Check that `overdraftLimit` is present and positive when `overdraftLimitEnabled` is true, and absent when it is false. | | 0173 | Overdraft Limit Below Usage | The new overdraft limit is lower than the current overdraft usage. Repay part of the overdraft or set a higher limit. | | 0174 | Stale Balance Version | The balance version in the request no longer matches the current version. Another transaction mutated the balance — please retry the operation. | | 0175 | Update of Internal Balance | A PATCH request targeted a balance with internal scope. Internal balances are system-managed and cannot be modified through the public API. | | 0484 | Route Not Found | The requested route does not exist. Please verify the HTTP method and path and try again. | | 0485 | Method Not Allowed | The HTTP method is not allowed for the requested route. Please verify the method and try again. | | 0497 | Request Header Fields Too Large | The request header fields are too large. Please reduce the size of the request headers and try again. | # List Accounts by Holder Source: https://docs.lerian.studio/en/reference/midaz/list-accounts-by-holder en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/holders/{holder_id}/accounts # Get all billing packages Source: https://docs.lerian.studio/en/reference/midaz/list-billing-packages en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/billing-packages # List Instruments Source: https://docs.lerian.studio/en/reference/midaz/list-instruments en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/instruments # Get all packages Source: https://docs.lerian.studio/en/reference/midaz/list-packages en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/packages # List Protection Audit Events Source: https://docs.lerian.studio/en/reference/midaz/list-protection-audit-events en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/protection/audit # Open a holder-owned account (with optional instrument) Source: https://docs.lerian.studio/en/reference/midaz/open-a-holder-account en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/ledgers/{ledger_id}/holders/{holder_id}/accounts # Calculate Billing Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/calculate-billing en/openapi/v3-current/fees-engine.yaml post /v1/billing/calculate Calculate billing charges for a given period. Evaluates all active billing packages matching the specified type and returns transaction payloads ready for Midaz. All-or-nothing — if any package fails, no results are returned. # Calculate Fees for a Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/calculate-fees en/openapi/v3-current/fees-engine.yaml post /v1/fees Use this endpoint to calculate and retrieve the fees associated with a specific package for a given transaction. # Create a Billing Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/create-billing-package en/openapi/v3-current/fees-engine.yaml post /v1/billing-packages Create a billing package for period-based charges. Supports two types — `volume` (transaction count-based) and `maintenance` (per-account fixed fee). # Create a Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/create-package en/openapi/v3-current/fees-engine.yaml post /v1/packages Use this endpoint to create packages along with their associated fees in a single request. # Delete a Billing Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/delete-billing-package en/openapi/v3-current/fees-engine.yaml delete /v1/billing-packages/{id} Soft-delete a billing package. Marked with `deletedAt` timestamp and excluded from standard queries. # Delete a Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/delete-package en/openapi/v3-current/fees-engine.yaml delete /v1/packages/{id} Use this endpoint to delete previously created packages. Once deleted, the package and its associated fees will no longer be available. # Fees Engine error list Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/fee-engine-error-list The **Fees Engine** returns consistent and structured error responses. This helps you quickly understand what went wrong and how to fix it. **Error format** ```jsonon JSON theme={null} { "code": "", "title": "", "message": "" } ``` **Field definitions** * **`code`** – A stable, unique identifier for the error. Useful for programmatic handling and support requests. * **`title`** – A short, human-readable summary of the issue. * **`message`** – Detailed guidance to help you resolve the error. This structure ensures you always get actionable feedback when something doesn’t go as expected. ## Fees Engine errors *** The following errors can occur when interacting with the Fees Engine endpoints. Each error follows our standard structure, making it easier to debug and respond to issues programmatically. Refer to the table below for a list of possible error codes, what they mean, and how to resolve them. ### 400 | `code` | `title` | `message` | | -------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FEE-0001 | Unexpected fields in the request | The request body contains more fields than expected. Please send only the allowed fields as per the documentation. The unexpected fields are listed in the fields object. | | FEE-0002 | Missing fields in request | Your request is missing one or more required fields. Please refer to the documentation to ensure all necessary fields are included in your request. | | FEE-0003 | Bad request | The server could not understand the request due to malformed syntax. Please check the listed fields and try again. | | FEE-0005 | Calculation field type invalid | The Calculation field type is invalid. Values can only be percentage or flat. | | FEE-0006 | Invalid query parameter | One or more query parameters are in an incorrect format. Please check the following parameters 'parametros\_erros' and ensure they meet the required format before trying again. | | FEE-0007 | Invalid date format error | The 'initialDate', 'finalDate', or both are in the incorrect format. Please use the 'yyyy-mm-dd' format and try again. | | FEE-0008 | Invalid final date error | The 'finalDate' cannot be earlier than the 'initialDate'. Please verify the dates and try again. | | FEE-0009 | Date range exceeds limit error | The range between 'initialDate' and 'finalDate' exceeds the permitted limit of %v months. Please adjust the dates and try again. | | FEE-0010 | Invalid date range error | Both 'initialDate' and 'finalDate' fields are required and must be in the 'yyyy-mm-dd' format. Please provide valid dates and try again. | | FEE-0011 | Pagination limit exceeded | The pagination limit exceeds the maximum allowed of %v items per page. Please verify the limit and try again. | | FEE-0012 | Entity not found | No entity was found for the given ID. Please make sure to use the correct ID for the entity you are trying to manage. | | FEE-0013 | Invalid fee priority | The priority field in fees is invalid. Field can not be repeated. | | FEE-0014 | Account not found on Midaz | Failed to find account '%v' on Midaz. Please check the account alias passed. | | FEE-0015 | minimumAmount greater than maximumAmount | minimumAmount value is greater than maximumAmount. | | FEE-0016 | Invalid path parameter | Path parameters is in an incorrect format. Please check the following parameter and ensure they meet the required format before trying again. | | FEE-0017 | Nothing to update | No updatable fields were provided. Please include at least one field to update. | | FEE-0018 | Package already exists | There is a Package with same index values for \[organizationId, ledgerId, segmentId, transactionRoute, minimumAmount, maximumAmount] | | FEE-0019 | Invalid header parameter | One or more headers parameters are in an incorrect format. Please check the following parameters and ensure they meet the required format before trying again. | | FEE-0020 | Missing header | Headers parameters is required. Please check the following header parameters %v and ensure they are passing the values correctly. | | FEE-0021 | Invalid transaction type | Only one transaction type ('amount', 'share', or 'remaining') must be specified in the '%v' field for each entry. Please review your input and try again. | | FEE-0022 | Failed to calculate fee | Error to make the calculation of a fee about a transaction. | | FEE-0023 | Missing calculation model | The calculation model is required to Fee. | | FEE-0024 | originalAmount is required when priority is one | For Priority equals to one, referenceAmount must be 'originalAmount' for fee. | | FEE-0025 | Failed to apply rule: flatFee or percentual | applicationRule flatFee or percentual must have exactly 1 calculation for Fee. | | FEE-0026 | Invalid calculation type: percentual | The calculation type percentual must be 'percentage' for Fee. | | FEE-0027 | Invalid calculation type: flatFee | The calculation type flatFee must be 'flat' for Fee. | | FEE-0028 | Missing required fee fields | All fields of a new Fee must be filled. Please check again the payload passed. | | FEE-0029 | Calculation field is required for fee | Please fill the Calculation object correctly. All calculation fields must be filled. | | FEE-0030 | referenceAmount is not valid | Field reference amount must be originalAmount or afterFeesAmount. | | FEE-0031 | Invalid applicationRule | Field application rule must be maxBetweenTypes, flatFee or percentual. | | FEE-0032 | Invalid calculation type | Field calculation type must be percentage or flat. | | FEE-0033 | maximumAmount less than minimumAmount | maximumAmount value is less than minimumAmount. | | FEE-0034 | Package filtering error | Failed to filter a single package by transactionRoute, segmentID, and maximum/minimum amount. Either no package was found or multiple packages matched the criteria. | | FEE-0035 | Package amount range overlap | The maximumAmount and minimumAmount of the new package overlap with the amount range of an existing package. | | FEE-0036 | Invalid sort order | Invalid 'sort\_order' value. Expected 'asc' or 'desc'. | | FEE-0037 | Error to distribute values | Error to try distribute the values of send object. Please check the data passed. | | FEE-0038 | Failed to apply rule: maxBetweenTypes | applicationRule maxBetweenTypes must have more than 1 calculation for Fee %v. | | FEE-0039 | Invalid segmentID | The specified segmentID is not a valid UUID. Please check the value passed. | | FEE-0040 | Invalid ledgerID | The specified ledgerID is not a valid UUID. Please check the value passed. | | FEE-0041 | Unmarshalling error | Invalid value for field '%s': expected type '%s', but got '%s'. | | FEE-0042 | Error to convert values | The value of the field %s is invalid. Remember to use dot (.) as decimal separator instead of comma (,). Example: use 1000.50 instead of 1000,50. | | FEE-0043 | originalAmount is required when isDeductibleFrom is true | For isDeductibleFrom `true`, referenceAmount must be ‘originalAmount' for '%s’. | | FEE-0044 | applicationRule invalid value | applicationRule is invalid, Err: %v. | | FEE-0045 | forbidden to access Midaz | Error to access Midaz. Please check the client credentials to access Midaz for creditAccount %v validation. | | FEE-0046 | calculation value percentage invalid | Calculation value is invalid can not utrapass 100%%. Please check the calculation value for Fee %v.. | | FEE-0047 | calculation value flat invalid | Calculation value is invalid can not utrapass the minimum amount %v. Please check the calculation value for Fee %v. | | FEE-0048 | error to access Midaz | Error to access Midaz to validate creditAccount %v. Please check the client credentials to access Midaz. | | FEE-0049 | deductible value forbidden | Can not update deductible value to true. The calculation value is bigger than 100%% for Fee %v. | | FEE-0050 | deductible value forbidden | Can not update deductible value to true. Calculation value is bigger than the minimum amount %v for Fee %v. | | FEE-0051 | Invalid page parameter | Query parameter page is invalid. The page must be greater than 0. | | FEE-0053 | Invalid billing package type | The billing package type is invalid. Valid types are 'volume' and 'maintenance'. | | FEE-0054 | Missing volume fields | Volume billing packages require: eventFilter (transactionRoute, status), pricingModel, tiers, assetCode, debitAccountAlias, and creditAccountAlias. | | FEE-0055 | Missing maintenance fields | Maintenance billing packages require: feeAmount, assetCode, maintenanceCreditAccount, and accountTarget. | | FEE-0056 | Invalid pricing model | The pricing model is invalid. Valid models are 'tiered' and 'fixed'. | | FEE-0057 | Invalid pricing tier | Pricing tier configuration is invalid. Each tier must have minQuantity and unitPrice. Tiers must not overlap. | | FEE-0058 | Billing route overlap | A billing package already exists for this organization, ledger, and transaction route combination. | | FEE-0063 | Invalid billing period | The billing period format is invalid. Use 'YYYY-MM' (monthly), 'YYYY-Www' (weekly), or 'YYYY-MM-DD' (daily) format. | | FEE-0064 | Invalid free quota | The free quota value is invalid. Must be a non-negative integer. | | FEE-0065 | Invalid discount tier | Discount tier configuration is invalid. Each tier must have minQuantity and discountPercentage between 0 and 100. | | FEE-0067 | Invalid count mode | The count mode is invalid. Valid modes are 'perRoute' and 'perAccount'. | | FEE-0069 | Invalid account target | The accountTarget must have exactly one of segmentId, portfolioId, or aliases. Aliases limited to 100 entries. | | FEE-0070 | Invalid fee amount | The fee amount is invalid. It must be a positive value greater than zero. | ### 404 | `code` | `title` | `message` | | -------- | -------------------------- | -------------------------------------------------------------------------------- | | FEE-0052 | Billing package not found | No billing package was found for the given ID. | | FEE-0059 | Target account not found | The target account was not found or is inactive in Midaz. | | FEE-0061 | No active billing packages | No active billing packages were found for the specified organization and ledger. | ### 422 | `code` | `title` | `message` | | -------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | FEE-0060 | Billing calculation failed | Failed to calculate billing. Check the error details for the specific package and resource that caused the failure. | | FEE-0062 | Segment resolution failed | Failed to resolve accounts for the configured segment. Verify the segment exists and try again. | | FEE-0068 | Service dependency unavailable | A required service is temporarily unavailable. Please try again later. | ### 500 | `code` | `title` | `message` | | -------- | --------------------- | -------------------------------------------------------------------------------------- | | FEE-0004 | Internal Server Error | The server encountered an unexpected error. Please try again later or contact support. | # List Billing Packages Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/list-billing-packages en/openapi/v3-current/fees-engine.yaml get /v1/billing-packages Retrieve all billing packages for the given organization and ledger. # List Packages Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/list-packages en/openapi/v3-current/fees-engine.yaml get /v1/packages Use this endpoint to retrieve all registered packages along with their associated fees. # Retrieve a Billing Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/retrieve-billing-package en/openapi/v3-current/fees-engine.yaml get /v1/billing-packages/{id} Retrieve a specific billing package by its ID. # Retrieve a Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/retrieve-package en/openapi/v3-current/fees-engine.yaml get /v1/packages/{id} Use this endpoint to retrieve a specific package and its associated fees. # Estimate Transaction Fees Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/simulate-fees en/openapi/v3-current/fees-engine.yaml post /v1/estimates Use this endpoint to estimate the fees for a transaction. It helps validate expected charges before processing. # Update a Billing Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/update-billing-package en/openapi/v3-current/fees-engine.yaml patch /v1/billing-packages/{id} Partially update a billing package. Only `label`, `description`, and `enable` can be updated. Send only the fields you want to change. # Update a Package Source: https://docs.lerian.studio/en/reference/midaz/plugins/fees-engine/update-package en/openapi/v3-current/fees-engine.yaml patch /v1/packages/{id} Use this endpoint to update a specific package. Send only the fields you want to change — any fields not included will remain unchanged. Ideal for partial updates without overwriting the entire configuration. # Provision an Organization for Envelope Encryption Source: https://docs.lerian.studio/en/reference/midaz/provision-encryption en/openapi/v3-current/ledger.yaml post /v1/organizations/{organization_id}/encryption/provision # Get billing package Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-billing-package en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/billing-packages/{billing_package_id} # Get package Source: https://docs.lerian.studio/en/reference/midaz/retrieve-a-package en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/packages/{package_id} # Get an AssetRate by External ID Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-asset-rate en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/{external_id} # Retrieve Instrument details Source: https://docs.lerian.studio/en/reference/midaz/retrieve-an-instrument en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} # Get an AssetRate by the Asset Code Source: https://docs.lerian.studio/en/reference/midaz/retrieve-asset-rates-by-code en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/ledgers/{ledger_id}/asset-rates/from/{asset_code} # Get Provisioning Status Source: https://docs.lerian.studio/en/reference/midaz/retrieve-encryption-status en/openapi/v3-current/ledger.yaml get /v1/organizations/{organization_id}/encryption/status # Update a billing package Source: https://docs.lerian.studio/en/reference/midaz/update-a-billing-package en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/billing-packages/{billing_package_id} # Update a package Source: https://docs.lerian.studio/en/reference/midaz/update-a-package en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/packages/{package_id} # Update an Instrument Source: https://docs.lerian.studio/en/reference/midaz/update-an-instrument en/openapi/v3-current/ledger.yaml patch /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} # Get a report Source: https://docs.lerian.studio/en/reference/reporter/check-report-status en/openapi/v3-current/reporter.yaml get /v1/reports/{id} Returns report information for the given ID. # Create a deadline Source: https://docs.lerian.studio/en/reference/reporter/create-deadline en/openapi/v3-current/reporter.yaml post /v1/deadlines Creates a deadline and returns the persisted entity. Use this protected endpoint to create a deadline through the Reporter API. # Create a report Source: https://docs.lerian.studio/en/reference/reporter/create-report en/openapi/v3-current/reporter.yaml post /v1/reports Creates a report from an existing template and dispatches its generation. # Delete a deadline Source: https://docs.lerian.studio/en/reference/reporter/delete-deadline en/openapi/v3-current/reporter.yaml delete /v1/deadlines/{id} Soft-deletes a deadline. Use this protected endpoint to soft-delete a deadline. Deleted deadlines are excluded from standard retrieval and listing. # Delete a template Source: https://docs.lerian.studio/en/reference/reporter/delete-template en/openapi/v3-current/reporter.yaml delete /v1/templates/{id} Deletes the template identified by the given ID. # Set deadline delivery state Source: https://docs.lerian.studio/en/reference/reporter/deliver-deadline en/openapi/v3-current/reporter.yaml patch /v1/deadlines/{id}/deliver Marks a deadline as delivered or reverts its delivery state. Use this protected endpoint to update a deadline's delivery state. Send `delivered: true` to mark it as delivered or `false` to clear it. # Download a report Source: https://docs.lerian.studio/en/reference/reporter/download-report en/openapi/v3-current/reporter.yaml get /v1/reports/{id}/download Downloads the generated report file. # Generate template code Source: https://docs.lerian.studio/en/reference/reporter/generate-template-code en/openapi/v3-current/reporter.yaml post /v1/templates/generate-code Converts template-builder blocks into Pongo2 template code and mapped fields. # Get system metrics Source: https://docs.lerian.studio/en/reference/reporter/get-metrics en/openapi/v3-current/reporter.yaml get /v1/metrics Returns aggregate template, report, data-source, and report-error counters. # Get the streaming event manifest Source: https://docs.lerian.studio/en/reference/reporter/get-streaming-events en/openapi/v3-current/reporter.yaml get /v1/streaming/events Returns the static catalog of business events Reporter can publish without disclosing broker topology. # Get template block configuration Source: https://docs.lerian.studio/en/reference/reporter/list-block-definitions en/openapi/v3-current/reporter.yaml get /v1/templates/blocks-config Returns the block definitions supported by the template builder. # List data sources Source: https://docs.lerian.studio/en/reference/reporter/list-data-sources en/openapi/v3-current/reporter.yaml get /v1/data-sources Returns every data source connected to Reporter. # Get template filters Source: https://docs.lerian.studio/en/reference/reporter/list-filter-definitions en/openapi/v3-current/reporter.yaml get /v1/templates/filters Returns the filters supported by the template builder. # List templates Source: https://docs.lerian.studio/en/reference/reporter/list-templates en/openapi/v3-current/reporter.yaml get /v1/templates Returns templates using the existing filter and pagination semantics. # Reporter API quick start Source: https://docs.lerian.studio/en/reference/reporter/reporter-developer-quick-start Get Reporter running in minutes: upload your first template, generate a report, and download the finished output with cURL against the Reporter API. **This guide is intended for developers.** If you're looking for a business-level overview of what Reporter does, see [What is Reporter?](/en/reporter/what-is-reporter). Get Reporter running in minutes. This guide walks you through the complete journey, from uploading your first template to downloading a generated report. ## Before you begin *** You need: * A running Reporter instance * A valid authentication token (if Access Manager is enabled) * A `.tpl` template file ready to upload All examples use `cURL`. Replace `$TOKEN` with your authentication token and `https://reporter.example.com` with your Reporter URL. ## Step 1: Upload a template *** Upload a `.tpl` file that defines the structure and content of your report. The template content must match the selected output format: HTML for `HTML` and `PDF`, XML for `XML`, CSV for `CSV`, and non-empty text for `TXT`. The uploaded file itself must have a `.tpl` extension. API reference: [Upload template](/en/reference/reporter/upload-template) ```bash cURL theme={null} curl -X POST "https://reporter.example.com/v1/templates" \ -H "Authorization: Bearer $TOKEN" \ -F "template=@account_summary.tpl" \ -F "outputFormat=PDF" \ -F "description=Daily account summary report" ``` ```json theme={null} { "id": "0196b270-a315-7137-9408-3f16af2685e1", "outputFormat": "pdf", "description": "Daily account summary report", "fileName": "0196b270-a315-7137-9408-3f16af2685e1.tpl", "createdAt": "2026-03-05T10:00:00Z", "updatedAt": "2026-03-05T10:00:00Z" } ``` Save the template `id`. You will use it to generate reports. ### Supported output formats | Format | Use case | | ------ | ------------------------------------------ | | `CSV` | Data exports and spreadsheet integration | | `XML` | Structured data and regulatory submissions | | `HTML` | Browser-viewable reports | | `PDF` | Print-ready and shareable documents | | `TXT` | Plain text and legacy system integration | ## Step 2: Verify the template *** List your templates to confirm the upload was successful. API reference: [List templates](/en/reference/reporter/list-templates) ```bash cURL theme={null} curl -X GET "https://reporter.example.com/v1/templates" \ -H "Authorization: Bearer $TOKEN" ``` ## Step 3: Generate a report *** Submit a report generation request with the template ID and the required `filters` object. Add filters to narrow the data, or send `{}` when no filtering is needed. API reference: [Create report](/en/reference/reporter/create-report) ```bash cURL theme={null} curl -X POST "https://reporter.example.com/v1/reports" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "templateId": "0196b270-a315-7137-9408-3f16af2685e1", "filters": { "midaz_onboarding": { "account": { "created_at": { "between": ["2026-03-01", "2026-03-05"] } } } } }' ``` ```json theme={null} { "id": "0196c5c0-5044-724f-95f3-4b32076e7ad7", "templateId": "0196b270-a315-7137-9408-3f16af2685e1", "templateOutputFormat": "pdf", "templateDescription": "Daily account summary report", "filters": { "midaz_onboarding": { "account": { "created_at": { "between": ["2026-03-01", "2026-03-05"] } } } }, "status": "Processing", "metadata": null, "completedAt": null, "createdAt": "2026-03-05T10:05:00Z", "updatedAt": "2026-03-05T10:05:00Z", "deletedAt": null } ``` Save the report `id` for the next steps. ### Filter structure Filters follow the path: **data source > table > field > operator > values**. | Operator | Description | Example | | ------------ | ------------------------------- | --------------------------------------------- | | `eq` | Equal to | `{ "eq": ["active"] }` | | `gt` / `gte` | Greater than / greater or equal | `{ "gte": ["2026-01-01"] }` | | `lt` / `lte` | Less than / less or equal | `{ "lt": [1000] }` | | `between` | Value within a range | `{ "between": ["2026-03-01", "2026-03-31"] }` | | `in` / `nin` | Value in / not in a list | `{ "in": ["active", "pending"] }` | The `filters` field is required. To generate a report without filtering, pass an empty object: `"filters": {}`. ## Step 4: Check report status *** Report generation is asynchronous. Poll the status endpoint until the report is ready. API reference: [Check report status](/en/reference/reporter/check-report-status) ```bash cURL theme={null} curl -X GET "https://reporter.example.com/v1/reports/0196c5c0-5044-724f-95f3-4b32076e7ad7" \ -H "Authorization: Bearer $TOKEN" ``` | Status | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------- | | `Processing` | Reporter is querying data and rendering the template | | `Finished` | The report is ready for download | | `Partial` | Some data sections succeeded and others failed; the report's `metadata` carries per-section error codes | | `Error` | An error prevented the report from being generated. | Wait for `Finished` before proceeding to download. ## Step 5: Download the report *** Once the report is finished, download the generated file. API reference: [Download report](/en/reference/reporter/download-report) ```bash cURL theme={null} curl -X GET "https://reporter.example.com/v1/reports/0196c5c0-5044-724f-95f3-4b32076e7ad7/download" \ -H "Authorization: Bearer $TOKEN" \ -o account_summary.pdf ``` The file is returned with `Content-Disposition` headers indicating the filename and format. ## Step 6: Explore data sources *** To understand what data is available for your templates, list the configured data sources. Then use `GET /v1/data-sources/{dataSourceId}` to inspect the schema of one data source. API reference: [List data sources](/en/reference/reporter/list-data-sources) | [Retrieve data source](/en/reference/reporter/retrieve-data-source) ```bash cURL theme={null} curl -X GET "https://reporter.example.com/v1/data-sources" \ -H "Authorization: Bearer $TOKEN" ``` The list response identifies each data source. The detail response includes its available tables and fields, which you can reference in your templates using the `{{ datasource.table.field }}` syntax. ## Next steps *** Full overview of template syntax, tags, and filters. Practical examples for HTML, XML, and TXT templates. Detailed guide on templates, storage, and data source configuration. Complete list of error codes and how to resolve them. # Reporter error list Source: https://docs.lerian.studio/en/reference/reporter/reporter-error-list Reporter returns consistent, structured error responses. Look up error codes, HTTP statuses, and remediation steps to resolve API failures quickly. **Error format** Reporter returns errors as RFC 9457 problem details with the `application/problem+json` media type: ```json JSON theme={null} { "type": "https://errors.lerian.studio/v1/RPT-0012", "title": "Bad Request", "status": 400, "detail": "The specified templateID is not a valid UUID. Please check the value passed.", "code": "RPT-0012" } ``` **Field definitions** * **`type`** – A URI that identifies the error in the Lerian error catalog. Built as `https://errors.lerian.studio/v1/`. * **`title`** – The HTTP status text (for example, `Bad Request`). * **`status`** – The HTTP status code. * **`detail`** – Detailed guidance to help you resolve the error. For `5xx` responses, the detail is always sanitized to `internal error` so no internal cause leaks; use `code` to branch programmatically. * **`code`** – A stable, unique identifier for the error (`RPT-NNNN`). Useful for programmatic handling and support requests. * **`errors`** – Optional list of per-field validation details, each with a `message` and a `location`. Some messages contain placeholders such as `%v` or `%s` — Reporter replaces them with the specific values from your request. ## Reporter errors *** The following errors can occur when interacting with the Reporter endpoints. Refer to the tables below for the possible error codes, what they mean, and how to resolve them. ## 400 — Validation errors *** | `code` | Description | `detail` | | -------- | ------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | RPT-0001 | Missing required fields | One or more required fields are missing. Please ensure all required fields are included. | | RPT-0002 | Invalid file format | The uploaded file must be a .tpl file. Other formats are not supported. | | RPT-0003 | Invalid output format | The outputFormat field must be one of: html, csv, or xml. | | RPT-0004 | Invalid header | One or more header values are missing or incorrectly formatted. Please verify required headers %v. | | RPT-0005 | Invalid File Uploaded | The file you submitted is invalid. Please check the uploaded file with error: %v | | RPT-0006 | Error File Empty | The file you submitted is empty. Please check the uploaded file. | | RPT-0007 | Error File Content Invalid | The file content is invalid because is not %s. Please check the uploaded file. | | RPT-0008 | Invalid Map Fields | The field on template file is invalid. Invalid field %s on %s. | | RPT-0009 | Invalid Path Parameter | Path parameters is in an incorrect format. Please check the following parameter %v and ensure they meet the required format before trying again. | | RPT-0010 | Update Output format without template File | Can not update output format without passing template file. Please check information passed and try again. | | RPT-0012 | Invalid templateID | The specified templateID is not a valid UUID. Please check the value passed. | | RPT-0013 | Invalid ledgerID | The specified ledgerID inside ledger ID list is not a valid UUID. Please check the value passed %v. | | RPT-0014 | Missing required fields | The fields mapped on template file are missing in the table schema or may be empty. Please check the fields passed: '%v'. | | RPT-0015 | Unexpected Fields in the Request | The request body contains more fields than expected. Please send only the allowed fields as per the documentation. The unexpected fields are listed in the fields object. | | RPT-0016 | 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. | | RPT-0017 | Bad Request | The server could not understand the request due to malformed syntax. Please check the listed fields and try again. | | RPT-0019 | Invalid Query Parameter | One or more query parameters are in an incorrect format. Please check the following parameters '%v' and ensure they meet the required format before trying again. | | RPT-0023 | Invalid Date Range Error | Both 'initialDate' and 'finalDate' fields are required and must be in the 'yyyy-mm-dd' format. Please provide valid dates and try again. | | RPT-0024 | Pagination Limit Exceeded | The pagination limit exceeds the maximum allowed of %v items per page. Please verify the limit and try again. | | RPT-0025 | Invalid Sort Order | The 'sort\_order' field must be 'asc' or 'desc'. Please provide a valid sort order and try again. | | RPT-0026 | Metadata Key Length Exceeded | The metadata key %v exceeds the maximum allowed length of %v characters. Please use a shorter key. | | RPT-0027 | Metadata Value Length Exceeded | The metadata value %v exceeds the maximum allowed length of %v characters. Please use a shorter value. | | RPT-0028 | Invalid Metadata Nesting | The metadata object cannot contain nested values. Please ensure that the value %v is not nested and try again. | | RPT-0030 | Missing Schema Table | The schema table %v is missing for data source '%v'. Please check the information passed. | | RPT-0031 | Missing Data Source Table | The data source %v is missing. Please check the value passed. | | RPT-0032 | Script Tag Detected | The template file contains a script tag and is not allowed. Please check the template file and try again. | | RPT-0035 | Ambiguous Schema Reference | The table '%v' exists in multiple schemas: %v. Please use explicit schema syntax: database:schema.table | | RPT-0036 | Schema Not Found | The schema '%v' was not found in database '%v'. Please verify the schema name. | | RPT-0037 | Table Not Found in Schema | The table '%v' was not found in schema '%v' of database '%v'. Please verify the table name and schema. | | RPT-0038 | Database Not Registered | The database '%v' is not registered. Please verify the datasource configuration. | | RPT-0041 | Bucket Required | The storage bucket name is required. Please check the storage configuration. | | RPT-0042 | Object Key Required | The object key is required for the storage operation. | | RPT-0044 | TTL Not Supported | TTL parameter is not supported in S3 mode. Use bucket lifecycle policies instead. | | RPT-0046 | Invalid Deadline Type | The 'type' field must be 'regulatory' or 'custom'. Please provide a valid deadline type and try again. | | RPT-0047 | Invalid Deadline Frequency | The 'frequency' field must be one of: 'once', 'daily', 'weekly', 'monthly', 'semiannual', 'annual'. Please provide a valid frequency and try again. | | RPT-0048 | Invalid Deadline Color | The 'color' field must be a valid hex color code (e.g., '#FF5733'). Please provide a valid color and try again. | | RPT-0050 | Months of Year Not Applicable | The 'monthsOfYear' field is not applicable for frequency '%v'. It can only be used with 'semiannual' or 'annual' frequencies. | | RPT-0052 | Months of Year Required | The 'monthsOfYear' field is required for frequency '%v'. Please specify which months of the year the deadline should recur on. | | RPT-0054 | Months of Year Out of Range | Each value in 'monthsOfYear' must be between 1 and 12. Received invalid value: %v. | | RPT-0055 | Due Date in the Past | The 'dueDate' must be today or a future date. Please provide a date that is not in the past. | | RPT-0056 | Months of Year Count Mismatch | The number of months in 'monthsOfYear' does not match the '%v' frequency. 'semiannual' requires exactly 2 months and 'annual' requires exactly 1 month. | | RPT-0059 | Schema Validation Failed | The schema validation failed. Please verify the fields against the data source schema. | | RPT-0062 | Invalid UTF-8 Encoding | The '%v' field contains invalid UTF-8 byte sequences. Please provide valid UTF-8 text and try again. | The RPT-0003 message lists `html`, `csv`, and `xml`, but the API accepts five output formats: `HTML`, `PDF`, `CSV`, `XML`, and `TXT`. See [Upload template](/en/reference/reporter/upload-template). ## 404 — Not found *** | `code` | Description | `detail` | | -------- | ------------------------------ | :------------------------------------------------------------------------------------------------------------------------------- | | RPT-0011 | Entity Not Found | No %v entity was found for the given ID. Please make sure to use the correct ID for the entity you are trying to manage. | | RPT-0020 | Invalid Date Format Error | The 'initialDate', 'finalDate', or both are in the incorrect format. Please use the 'yyyy-mm-dd' format and try again. | | RPT-0021 | Invalid Final Date Error | The 'finalDate' cannot be earlier than the 'initialDate'. Please verify the dates and try again. | | RPT-0022 | Date Range Exceeds Limit Error | The range between 'initialDate' and 'finalDate' exceeds the permitted limit of %v months. Please adjust the dates and try again. | | RPT-0043 | Object Not Found | The requested object was not found in storage. | | RPT-0057 | Data Source Not Found | The requested data source was not found. Please verify the data source ID. | ## 409 — Conflicts *** | `code` | Description | `detail` | | -------- | --------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | | RPT-0039 | Duplicate Request In Flight | A duplicate request is currently being processed. Please wait and try again. | | RPT-0040 | Idempotency Conflict | A request with this idempotency key has already been processed. | | RPT-0045 | Duplicate Deadline | A deadline with the same name, type, due date, and frequency already exists. Please use different values or update the existing deadline. | ## 422 — Unprocessable *** | `code` | Description | `detail` | | -------- | -------------------------- | :------------------------------------------------------------- | | RPT-0029 | Report status not Finished | The Report is not ready to download. Report is processing yet. | ## 500 — Server errors *** | `code` | Description | `detail` | | -------- | --------------------- | :------------- | | RPT-0018 | Internal Server Error | internal error | Every unexpected failure during synchronous endpoint handling surfaces as HTTP 500 with `code: RPT-0018` and a sanitized `internal error` detail — internal causes never leak into the response body. Asynchronous worker and report-generation failures instead follow the report statuses and metadata described below; they are not returned to the original request as HTTP 500. ## Report-generation errors (asynchronous) *** Report generation runs asynchronously in the worker. A failed data-extraction section is not returned to the original HTTP request: the report ends with status `Error` when all sections fail or `Partial` when some fail. In either case, `metadata.error_code` is `RPT-0060`; `metadata.sections` is keyed by database name, and each failed entry contains only its classified `error_code` (`RPT-0018` for an untyped failure). Other worker failures end with status `Error` and a safe non-RPT `metadata.error_code` of `report_generation_failed`, `report_generation_timeout`, or `report_generation_canceled`; they have no `sections` map and do not retain the underlying RPT code. | `code` | Description | Meaning | | -------- | ---------------------------------- | :------------------------------------------------------------------------------------------------------------------ | | RPT-0033 | Encryption data error | Failed to decrypt CRM data during extraction. | | RPT-0034 | Communication Error with SeaweedFS | Error communicating with the file storage to download or upload a file. Please try again. | | RPT-0058 | Data Source Unavailable | The data source is currently unavailable. Results may be incomplete. | | RPT-0060 | Extraction Job Failed | The extraction job failed. Please try again later or contact support. | | RPT-0061 | Template Rendering Failed | The template could not be rendered with the provided data. This is a permanent error and will not succeed on retry. | | RPT-0063 | CRM Hash Key Not Configured | CRM hash key not configured. | | RPT-0064 | CRM Encrypt Key Not Configured | CRM encrypt key not configured. | | RPT-0065 | Record Decryption Failed | Record decryption failed. | | RPT-0066 | Cipher Init Failed | Cipher initialization failed. | | RPT-0067 | Invalid Extracted Data | The extracted data is invalid. | | RPT-0068 | Unexpected Collection Result | The data collection returned an unexpected result. | | RPT-0069 | Data Source Not Found | The data source referenced by the report was not found. | | RPT-0070 | Data Source Unavailable | The data source was unavailable during extraction. | # Get a data source Source: https://docs.lerian.studio/en/reference/reporter/retrieve-data-source en/openapi/v3-current/reporter.yaml get /v1/data-sources/{dataSourceId} Returns schema details for one connected data source. # List deadline notifications Source: https://docs.lerian.studio/en/reference/reporter/retrieve-deadline-notifications en/openapi/v3-current/reporter.yaml get /v1/deadlines/notifications Returns active deadlines inside their notification window, ordered by urgency. # List deadlines Source: https://docs.lerian.studio/en/reference/reporter/retrieve-deadlines en/openapi/v3-current/reporter.yaml get /v1/deadlines Returns a filtered page of deadlines. These endpoints power deadline management in the **Console**. You can also call them directly to integrate deadlines into your own systems. # List reports Source: https://docs.lerian.studio/en/reference/reporter/retrieve-reports en/openapi/v3-current/reporter.yaml get /v1/reports Returns reports using the existing filter and cursor pagination semantics. # Get a template Source: https://docs.lerian.studio/en/reference/reporter/retrieve-template-details en/openapi/v3-current/reporter.yaml get /v1/templates/{id} Returns template information for the given ID. # Update a deadline Source: https://docs.lerian.studio/en/reference/reporter/update-deadline en/openapi/v3-current/reporter.yaml patch /v1/deadlines/{id} Partially updates a deadline and returns the persisted entity. These endpoints power deadline management in the **Console**. You can also call them directly to integrate deadlines into your own systems. # Update a template Source: https://docs.lerian.studio/en/reference/reporter/update-templates en/openapi/v3-current/reporter.yaml patch /v1/templates/{id} Updates a template's file, output format, or description. # Create a template Source: https://docs.lerian.studio/en/reference/reporter/upload-template en/openapi/v3-current/reporter.yaml post /v1/templates Creates a report template from a multipart form upload. Need a sample to test this endpoint? Click here to download a sample .tpl file. # Validate template blocks Source: https://docs.lerian.studio/en/reference/reporter/validate-template-blocks en/openapi/v3-current/reporter.yaml post /v1/templates/validate Validates template-builder blocks and returns semantic validation errors. # Activate a Limit Source: https://docs.lerian.studio/en/reference/tracer/activate-limit en/openapi/v3-current/tracer.yaml post /v1/limits/{id}/activate Use this endpoint to activate a spending limit. Activates the limit for enforcement in validations. # Activate a Rule Source: https://docs.lerian.studio/en/reference/tracer/activate-rule en/openapi/v3-current/tracer.yaml post /v1/rules/{id}/activate Use this endpoint to activate a rule. Activates the rule for evaluation in validations. Valid transitions are DRAFT to ACTIVE and INACTIVE to ACTIVE. # Confirm a Reservation Source: https://docs.lerian.studio/en/reference/tracer/confirm-reservation en/openapi/v3-current/tracer.yaml post /v1/reservations/{id}/confirm Use this endpoint to confirm a single reservation, committing the capacity it holds — phase two of the two-phase reservation flow. Confirm is idempotent — confirming an already-confirmed reservation returns the same `CONFIRMED` status with HTTP 200. # Confirm a Transaction's Reservations Source: https://docs.lerian.studio/en/reference/tracer/confirm-reservation-by-transaction en/openapi/v3-current/tracer.yaml post /v1/reservations/transaction/{transaction_id}/confirm Use this endpoint to confirm every reservation a transaction holds in a single call, addressing them by the ledger `transactionId` — phase two of the two-phase reservation flow. The response reports how many reservations were transitioned in `flipped`. A `flipped` value of `0` is a valid, idempotent success — the transaction held no open reservations. # Create a Limit Source: https://docs.lerian.studio/en/reference/tracer/create-limit en/openapi/v3-current/tracer.yaml post /v1/limits Use this endpoint to create a spending limit with scopes array. Limits are created in DRAFT status. Use the activate endpoint to start enforcement. After creation, limitType and currency cannot be changed. # Reserve Transaction Capacity Source: https://docs.lerian.studio/en/reference/tracer/create-reservation en/openapi/v3-current/tracer.yaml post /v1/reservations Use this endpoint to reserve transaction capacity against spending limits — phase one of the two-phase reservation flow. Provide the ledger `transactionId` the reservation is keyed on; the response returns one reservation ID per counter-backed limit that must later be confirmed or released. When a spending limit would be exceeded, the response is returned with `denied` set to `true` and no capacity is held. Reserves are idempotent by `transactionId` — retrying the same reserve returns the same handle. # Create a Rule Source: https://docs.lerian.studio/en/reference/tracer/create-rule en/openapi/v3-current/tracer.yaml post /v1/rules Use this endpoint to create a validation rule with a CEL expression and scopes array. Rules are always created in DRAFT status. Use the activate endpoint to start evaluation. # Deactivate a Limit Source: https://docs.lerian.studio/en/reference/tracer/deactivate-limit en/openapi/v3-current/tracer.yaml post /v1/limits/{id}/deactivate Use this endpoint to deactivate a spending limit. Pauses the limit from being enforced in validations. # Deactivate a Rule Source: https://docs.lerian.studio/en/reference/tracer/deactivate-rule en/openapi/v3-current/tracer.yaml post /v1/rules/{id}/deactivate Use this endpoint to deactivate a rule. Pauses the rule from being evaluated in validations. Valid transitions are ACTIVE to INACTIVE and DRAFT to INACTIVE. # Delete a Limit Source: https://docs.lerian.studio/en/reference/tracer/delete-limit en/openapi/v3-current/tracer.yaml delete /v1/limits/{id} Use this endpoint to soft-delete a spending limit. Only DRAFT and INACTIVE limits can be deleted. ACTIVE limits must be deactivated first. The limit is preserved in the audit trail. # Delete a Rule Source: https://docs.lerian.studio/en/reference/tracer/delete-rule en/openapi/v3-current/tracer.yaml delete /v1/rules/{id} Use this endpoint to soft-delete a rule. Only DRAFT and INACTIVE rules can be deleted. ACTIVE rules must be deactivated first. The rule is preserved in the audit trail. # Draft a Limit Source: https://docs.lerian.studio/en/reference/tracer/draft-limit en/openapi/v3-current/tracer.yaml post /v1/limits/{id}/draft Use this endpoint to transition a limit from INACTIVE back to DRAFT. Allows re-editing a previously deactivated limit before re-activating it. # Draft a Rule Source: https://docs.lerian.studio/en/reference/tracer/draft-rule en/openapi/v3-current/tracer.yaml post /v1/rules/{id}/draft Use this endpoint to transition a rule from INACTIVE back to DRAFT. Allows re-editing a previously deactivated rule before re-activating it. # List Audit Events Source: https://docs.lerian.studio/en/reference/tracer/list-audit-events en/openapi/v3-current/tracer.yaml get /v1/audit-events Use this endpoint to list audit events with filters and cursor-based pagination. Designed for SOX/GLBA compliance reporting. # List Limits Source: https://docs.lerian.studio/en/reference/tracer/list-limits en/openapi/v3-current/tracer.yaml get /v1/limits Use this endpoint to list spending limits with cursor-based pagination and optional filters. DELETED limits are not returned in listings. # List Rules Source: https://docs.lerian.studio/en/reference/tracer/list-rules en/openapi/v3-current/tracer.yaml get /v1/rules Use this endpoint to list validation rules with cursor-based pagination and optional filters. DELETED rules are not returned in listings. # List Transaction Validations Source: https://docs.lerian.studio/en/reference/tracer/list-validations en/openapi/v3-current/tracer.yaml get /v1/validations Use this endpoint to list transaction validation records with cursor-based pagination and filters. Useful for compliance reporting and trend analysis. # Release a Reservation Source: https://docs.lerian.studio/en/reference/tracer/release-reservation en/openapi/v3-current/tracer.yaml post /v1/reservations/{id}/release Use this endpoint to release a single reservation, returning the capacity it holds without committing it — phase two of the two-phase reservation flow. Release is idempotent — releasing an already-released reservation returns the same `RELEASED` status with HTTP 200. # Release a Transaction's Reservations Source: https://docs.lerian.studio/en/reference/tracer/release-reservation-by-transaction en/openapi/v3-current/tracer.yaml post /v1/reservations/transaction/{transaction_id}/release Use this endpoint to release every reservation a transaction holds in a single call, addressing them by the ledger `transactionId` — phase two of the two-phase reservation flow. The response reports how many reservations were transitioned in `flipped`. A `flipped` value of `0` is a valid, idempotent success — the transaction held no open reservations. # Retrieve an Audit Event Source: https://docs.lerian.studio/en/reference/tracer/retrieve-audit-event en/openapi/v3-current/tracer.yaml get /v1/audit-events/{id} Use this endpoint to retrieve a single audit event by its unique identifier. Designed for SOX/GLBA compliance. # Retrieve a Limit Source: https://docs.lerian.studio/en/reference/tracer/retrieve-limit en/openapi/v3-current/tracer.yaml get /v1/limits/{id} Use this endpoint to retrieve a spending limit by its unique identifier. # Retrieve Limit Usage Source: https://docs.lerian.studio/en/reference/tracer/retrieve-limit-usage en/openapi/v3-current/tracer.yaml get /v1/limits/{id}/usage Use this endpoint to retrieve the current usage snapshot for a spending limit. # Retrieve a Rule Source: https://docs.lerian.studio/en/reference/tracer/retrieve-rule en/openapi/v3-current/tracer.yaml get /v1/rules/{id} Use this endpoint to retrieve a rule by its unique identifier. # Retrieve a Transaction Validation Source: https://docs.lerian.studio/en/reference/tracer/retrieve-validation en/openapi/v3-current/tracer.yaml get /v1/validations/{id} Use this endpoint to retrieve a transaction validation record by its unique identifier. Useful for auditing and debugging validation decisions. # Tracer API quick start Source: https://docs.lerian.studio/en/reference/tracer/tracer-api-quick-start Get Tracer running in minutes: create your first rule and spending limit, validate a transaction, and review the audit trail using the Tracer REST APIs. **This guide is intended for developers.** If you're looking for a business-level overview of what Tracer does, see [What is Tracer?](/en/tracer/what-is-tracer). Get Tracer running in minutes. This guide walks you through the complete journey, from creating your first rule and spending limit to validating a transaction and reviewing the audit trail. ## Before you begin *** You need: * A running Tracer instance * Credentials for one of the two supported auth modes All examples use `cURL`. Replace `$API_KEY` with your API key (single-tenant) or `$JWT` with your Bearer token (multi-tenant), and `https://tracer.sandbox.lerian.net` with your Tracer URL. **Auth mode depends on deployment.** Single-tenant deployments use `X-API-Key`. Multi-tenant deployments (SaaS / BYOC Multi-Tenant) use `Authorization: Bearer ` — the JWT is issued by [Access Manager](/en/platform/access-manager/access-manager) and carries the `tenantId` claim. In multi-tenant mode, replace every `-H "X-API-Key: $API_KEY"` in this guide with `-H "Authorization: Bearer $JWT"`. Tracer resolves the tenant from the token automatically — never pass the tenant identifier in any other field. See [Multi-tenancy](/en/multi-tenancy) for the model. ## Step 1: Create a rule *** Create a validation rule with a CEL expression. Rules are always created in `DRAFT` status — they do not affect transactions until you activate them. API reference: [Create rule](/en/reference/tracer/create-rule) ```bash cURL theme={null} curl -X POST "https://tracer.sandbox.lerian.net/v1/rules" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{ "name": "Block high-value transactions", "description": "Deny transactions above BRL 10,000 for card payments", "expression": "amount > 1000000", "action": "DENY", "scopes": [ { "transactionType": "CARD" } ] }' ``` ```json theme={null} { "ruleId": "019c96a0-4b20-7123-9a1b-2c3d4e5f6a7b", "name": "Block high-value transactions", "description": "Deny transactions above BRL 10,000 for card payments", "expression": "amount > 1000000", "action": "DENY", "scopes": [ { "transactionType": "CARD" } ], "status": "DRAFT", "createdAt": "2026-03-05T10:00:00Z", "updatedAt": "2026-03-05T10:00:00Z" } ``` Save the `ruleId`. You will use it to activate the rule. Monetary values (transaction `amount`, spending limit `maxAmount`, and usage counters) are expressed as decimal strings, for example `"1500.00"` or `"10000.00"`. ## Step 2: Activate the rule *** Activate the rule so it is evaluated against incoming transactions. API reference: [Activate rule](/en/reference/tracer/activate-rule) ```bash cURL theme={null} curl -X POST "https://tracer.sandbox.lerian.net/v1/rules/019c96a0-4b20-7123-9a1b-2c3d4e5f6a7b/activate" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" ``` ```json theme={null} { "ruleId": "019c96a0-4b20-7123-9a1b-2c3d4e5f6a7b", "name": "Block high-value transactions", "description": "Deny transactions above BRL 10,000 for card payments", "expression": "amount > 1000000", "action": "DENY", "scopes": [ { "transactionType": "CARD" } ], "status": "ACTIVE", "createdAt": "2026-03-05T10:00:00Z", "updatedAt": "2026-03-05T10:01:00Z" } ``` The rule status changes from `DRAFT` to `ACTIVE`. ### Rule lifecycle | Status | Behavior | | ---------- | -------------------------------------------------------------- | | `DRAFT` | Created but not evaluated during validations | | `ACTIVE` | Evaluated during validation, on transactions its scope matches | | `INACTIVE` | Paused and excluded from evaluation | `INACTIVE` rules can transition back to `DRAFT` for re-editing using `POST /v1/rules/{id}/draft`. ## Step 3: Create a spending limit *** Create a spending limit to control transaction amounts by scope and time period. Like rules, limits start in `DRAFT` status. API reference: [Create limit](/en/reference/tracer/create-limit) ```bash cURL theme={null} curl -X POST "https://tracer.sandbox.lerian.net/v1/limits" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{ "name": "Daily Corporate Limit", "description": "Daily spending limit for corporate segment", "limitType": "DAILY", "maxAmount": "50000.00", "currency": "BRL", "scopes": [ { "segmentId": "019c96a0-0b4e-7079-8be0-ab6bdccf975f", "transactionType": "CARD" } ] }' ``` ```json theme={null} { "limitId": "019c96a0-4a10-7dfe-b5c1-8a1b2c3d4e5f", "name": "Daily Corporate Limit", "description": "Daily spending limit for corporate segment", "limitType": "DAILY", "maxAmount": "50000.00", "currency": "BRL", "scopes": [ { "segmentId": "019c96a0-0b4e-7079-8be0-ab6bdccf975f", "transactionType": "CARD" } ], "status": "DRAFT", "resetAt": "2026-03-06T00:00:00Z", "createdAt": "2026-03-05T10:02:00Z", "updatedAt": "2026-03-05T10:02:00Z" } ``` ### Limit types | Type | Period counting | Use case | | ----------------- | --------------------------------------------------------- | ------------------------------- | | `DAILY` | A new count starts each calendar day at 00:00 UTC | Daily spending caps | | `WEEKLY` | A new count starts each ISO week, Monday at 00:00 UTC | Weekly spending caps | | `MONTHLY` | A new count starts on the 1st of the month at 00:00 UTC | Monthly budget control | | `CUSTOM` | One count for the whole range | Fixed campaign or event windows | | `PER_TRANSACTION` | No count is kept — each transaction is checked on its own | Per-transaction maximums | Activate the limit the same way you activated the rule: ```bash cURL theme={null} curl -X POST "https://tracer.sandbox.lerian.net/v1/limits/019c96a0-4a10-7dfe-b5c1-8a1b2c3d4e5f/activate" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" ``` ## Step 4: Validate a transaction *** Send a transaction to Tracer for real-time validation against the rules and limits that apply to it. Tracer does not make external calls during evaluation, so response times stay under 80ms (p99). API reference: [Validate transaction](/en/reference/tracer/validate-transaction) ```bash cURL theme={null} curl -X POST "https://tracer.sandbox.lerian.net/v1/validations" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{ "requestId": "019c96a0-10ce-75fc-a273-dc799079a99c", "transactionType": "CARD", "subType": "debit", "amount": "1500.00", "currency": "BRL", "transactionTimestamp": "2026-03-05T10:30:00Z", "account": { "accountId": "019c96a0-0c0c-7221-8cf3-13313fb60081", "type": "checking", "status": "active" }, "segment": { "segmentId": "019c96a0-0b4e-7079-8be0-ab6bdccf975f", "name": "corporate" }, "metadata": { "channel": "MOBILE_APP" } }' ``` ```json theme={null} { "requestId": "019c96a0-10ce-75fc-a273-dc799079a99c", "validationId": "019c96a0-4a10-7dfe-b5c1-8a1b2c3d4e5f", "decision": "ALLOW", "reason": "Transaction approved", "matchedRuleIds": [], "evaluatedRuleIds": [ "019c96a0-4b20-7123-9a1b-2c3d4e5f6a7b" ], "limitUsageDetails": [ { "limitId": "019c96a0-4a10-7dfe-b5c1-8a1b2c3d4e5f", "limitAmount": "50000.00", "currentUsage": "1500.00", "exceeded": false, "period": "DAILY" } ], "processingTimeMs": 23 } ``` ### Decision types | Decision | Meaning | Your system should | | -------- | ---------------------------------------------- | ---------------------------- | | `ALLOW` | All rules passed, all limits within threshold | Proceed with the transaction | | `DENY` | A deny rule matched or a limit was exceeded | Block the transaction | | `REVIEW` | A review rule matched, no deny rules triggered | Route to manual review | Tracer returns decisions as recommendations. Your system is responsible for acting on the decision (block, approve, or queue the transaction). ### Transaction types | Type | Example subtypes | Description | | -------- | ----------------------------- | --------------------------- | | `CARD` | debit, credit, prepaid | Card transactions | | `WIRE` | domestic, international, ach | Wire transfers | | `PIX` | instant, scheduled | Brazilian instant payments | | `CRYPTO` | bitcoin, ethereum, stablecoin | Cryptocurrency transactions | `transactionType` accepts only the four values above. `subType` is a free-form string (up to 50 characters, normalized to lowercase) — the subtypes listed are common examples, not a closed list. ## Step 5: Check limit usage *** Review a limit's cumulative consumption. For the consumption behind a single decision, read `limitUsageDetails` on the `POST /v1/validations` response. API reference: [Retrieve limit usage](/en/reference/tracer/retrieve-limit-usage) ```bash cURL theme={null} curl -X GET "https://tracer.sandbox.lerian.net/v1/limits/019c96a0-4a10-7dfe-b5c1-8a1b2c3d4e5f/usage" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" ``` ```json theme={null} { "limitId": "019c96a0-4a10-7dfe-b5c1-8a1b2c3d4e5f", "limitAmount": "50000.00", "currentUsage": "15000.00", "utilizationPercent": 30.0, "nearLimit": false, "resetAt": "2026-03-06T00:00:00Z" } ``` `currentUsage` here adds up the usage counters recorded for the limit, across periods and scopes. ## Step 6: Review audit events *** Every validation decision and configuration change is recorded in an immutable audit log. Query audit events for compliance reporting and debugging. API reference: [List audit events](/en/reference/tracer/list-audit-events) ```bash cURL theme={null} curl -X GET "https://tracer.sandbox.lerian.net/v1/audit-events?event_type=TRANSACTION_VALIDATED&start_date=2026-03-05T00:00:00Z&end_date=2026-03-06T00:00:00Z" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" ``` ### Audit event types | Event type | Description | | ----------------------- | --------------------------- | | `TRANSACTION_VALIDATED` | A transaction was validated | | `RULE_CREATED` | A new rule was created | | `RULE_ACTIVATED` | A rule was activated | | `RULE_DEACTIVATED` | A rule was deactivated | | `LIMIT_CREATED` | A new limit was created | | `LIMIT_ACTIVATED` | A limit was activated | | `LIMIT_DEACTIVATED` | A limit was deactivated | This table shows the most common event types. For the complete list — including update, delete, draft, and reservation lifecycle events — see [Audit and compliance](/en/tracer/audit-compliance). ## Step 7: Verify audit integrity *** Verify the cryptographic hash chain of audit events to confirm that no records have been tampered with. This is essential for SOX and GLBA compliance. API reference: [Verify audit event](/en/reference/tracer/verify-audit-event) ```bash cURL theme={null} curl -X GET "https://tracer.sandbox.lerian.net/v1/audit-events/019c96a0-4a10-7dfe-b5c1-8a1b2c3d4e5f/verify" \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" ``` ```json theme={null} { "isValid": true, "totalChecked": 1234, "message": "Hash chain integrity verified successfully" } ``` ## Next steps *** Business overview of the validation lifecycle and core concepts. Deep dive into CEL expressions and advanced rule configuration. Configure and manage limits by scope, period, and transaction type. Complete list of error codes and how to resolve them. # Tracer error list Source: https://docs.lerian.studio/en/reference/tracer/tracer-error-list Tracer APIs return a structured error object with a stable code, HTTP status, and message so you can diagnose issues and route them to the right team. The Tracer API returns errors as an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details object, served with the `application/problem+json` content type: ```json theme={null} { "type": "https://errors.lerian.studio/v1/", "title": "", "status": , "detail": "", "code": "" } ``` **Field definitions** * `type`: A URI that identifies the error type, built as `https://errors.lerian.studio/v1/` followed by the error code. * `title`: A brief summary of the issue. * `status`: The HTTP status code of the response. * `detail`: Detailed guidance for resolving the error. The tables below list this content in the `message` column. * `code`: A unique, stable identifier for the error. It is usually a four-digit numeric string drawn from the shared platform error registry (for example, `0347`); authentication failures are the exception and use the literal string `Unauthenticated` (see the note below). * `entityType`: The entity the error relates to (for example, `Rule`). Only present when applicable. * `message`: The human-readable reason, exposed verbatim as a top-level field. Only present on `413 Payload Too Large` and `504 Gateway Timeout` responses; all other errors omit it. For server-side errors (HTTP 5xx), `title` and `detail` are sanitized to generic values so internal causes never leak. Use `code` and `type` to identify the error. For `504` timeouts, the top-level `message` field still carries the specific reason. **Field-level validation** Struct-level field validation failures return code `0009` with the title `Validation Error` and a `detail` that names the specific field and constraint — for example, `transactionType must be one of [CARD WIRE PIX CRYPTO]`. Examples: ```json Missing required field theme={null} { "type": "https://errors.lerian.studio/v1/0009", "title": "Validation Error", "status": 400, "detail": "name is a required field", "code": "0009" } ``` ```json Invalid expression type theme={null} { "type": "https://errors.lerian.studio/v1/0341", "title": "Expression Type", "status": 400, "detail": "Expression must return boolean.", "code": "0341", "entityType": "Rule" } ``` Two response families keep a legacy flat `{"code", "title", "message"}` shape instead of the problem details object, because they are emitted by middleware that runs before the API layer. Authentication failures: a missing or invalid API key returns HTTP 401 with `"code": "Unauthenticated"`, `"title": "Unauthorized"`, and `"message": "API Key missing or invalid"` — match on the literal `Unauthenticated` string; a Bearer token that parses but lacks the required `sub` claim returns HTTP 401 with `"code": "0474"`. Tenant capacity: code `0466` returns HTTP 503 in the same flat shape. ## General errors *** These errors can be returned by any Tracer API endpoint. | `code` | `title` | `message` | | :----- | :------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0009 | Missing Fields in Request | Your request is missing one or more required fields: %v. Please refer to the documentation to ensure all necessary fields are included in your request. | | 0046 | Internal Server Error | The server encountered an unexpected error. Please try again later or contact support. | | 0065 | Invalid Path Parameter | One or more path parameters are in an incorrect format. Please check the following parameters %v and ensure they meet the required format before trying again. | | 0082 | Invalid Query Parameter | One or more query parameters are in an incorrect format. Please check the following parameters '%v' and ensure they meet the required format before trying again. | | 0094 | Bad Request | The request body is malformed or contains invalid JSON. Please verify the syntax and try again. | | 0143 | Payload Too Large | payload too large: exceeds 100KB limit | | 0183 | Nothing to Update | No updatable fields were provided. Please include at least one field to update. | | 0330 | Context Cancelled | Context cancelled / service unavailable. | | 0484 | Route Not Found | The requested route does not exist. Please verify the HTTP method and path and try again. | | 0485 | Method Not Allowed | The HTTP method is not allowed for the requested route. Please verify the method and try again. | | 0497 | Request Header Fields Too Large | The request header fields are too large. Please reduce the size of the request headers and try again. | Code `0009` also appears with the title `Validation Error` when field-level validation rejects a request — see the note above. Code `0143` is returned with HTTP `413` when a request body exceeds the 100KB limit on the validation and reservation endpoints; the reason also appears in the top-level `message` field. Code `0497` is returned with HTTP `431` when the request headers are too large. Code `0484` is returned with HTTP `404` for a path the service does not serve, and code `0485` with HTTP `405` for a method the path does not accept. ## Date and time errors *** | `code` | `title` | `message` | | :----- | :------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------- | | 0077 | Invalid Date Format Error | The 'initialDate', 'finalDate', or both are in the incorrect format. Please use the 'yyyy-mm-dd' format and try again. | | 0083 | Invalid Date Range Error | Both 'initialDate' and 'finalDate' fields are required and must be in the 'yyyy-mm-dd' format. Please provide valid dates and try again. | ## Pagination errors *** | `code` | `title` | `message` | | :----- | :------------------------ | :------------------------------------------------------------------------------------------------------------ | | 0080 | Pagination Limit Exceeded | The pagination limit exceeds the maximum allowed of %v items per page. Please verify the limit and try again. | | 0081 | Invalid Sort Order | The 'sort\_order' field must be 'asc' or 'desc'. Please provide a valid sort order and try again. | | 0331 | Pagination Limit Invalid | Pagination limit must be positive. | | 0332 | Invalid Sort Column | Sort column not in allowed list. | | 0333 | Invalid Cursor | Invalid or corrupted pagination cursor. | | 0334 | Cursor With Sort Params | Cursor and sort parameters are mutually exclusive. | ## Metadata errors *** These errors are raised by the `metadata` map on a validation request (`POST /v1/validations`) and on a reserve request (`POST /v1/reservations`). | `code` | `title` | `message` | | :----- | :--------------------------- | :-------------------------------------------------------------------------------------------- | | 0050 | Metadata Key Length Exceeded | A metadata key exceeds the maximum allowed length of 64 characters. Please use a shorter key. | | 0335 | Metadata Entries Exceeded | Metadata entries exceed maximum of 50. | | 0336 | Metadata Key Invalid Chars | Metadata key contains invalid characters. | A metadata key longer than 64 characters, or more than 50 metadata entries on one request, is rejected with HTTP `400`. ## CEL expression errors *** Rules are written as CEL (Common Expression Language) expressions. These errors are raised when a rule expression is created, updated, or evaluated. | `code` | `title` | `message` | | :----- | :------------------------- | :--------------------------------------------------------------------- | | 0340 | Expression Syntax | Invalid CEL syntax. | | 0341 | Expression Type | Expression must return boolean. | | 0342 | Expression Cost Exceeded | Cost limit exceeded (cost computed and above threshold). | | 0343 | Expression Evaluation | Runtime evaluation error. | | 0344 | Expression Program | Program creation failed (compilation phase). | | 0345 | Expression Cost Estimation | Failed to estimate expression cost. | | 0346 | Amount Exceeds Precision | Amount exceeds safe precision for CEL float64 evaluation (max: ±2^53). | | 0351 | Expression Not Modifiable | Expression cannot be modified for non-DRAFT rules. | ## Rule errors *** | `code` | `title` | `message` | | :----- | :------------------------------ | :-------------------------------------------- | | 0347 | Rule Not Found | Rule not found by ID. | | 0348 | Rule Name Already Exists | Rule name must be unique. | | 0349 | Rule Invalid Status | Invalid rule status transition. | | 0350 | Rule Evaluation Failed | Rule evaluation failed. | | 0352 | Rule Nil Input | Rule input cannot be nil. | | 0353 | Rule Name Required | Rule name is required. | | 0354 | Rule Name Too Long | Rule name exceeds max length (255). | | 0355 | Rule Expression Required | Rule expression is required. | | 0356 | Rule Expression Too Long | Rule expression exceeds max length (5000). | | 0357 | Rule Invalid Action | Action must be one of \[ALLOW, DENY, REVIEW]. | | 0358 | Rule Invalid Scope | Scope must have at least one field set. | | 0359 | Rule Description Too Long | Rule description exceeds max length (1000). | | 0360 | Rule Scopes Too Many | Rule scopes exceed maximum (100). | | 0437 | Rule Cache Not Ready | Rule cache is not ready. | | 0441 | Rule Name Already Exists In Ctx | Rule name already exists in this context. | ## Limit errors *** | `code` | `title` | `message` | | :----- | :-------------------------------- | :----------------------------------------------------------------- | | 0362 | Limit Not Found | Limit not found by ID. | | 0363 | Limit Invalid Status Change | Invalid limit status transition. | | 0364 | Limit Invalid Type | Invalid limit type. | | 0365 | Limit Invalid Max Amount | MaxAmount must be positive. | | 0366 | Limit Invalid Currency | Currency must be valid ISO 4217. | | 0367 | Limit Invalid Scope | Scope validation failed. | | 0368 | Limit Name Required | Limit name is required. | | 0369 | Limit Name Too Long | Limit name exceeds max length. | | 0370 | Limit Already Deleted | Limit is already in DELETED state. | | 0371 | Limit Name Invalid Chars | Limit name contains invalid characters. | | 0372 | Limit Description Invalid Chars | Limit description contains invalid characters. | | 0373 | Limit Invalid ID | Limit ID is invalid or nil. | | 0378 | Limit Check Failed | Limit check failed. | | 0379 | Limit Nil Input | Limit input cannot be nil. | | 0380 | Limit Immutable Field | Cannot modify immutable field (limitType, currency). | | 0438 | Limit Time Window Mismatch | ActiveTimeStart and activeTimeEnd must both be set or both be nil. | | 0442 | Limit Name Already Exists | Limit name already exists. | | 0447 | Limit Invalid Custom Start Format | Invalid customStartDate format, expected RFC3339. | | 0448 | Limit Invalid Custom End Format | Invalid customEndDate format, expected RFC3339. | | 0449 | Limit Custom Dates Required | CustomStartDate and customEndDate required for CUSTOM limitType. | ## Audit event errors *** | `code` | `title` | `message` | | :----- | :-------------------------- | :------------------------------------- | | 0381 | Audit Event Not Found | Audit event not found. | | 0382 | Invalid Audit Event Filters | Invalid audit event filter parameters. | ## Validation request errors *** These errors are returned by the transaction-validation endpoints (`POST /v1/validations` and the validation queries). | `code` | `title` | `message` | | :----- | :------------------------------------- | :--------------------------------------------------- | | 0413 | Validation Request IDRequired | RequestId is required. | | 0414 | Validation Invalid Transaction Type | Invalid transactionType. | | 0415 | Validation Amount Non Positive | Amount must be positive. | | 0416 | Validation Currency Required | Currency is required. | | 0417 | Validation Invalid Currency | Currency must be valid ISO 4217. | | 0418 | Validation Timestamp Required | Timestamp is required. | | 0419 | Validation Timestamp Future | Timestamp cannot be in the future. | | 0420 | Validation Account Required | Account is required. | | 0421 | Validation Timestamp Past | Timestamp is too far in the past. | | 0422 | Gateway Timeout | validation timeout | | 0423 | Validation Segment IDRequired | SegmentId is required when segment is provided. | | 0424 | Validation Portfolio IDRequired | PortfolioId is required when portfolio is provided. | | 0425 | Validation Sub Type Too Long | SubType exceeds maximum length of 50 characters. | | 0426 | Validation Invalid Account Type | Account.type must be checking, savings, or credit. | | 0427 | Validation Invalid Account Status | Account.status must be active, suspended, or closed. | | 0428 | Validation Invalid Merchant Category | Merchant.category must be 4-digit MCC code. | | 0429 | Validation Invalid Merchant Country | Merchant.country must be ISO 3166-1 alpha-2. | | 0430 | Validation Merchant IDRequired | Merchant.id is required when merchant is provided. | | 0431 | Invalid Transaction Validation Filters | Invalid transaction validation filter parameters. | | 0432 | Transaction Validation Not Found | Transaction validation record not found. | | 0433 | Gateway Timeout | query timeout exceeded | Codes `0422` and `0433` are returned with HTTP `504 Gateway Timeout` — `0422` when a validation evaluation exceeds its deadline, `0433` when a list-validations query does. As with all 5xx responses, `detail` is sanitized; the specific reason is carried in the top-level `message` field. ## Reservation errors *** These errors are returned by the usage-reservation endpoints (`/reservations`), the two-phase reserve / confirm / release surface. Reservation requests can also return the general and validation-request errors listed above. | `code` | `title` | `message` | | :----- | :---------------------------- | :-------------------------------------------------------------------------- | | 0476 | Reservation Transaction IDReq | Reservation: transactionId is required. | | 0480 | Reservation Invalid Status | Reservation: status must be one of RESERVED, CONFIRMED, RELEASED, EXPIRED. | | 0482 | Reservation Not Found | Reservation: reservation not found. | | 0483 | Reservation Already Terminal | Reservation: reservation is already in a terminal state. | | 0487 | Reservation Tenant Required | Reservation: tenant id is required on the multi-tenant reservation surface. | ## Multi-tenant and authentication errors *** The instance returns HTTP 503 with a `Retry-After` header when it reaches its per-pod tenant worker cap; clients should back off and retry. A Bearer token that parses but lacks the required `sub` claim is rejected with HTTP 401. | `code` | `title` | `message` | | :----- | :---------------------- | :------------------------------------------------------------------------------- | | 0466 | Tenant Capacity Reached | Tenant capacity reached; please retry shortly | | 0474 | Unauthorized | Bearer token is missing the required 'sub' claim; identity cannot be attributed. | ## Multi-tenant bootstrap errors *** These codes surface only at service startup, when `MULTI_TENANT_ENABLED=true` and a required configuration is missing or incompatible. They appear in startup logs and prevent the service from starting; they never reach `/v1/*` API consumers. | `code` | `title` | `message` | | :----- | :----------------------------- | :----------------------------------------------------------------------------------------- | | 0451 | MTConfig Required | Multi-tenant config: cfg is required. | | 0452 | MTLogger Required | Multi-tenant config: logger is required. | | 0453 | MTURLRequired | MULTI\_TENANT\_URL must be set when MULTI\_TENANT\_ENABLED=true. | | 0454 | MTURLInvalid | MULTI\_TENANT\_URL must be a valid absolute URL with scheme and host. | | 0455 | MTService APIKey Required | MULTI\_TENANT\_SERVICE\_API\_KEY must be set when MULTI\_TENANT\_ENABLED=true. | | 0456 | MTRedis Host Required | MULTI\_TENANT\_REDIS\_HOST must be set when MULTI\_TENANT\_ENABLED=true. | | 0457 | MTPlugin Auth Required | MULTI\_TENANT\_ENABLED=true requires PLUGIN\_AUTH\_ENABLED=true. | | 0458 | MTAPIKey Only Validation Confl | MULTI\_TENANT\_ENABLED=true is incompatible with API\_KEY\_ENABLED\_ONLY\_VALIDATION=true. | ## Readiness probe errors *** These codes are surfaced by the operational `/readyz` endpoint and by the worker-supervisor lifecycle. They appear in the `error` field of the `/readyz` JSON response — which carries the code only — not in `/v1/*` API responses. The `title` and `message` below describe each code for operator reference. The `/readyz` cycle probes five dependencies: `postgres` and `rule_cache` always; `redis` and `tenant_manager` only in multi-tenant mode (skipped otherwise); `streaming` is advisory — it appears in the checks and metrics but never forces a 503. | `code` | `title` | `message` | | :----- | :-------------------------------------- | :---------------------------------------------------------------------- | | 0436 | Rule Cache Warm Up Failed | Rule cache warm-up failed. | | 0459 | Readyz Pg Connection Not Established | Postgres readyz: connection not established. | | 0460 | Readyz Pg Connection Failed | Postgres readyz: connection failed. | | 0461 | Readyz Pg Ping Failed | Postgres readyz: ping failed. | | 0462 | Readyz Dependencies Unhealthy | /readyz aggregate: one or more dependencies unhealthy. | | 0463 | Readyz Cache Not Ready | Rule\_cache readyz: cache not ready. | | 0464 | Readyz Cache Stale | Rule\_cache readyz: cache data stale. | | 0465 | Supervisor Shutting Down | Worker supervisor: shutting down, refusing to spawn new tenant workers. | | 0493 | Readyz Redis Connection Not Established | Redis readyz: connection not established. | | 0494 | Readyz Redis Ping Failed | Redis readyz: ping failed. | | 0495 | Readyz Tenant Manager Unavailable | Tenant\_manager readyz: service unavailable. | | 0496 | Readyz Streaming Unhealthy | Streaming readyz: producer unhealthy. | # Update a Limit Source: https://docs.lerian.studio/en/reference/tracer/update-limit en/openapi/v3-current/tracer.yaml patch /v1/limits/{id} Use this endpoint to partially update a spending limit. Only provided fields are updated. limitType and currency are immutable and cannot be changed. # Update a Rule Source: https://docs.lerian.studio/en/reference/tracer/update-rule en/openapi/v3-current/tracer.yaml patch /v1/rules/{id} Use this endpoint to partially update a rule. Only provided fields are updated. Expression can only be modified when rule is in DRAFT status. # Validate a Transaction Source: https://docs.lerian.studio/en/reference/tracer/validate-transaction en/openapi/v3-current/tracer.yaml post /v1/validations Use this endpoint to validate a transaction against configured rules and limits in real-time. Returns a decision (ALLOW, DENY, or REVIEW) along with details about which rules matched and limit usage. Performance target is under 80ms (p99). # Verify Audit Event Hash Chain Source: https://docs.lerian.studio/en/reference/tracer/verify-audit-event en/openapi/v3-current/tracer.yaml get /v1/audit-events/{id}/verify Use this endpoint to verify the integrity of the audit event hash chain up to a specific event. Detects tampering attempts. Designed for SOX/GLBA compliance. # Assign connection to product Source: https://docs.lerian.studio/en/reference/fetcher/connections/assign-connection en/openapi/v3-current/connections.yaml post /v1/management/connections/{id}/assign Associate an unassigned connection to a product. This is a one-time, irreversible operation for migration purposes. The product name must be provided via the X-Product-Name header. # Delete a connection Source: https://docs.lerian.studio/en/reference/fetcher/connections/delete-connection en/openapi/v3-current/connections.yaml delete /v1/management/connections/{id} Deletes a connection when it has no active jobs. # List unassigned connections Source: https://docs.lerian.studio/en/reference/fetcher/connections/list-unassigned-connections en/openapi/v3-current/connections.yaml get /v1/management/connections/unassigned List connections that have no product assigned, useful for migration purposes. # Discover a connection schema Source: https://docs.lerian.studio/en/reference/fetcher/connections/retrieve-connection-schema en/openapi/v3-current/connections.yaml get /v1/management/connections/{id}/schema Returns tables and fields discovered from the configured datasource. # Test a connection Source: https://docs.lerian.studio/en/reference/fetcher/connections/test-connection en/openapi/v3-current/connections.yaml post /v1/management/connections/{id}/test Attempts to connect to the configured datasource and reports the observed latency. # Update a connection Source: https://docs.lerian.studio/en/reference/fetcher/connections/update-connection en/openapi/v3-current/connections.yaml patch /v1/management/connections/{id} Applies a partial update to an existing database connection. # Validate a connection schema Source: https://docs.lerian.studio/en/reference/fetcher/connections/validate-schema en/openapi/v3-current/connections.yaml post /v1/management/connections/validate-schema Validates mapped datasource, table, and field identifiers before a connection is used. # Create a data extraction job Source: https://docs.lerian.studio/en/reference/fetcher/create-fetcher-job en/openapi/v3-current/connections.yaml post /v1/fetcher Creates and queues a data extraction job, or returns the existing job for an idempotent duplicate request. # Get a data extraction job Source: https://docs.lerian.studio/en/reference/fetcher/get-fetcher-job en/openapi/v3-current/connections.yaml get /v1/fetcher/{id} Returns a data extraction job by identifier. # Add a comment to an exception Source: https://docs.lerian.studio/en/reference/matcher/add-exception-comment en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/comments Use this endpoint to add a comment to a specific exception. Comments help track team discussions, decisions, and investigation notes. # Adjust entry for an exception Source: https://docs.lerian.studio/en/reference/matcher/adjust-entry-exception en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/adjust-entry Use this endpoint to resolve an exception by creating an adjustment entry. This is used when a monetary correction is needed to reconcile the transaction. # Archive a reconciliation context Source: https://docs.lerian.studio/en/reference/matcher/archive-context en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/archive Archives (soft-deletes) a reconciliation context by ID. Archiving flips the context to ARCHIVED in place and never erases its reconciled financial history; an archived context with children is valid. Archived contexts are excluded from the default listing. Use restore to bring it back to DRAFT. # Archive a reconciliation source Source: https://docs.lerian.studio/en/reference/matcher/archive-source en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/sources/{sourceId}/archive Archives (soft-deletes) a reconciliation source by ID. Archiving never erases the source's imported transactions or field map; the source is simply excluded from listings, readiness counts, clone reads, and the matching working set. Use restore to bring it back. # Assign an exception Source: https://docs.lerian.studio/en/reference/matcher/assign-exception en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/assign Assigns a single exception to a user. Mirrors the bulk-assign auth and validation but acts on exactly one exception addressed by path. Returns the updated exception. # Bulk assign exceptions Source: https://docs.lerian.studio/en/reference/matcher/bulk-assign-exceptions en/openapi/v3-current/matcher.yaml post /v1/exceptions/bulk/assign Use this endpoint to assign multiple exceptions to a team member in a single operation. You can assign up to 100 exceptions at a time. # Bulk dispatch exceptions Source: https://docs.lerian.studio/en/reference/matcher/bulk-dispatch-exceptions en/openapi/v3-current/matcher.yaml post /v1/exceptions/bulk/dispatch Use this endpoint to dispatch multiple exceptions to an external system in a single operation. You can dispatch up to 100 exceptions at a time. # Bulk resolve exceptions Source: https://docs.lerian.studio/en/reference/matcher/bulk-resolve-exceptions en/openapi/v3-current/matcher.yaml post /v1/exceptions/bulk/resolve Use this endpoint to resolve multiple exceptions in a single operation. You can resolve up to 100 exceptions at a time with a shared resolution and reason. # Cancel an export job Source: https://docs.lerian.studio/en/reference/matcher/cancel-export-job en/openapi/v3-current/matcher.yaml post /v1/export-jobs/{jobId}/cancel Use this endpoint to cancel a queued or running export job. # Clone a reconciliation context Source: https://docs.lerian.studio/en/reference/matcher/clone-context en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/clone Use this endpoint to clone an existing reconciliation context with its associated sources, rules, and fee schedules. You can selectively include or exclude specific components. # Close a dispute Source: https://docs.lerian.studio/en/reference/matcher/close-dispute en/openapi/v3-current/matcher.yaml post /v1/disputes/{disputeId}/close Use this endpoint to close a dispute with a resolution. The dispute can be marked as won or lost based on the outcome. # Create a reconciliation context Source: https://docs.lerian.studio/en/reference/matcher/create-context en/openapi/v3-current/matcher.yaml post /v1/contexts Use this endpoint to create a new reconciliation context. Contexts are used to scope matching rules and organize your reconciliation configuration. # Create an export job Source: https://docs.lerian.studio/en/reference/matcher/create-export-job en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/export-jobs Use this endpoint to create an asynchronous export job for large report exports. Supported formats include CSV, JSON, and XML. # Create a fee rule Source: https://docs.lerian.studio/en/reference/matcher/create-fee-rule en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/fee-rules Use this endpoint to create a new fee rule that maps transaction metadata to a fee schedule within a context. Priority must be unique within a context across all sides (LEFT, RIGHT, and ANY rules share the same priority space). The caller must also be allowed to read the referenced fee schedule. # Create a fee schedule Source: https://docs.lerian.studio/en/reference/matcher/create-fee-schedule en/openapi/v3-current/matcher.yaml post /v1/fee-schedules Use this endpoint to create a new fee schedule. Fee schedules define the fee structure applied to transactions during reconciliation. # Create a field map Source: https://docs.lerian.studio/en/reference/matcher/create-field-map en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/sources/{sourceId}/field-maps Use this endpoint to create a field map for a specific source within a context. Field maps define how source fields are mapped to reconciliation fields. # Create a match rule Source: https://docs.lerian.studio/en/reference/matcher/create-match-rule en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/rules Use this endpoint to create a new match rule within a specific context. Match rules define how transactions are matched during reconciliation. # Create a schedule Source: https://docs.lerian.studio/en/reference/matcher/create-schedule en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/schedules Use this endpoint to create a cron-based reconciliation schedule for automatic matching within a context. # Create a reconciliation source Source: https://docs.lerian.studio/en/reference/matcher/create-source en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/sources Use this endpoint to create a new reconciliation source under a specific context. Sources define where transaction data comes from for reconciliation. # Create a source binding Source: https://docs.lerian.studio/en/reference/matcher/create-source-binding en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/sources/{sourceId}/bindings Creates a source binding tying a reconciliation source to a pull rail (file or query) plus an interval schedule. Exactly one rail payload is meaningful per kind: the file rail populates transportConfig, the query rail populates connectionId. # Delete an actor mapping Source: https://docs.lerian.studio/en/reference/matcher/delete-actor-mapping en/openapi/v3-current/matcher.yaml delete /v1/governance/actor-mappings/{actorId} Use this endpoint to delete an actor mapping by its identifier. # Delete an exception comment Source: https://docs.lerian.studio/en/reference/matcher/delete-exception-comment en/openapi/v3-current/matcher.yaml delete /v1/exceptions/{exceptionId}/comments/{commentId} Use this endpoint to delete a specific comment from an exception. # Delete a fee rule Source: https://docs.lerian.studio/en/reference/matcher/delete-fee-rule en/openapi/v3-current/matcher.yaml delete /v1/fee-rules/{feeRuleId} Use this endpoint to permanently remove a fee rule by its identifier. # Delete a fee schedule Source: https://docs.lerian.studio/en/reference/matcher/delete-fee-schedule en/openapi/v3-current/matcher.yaml delete /v1/fee-schedules/{scheduleId} Use this endpoint to delete a specific fee schedule by its identifier. # Delete a field map Source: https://docs.lerian.studio/en/reference/matcher/delete-field-map en/openapi/v3-current/matcher.yaml delete /v1/field-maps/{fieldMapId} Use this endpoint to delete a specific field map by its identifier. # Delete a match rule Source: https://docs.lerian.studio/en/reference/matcher/delete-match-rule en/openapi/v3-current/matcher.yaml delete /v1/contexts/{contextId}/rules/{ruleId} Use this endpoint to delete a specific match rule by its identifier. # Delete a schedule Source: https://docs.lerian.studio/en/reference/matcher/delete-schedule en/openapi/v3-current/matcher.yaml delete /v1/contexts/{contextId}/schedules/{scheduleId} Use this endpoint to delete a reconciliation schedule from a context. # Delete a source binding Source: https://docs.lerian.studio/en/reference/matcher/delete-source-binding en/openapi/v3-current/matcher.yaml delete /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId} Removes a source binding by ID. # Dispatch exception to external system Source: https://docs.lerian.studio/en/reference/matcher/dispatch-exception en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/dispatch Use this endpoint to dispatch an exception to an external ticketing system such as JIRA or ServiceNow for tracking and resolution. # Download an audit log archive Source: https://docs.lerian.studio/en/reference/matcher/download-archive en/openapi/v3-current/matcher.yaml get /v1/governance/archives/{id}/download Use this endpoint to download an audit log archive. Returns a pre-signed URL with a time-limited download link and a SHA-256 checksum for integrity verification. # Download export file Source: https://docs.lerian.studio/en/reference/matcher/download-export-job en/openapi/v3-current/matcher.yaml get /v1/export-jobs/{jobId}/download Use this endpoint to download the export file. The response includes a presigned URL for downloading the file. # Force match an exception Source: https://docs.lerian.studio/en/reference/matcher/force-match-exception en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/force-match Use this endpoint to resolve an exception by forcing a match with an override reason. This is used when manual intervention determines the transaction should be considered matched despite discrepancies. # Get an actor mapping Source: https://docs.lerian.studio/en/reference/matcher/get-actor-mapping en/openapi/v3-current/matcher.yaml get /v1/governance/actor-mappings/{actorId} Use this endpoint to retrieve the display name and email mapped to a specific actor identifier. # Get context setup progress Source: https://docs.lerian.studio/en/reference/matcher/get-setup-progress en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/setup-progress Returns configured-resource counts, last-run state, and activation readiness for a context in a single aggregate, so clients derive wizard/checklist state from one request. # Get a source binding Source: https://docs.lerian.studio/en/reference/matcher/get-source-binding en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId} Returns a source binding by ID. # List actor mappings Source: https://docs.lerian.studio/en/reference/matcher/list-actor-mappings en/openapi/v3-current/matcher.yaml get /v1/governance/actor-mappings Lists actor mappings by actor ID with optional actor ID prefix filtering. Does not return display name or email. # List audit log archives Source: https://docs.lerian.studio/en/reference/matcher/list-archives en/openapi/v3-current/matcher.yaml get /v1/governance/archives Use this endpoint to list all audit log archives. Archives contain historical audit log data that has been compressed and moved to long-term storage. Supports offset-based pagination. # List audit logs Source: https://docs.lerian.studio/en/reference/matcher/list-audit-logs en/openapi/v3-current/matcher.yaml get /v1/governance/audit-logs Use this endpoint to retrieve all audit log entries. You can filter the results by actor, date range, action type, and entity type. The response uses cursor-based pagination. # List audit logs by entity Source: https://docs.lerian.studio/en/reference/matcher/list-audit-logs-by-entity en/openapi/v3-current/matcher.yaml get /v1/governance/entities/{entityType}/{entityId}/audit-logs Use this endpoint to retrieve all audit log entries for a specific entity. The results are ordered by creation time descending. This allows you to trace the complete change history of any entity in the system. # List callback credentials Source: https://docs.lerian.studio/en/reference/matcher/list-callback-credentials en/openapi/v3-current/matcher.yaml get /v1/exceptions/callbacks/credentials Lists the caller tenant's callback credentials (live and revoked), newest first. Returns metadata only — never the token or its hash. # List field maps for a context Source: https://docs.lerian.studio/en/reference/matcher/list-context-field-maps en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/field-maps Returns a cursor-paginated list of every field map under a context, replacing per-source probing. # List reconciliation contexts Source: https://docs.lerian.studio/en/reference/matcher/list-contexts en/openapi/v3-current/matcher.yaml get /v1/contexts Use this endpoint to retrieve all reconciliation contexts. Supports filtering by type and status and uses cursor-based pagination. # List disputes Source: https://docs.lerian.studio/en/reference/matcher/list-disputes en/openapi/v3-current/matcher.yaml get /v1/disputes Use this endpoint to retrieve all disputes. Supports filtering by state, category, and date range with cursor-based pagination. # List exception comments Source: https://docs.lerian.studio/en/reference/matcher/list-exception-comments en/openapi/v3-current/matcher.yaml get /v1/exceptions/{exceptionId}/comments Use this endpoint to retrieve all comments for a specific exception. Comments provide an audit trail of team discussions and decisions. # List exceptions Source: https://docs.lerian.studio/en/reference/matcher/list-exceptions en/openapi/v3-current/matcher.yaml get /v1/exceptions Use this endpoint to retrieve all exceptions. You can filter the results by status, severity, assigned user, external system, and date range. The response uses cursor-based pagination. # List export jobs Source: https://docs.lerian.studio/en/reference/matcher/list-export-jobs en/openapi/v3-current/matcher.yaml get /v1/export-jobs Use this endpoint to retrieve all export jobs for the authenticated tenant. # List export jobs by context Source: https://docs.lerian.studio/en/reference/matcher/list-export-jobs-by-context en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/export-jobs Use this endpoint to list all export jobs for a specific reconciliation context. Supports cursor-based pagination. # List fee rules Source: https://docs.lerian.studio/en/reference/matcher/list-fee-rules en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/fee-rules Use this endpoint to retrieve all fee rules for a reconciliation context, ordered by priority. # List fee schedules Source: https://docs.lerian.studio/en/reference/matcher/list-fee-schedules en/openapi/v3-current/matcher.yaml get /v1/fee-schedules Use this endpoint to retrieve all fee schedules for the authenticated tenant. # List ingestion jobs for a context Source: https://docs.lerian.studio/en/reference/matcher/list-ingestion-jobs en/openapi/v3-current/matcher.yaml get /v1/imports/contexts/{contextId}/jobs Use this endpoint to retrieve all ingestion jobs for a specific reconciliation context. You can sort the results by various fields. The response uses cursor-based pagination. # List match rules Source: https://docs.lerian.studio/en/reference/matcher/list-match-rules en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/rules Use this endpoint to retrieve all match rules under a specific context. You can filter the results by rule type. The response uses cursor-based pagination. # List schedules for a context Source: https://docs.lerian.studio/en/reference/matcher/list-schedules en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/schedules Use this endpoint to retrieve all reconciliation schedules for a specific context. # List source bindings Source: https://docs.lerian.studio/en/reference/matcher/list-source-bindings en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/sources/{sourceId}/bindings Returns every source binding under a source's context, enabled and disabled, so a disabled binding stays visible instead of silently vanishing. # List reconciliation sources Source: https://docs.lerian.studio/en/reference/matcher/list-sources en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/sources Use this endpoint to retrieve all reconciliation sources under a specific context. You can filter the results by source type. The response uses cursor-based pagination. # Matcher API quick start Source: https://docs.lerian.studio/en/reference/matcher/matcher-developer-quick-start Get Matcher running in minutes: create your first reconciliation context, upload files, and review matched transactions with cURL and the Matcher API. **This guide is intended for developers.** If you're looking for a business-level overview of what Matcher does and how it helps your team, see [What is Matcher?](/en/matcher/what-is-matcher). Get Matcher running in minutes. This guide walks you through the complete journey, from creating your first reconciliation context to reviewing matched transactions. ## Before you begin *** You need: * A running Matcher instance * A valid JWT token for authentication * Two transaction files to reconcile (CSV, JSON, or XML) All examples use `cURL`. Replace `$TOKEN` with your JWT token and `https://api.matcher.example.com` with your Matcher URL. ## Step 1: Create a reconciliation context *** A context defines the scope of your reconciliation: what you are comparing and how. API reference: [Create context](/en/reference/matcher/create-context) ```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", "type": "1:1", "interval": "daily" }' ``` The `type` field defines how transactions are paired: | Type | Description | | ----- | ------------------------------------------------- | | `1:1` | Each transaction matches exactly one counterpart | | `1:N` | One transaction can match multiple counterparts | | `N:M` | Multiple transactions can match across both sides | Save the `id` from the response. You will use it in every subsequent step. ```json theme={null} { "id": "019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "name": "Daily Bank Reconciliation", "type": "1:1", "interval": "daily", "status": "DRAFT", "createdAt": "2026-03-04T12:00:00Z", "updatedAt": "2026-03-04T12:00:00Z" } ``` The context starts in DRAFT status. It moves to ACTIVE when you are ready to run reconciliation. ## Step 2: Add data sources *** Every context needs at least two sources: the systems whose transactions you want to compare. API reference: [Create source](/en/reference/matcher/create-source) ### Create a bank source ```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" }' ``` ### Create a ledger source ```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": "General Ledger - GL 1000", "type": "LEDGER" }' ``` Save both source `id` values. ### Source types | Type | Use case | | --------- | ---------------------------- | | `BANK` | Bank statements | | `LEDGER` | General ledger / ERP exports | | `GATEWAY` | Payment processor data | | `CUSTOM` | Any other data source | ## Step 3: Map source fields *** Your source files probably use different column names than Matcher expects. Field maps translate them into Matcher's standard schema. API reference: [Create field map](/en/reference/matcher/create-field-map) ### Map the bank source ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{bankSourceId}/field-maps" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mapping": { "Transaction ID": "transaction_id", "Amount": "amount", "Currency": "currency", "Post Date": "date", "Description": "reference" } }' ``` ### Map the ledger source ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{ledgerSourceId}/field-maps" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mapping": { "entry_id": "transaction_id", "debit_credit_amount": "amount", "currency_code": "currency", "posting_date": "date", "memo": "reference" } }' ``` ### Required fields Every transaction must have these fields after mapping: | Field | Type | Description | | ---------------- | ------- | ----------------------------------- | | `transaction_id` | String | Unique identifier within the source | | `amount` | Decimal | Transaction amount | | `currency` | String | ISO 4217 currency code (e.g. USD) | | `date` | Date | Transaction date (YYYY-MM-DD) | **Optional but recommended:** `reference` (external reference or description). ## Step 4: Create match rules *** Rules define how Matcher compares transactions. Start with an exact rule, which is the most precise. API reference: [Create match rule](/en/reference/matcher/create-match-rule) ### Create an 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, "caseInsensitive": true, "datePrecision": "DAY" } }' ``` ### Add a tolerance rule as fallback Catch small differences like bank fees or rounding: ```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": 20, "config": { "percentTolerance": 0.01, "absTolerance": 5.0, "dateWindowDays": 2, "matchCurrency": true, "matchReference": true, "caseInsensitive": true } }' ``` Matcher evaluates rules by priority (lowest number first). The exact rule runs first. Only unmatched transactions fall through to the tolerance rule. ### Rule types | Type | When to use | Priority range | | ----------- | --------------------------- | -------------- | | `EXACT` | Values should match exactly | 1-10 | | `TOLERANCE` | Small, expected differences | 11-50 | | `DATE_LAG` | Date delays between systems | 51-100 | ## Step 5: Activate the context *** Move the context from DRAFT to ACTIVE: API reference: [Update context](/en/reference/matcher/update-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 '{ "status": "ACTIVE" }' ``` ## Step 6: Upload transaction files *** Upload one file per source. Matcher accepts CSV, JSON, and XML formats via multipart form upload. * API reference: [Upload transaction file](/en/reference/matcher/upload-transaction-file) * [List ingestion jobs](/en/reference/matcher/list-ingestion-jobs) ### Upload bank transactions ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{bankSourceId}/upload" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@bank_transactions.csv" \ -F "format=csv" ``` ### Upload ledger transactions ```bash cURL theme={null} curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{ledgerSourceId}/upload" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@ledger_entries.csv" \ -F "format=csv" ``` Each upload creates an ingestion job. Check the job status: ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/jobs" \ -H "Authorization: Bearer $TOKEN" ``` Wait for both jobs to reach `COMPLETED` status before running the match. ## Step 7: Run matching *** Start with a dry run to preview results without persisting: API reference: [Run match](/en/reference/matcher/run-match) ```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" }' ``` Both responses include a `runId`. Save it for Step 8. Review the dry run results. When satisfied, run with COMMIT to persist 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": "COMMIT" }' ``` ## Step 8: Review results *** * API reference: [List match run groups](/en/reference/matcher/list-match-run-groups) * [Unmatch group](/en/reference/matcher/unmatch-group) ### View match groups ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/matching/runs/{runId}/groups?contextId={contextId}" \ -H "Authorization: Bearer $TOKEN" ``` Each match group contains paired transactions and a confidence score (0-100): | Score | What happens | | -------- | -------------------------------------- | | 90-100 | Auto-confirmed, no action needed | | 60-89 | Needs manual review | | Below 60 | No match created, becomes an exception | ### Undo an incorrect match Use the unmatch endpoint to reject a `PROPOSED` match group and return its transactions to the unmatched pool. For a `CONFIRMED` group, Matcher first verifies that it can reverse the residual/open-item effects of that confirmation. A successful unmatch reverses those effects atomically with revoking the group and returning its transactions: ```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": "Transactions belong to different records" }' ``` If the confirmed group's reversal removes the final live contribution behind an obligation, the open item becomes terminal `WITHDRAWN`: it remains as history but is not nettable or carried into another run. If a later live entry still stands on the residual, or a newer live obligation would conflict with restoring a terminal item on the same identity, Matcher returns `409 Conflict` before changing the group, transactions, or open items. After a successful unmatch, the transactions return to the unmatched pool for the next run. ## Step 9: Handle exceptions *** Exceptions are transactions that could not be matched automatically. Matcher classifies each exception by severity: API reference: [List exceptions](/en/reference/matcher/list-exceptions) | Severity | Criteria | SLA | | ---------- | -------------------------------- | -------- | | `CRITICAL` | Amount >= 100,000 or age >= 120h | 24 hours | | `HIGH` | Amount >= 10,000 or age >= 72h | 72 hours | | `MEDIUM` | Amount >= 1,000 or age >= 24h | 5 days | | `LOW` | All others | 7 days | ### List exceptions ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/exceptions" \ -H "Authorization: Bearer $TOKEN" ``` Resolve exceptions by force matching, creating adjustments, or dispatching to external systems like JIRA. ## Next steps *** Full guide to context and source configuration. All rule types and config options in detail. How scores are calculated and what they mean. Handle unmatched transactions. # Mint a callback credential Source: https://docs.lerian.studio/en/reference/matcher/mint-callback-credential en/openapi/v3-current/matcher.yaml post /v1/exceptions/callbacks/credentials Mints an opaque bearer token bound to the caller's tenant for authenticating inbound external-system exception callbacks, and returns the raw token ONCE. Only the token's SHA-256 hash is stored server-side. # Open a dispute Source: https://docs.lerian.studio/en/reference/matcher/open-dispute en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/disputes Use this endpoint to open a new dispute for an exception. Disputes are used to formally challenge or investigate discrepancies with external parties. # Process exception callback Source: https://docs.lerian.studio/en/reference/matcher/process-exception-callback en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/callback Use this endpoint to process a callback from an external system for a dispatched exception. This updates the exception status based on the external system resolution. # Pseudonymize an actor Source: https://docs.lerian.studio/en/reference/matcher/pseudonymize-actor en/openapi/v3-current/matcher.yaml post /v1/governance/actor-mappings/{actorId}/pseudonymize Use this endpoint to pseudonymize an actor by replacing their display name and email with "[REDACTED]". This supports GDPR right-to-erasure compliance. This action is irreversible. # Reorder match rules Source: https://docs.lerian.studio/en/reference/matcher/reorder-match-rules en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/rules/reorder Use this endpoint to reorder the priority of match rules within a specific context. The order determines which rules are evaluated first during reconciliation. # Resolve an exception Source: https://docs.lerian.studio/en/reference/matcher/resolve-exception en/openapi/v3-current/matcher.yaml post /v1/exceptions/{exceptionId}/resolve Resolves a single exception with a resolution and optional reason. Mirrors the bulk-resolve auth and validation but acts on exactly one exception addressed by path. Returns the updated exception. # Restore an archived reconciliation context Source: https://docs.lerian.studio/en/reference/matcher/restore-context en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/restore Restores an archived reconciliation context back to DRAFT so it can be re-validated and re-activated. Restoring never auto-resumes matching. Restoring a context that is not archived is rejected with a 409 invalid-state. # Restore an archived reconciliation source Source: https://docs.lerian.studio/en/reference/matcher/restore-source en/openapi/v3-current/matcher.yaml post /v1/contexts/{contextId}/sources/{sourceId}/restore Restores an archived reconciliation source back into the live working set. # Get audit log by ID Source: https://docs.lerian.studio/en/reference/matcher/retrieve-audit-log en/openapi/v3-current/matcher.yaml get /v1/governance/audit-logs/{id} Use this endpoint to retrieve the details of a specific audit log entry by its identifier. Audit logs contain immutable records of all system operations for compliance and debugging purposes. # Get a reconciliation context Source: https://docs.lerian.studio/en/reference/matcher/retrieve-context en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId} Use this endpoint to retrieve the details of a specific reconciliation context by its identifier. # Get a dispute Source: https://docs.lerian.studio/en/reference/matcher/retrieve-dispute en/openapi/v3-current/matcher.yaml get /v1/disputes/{disputeId} Use this endpoint to retrieve the details of a specific dispute by its identifier, including its current state, category, and any submitted evidence. # Get exception Source: https://docs.lerian.studio/en/reference/matcher/retrieve-exception en/openapi/v3-current/matcher.yaml get /v1/exceptions/{exceptionId} Use this endpoint to retrieve the details of a specific exception by its identifier. # Get exception history Source: https://docs.lerian.studio/en/reference/matcher/retrieve-exception-history en/openapi/v3-current/matcher.yaml get /v1/exceptions/{exceptionId}/history Use this endpoint to retrieve the audit history for an exception. The history shows all actions taken on the exception. # Get export job status Source: https://docs.lerian.studio/en/reference/matcher/retrieve-export-job en/openapi/v3-current/matcher.yaml get /v1/export-jobs/{jobId} Use this endpoint to retrieve the status of a specific export job. # Get a fee rule Source: https://docs.lerian.studio/en/reference/matcher/retrieve-fee-rule en/openapi/v3-current/matcher.yaml get /v1/fee-rules/{feeRuleId} Use this endpoint to retrieve the details of a specific fee rule by its identifier. # Get a fee schedule Source: https://docs.lerian.studio/en/reference/matcher/retrieve-fee-schedule en/openapi/v3-current/matcher.yaml get /v1/fee-schedules/{scheduleId} Use this endpoint to retrieve the details of a specific fee schedule by its identifier. # Get a field map by source Source: https://docs.lerian.studio/en/reference/matcher/retrieve-field-map-by-source en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/sources/{sourceId}/field-maps Use this endpoint to retrieve the field map for a specific source within a context. Field maps define how source fields are mapped to reconciliation fields. # Get a match rule Source: https://docs.lerian.studio/en/reference/matcher/retrieve-match-rule en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/rules/{ruleId} Use this endpoint to retrieve the details of a specific match rule by its identifier. # Get a schedule Source: https://docs.lerian.studio/en/reference/matcher/retrieve-schedule en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/schedules/{scheduleId} Use this endpoint to retrieve a specific reconciliation schedule by its identifier. # Get a reconciliation source Source: https://docs.lerian.studio/en/reference/matcher/retrieve-source en/openapi/v3-current/matcher.yaml get /v1/contexts/{contextId}/sources/{sourceId} Use this endpoint to retrieve the details of a specific reconciliation source by its identifier. # Revoke a callback credential Source: https://docs.lerian.studio/en/reference/matcher/revoke-callback-credential en/openapi/v3-current/matcher.yaml delete /v1/exceptions/callbacks/credentials/{credentialId} Revokes a callback credential so it can no longer authenticate inbound callbacks. Revocation is terminal and append-only audited. # Rotate a callback credential Source: https://docs.lerian.studio/en/reference/matcher/rotate-callback-credential en/openapi/v3-current/matcher.yaml post /v1/exceptions/callbacks/credentials/{credentialId}/rotate Atomically supersedes a live callback credential with a freshly minted one (same label) and returns the new raw token ONCE. The old credential is revoked in the same transaction. # Select exception ids by filter Source: https://docs.lerian.studio/en/reference/matcher/select-exception-ids en/openapi/v3-current/matcher.yaml get /v1/exceptions/ids Resolves the ids of every exception matching the SAME filter as GET /v1/exceptions (status, severity, contextId, reason, assigned_to, external_system, search, date_from, date_to, min_age_days), up to a cap of 10000, for the bulk "select all matching" path. The response carries the filtered total and a truncated flag; the client chunks the returned ids into the capped bulk-action calls. No pagination/sort: it returns the whole filtered set (capped). # Simulate fee calculation Source: https://docs.lerian.studio/en/reference/matcher/simulate-fee-schedule en/openapi/v3-current/matcher.yaml post /v1/fee-schedules/{scheduleId}/simulate Use this endpoint to simulate fee calculation for a given gross amount using a specific fee schedule. Returns the net amount, total fee, and per-item breakdown. # Submit evidence to a dispute Source: https://docs.lerian.studio/en/reference/matcher/submit-evidence en/openapi/v3-current/matcher.yaml post /v1/disputes/{disputeId}/evidence Use this endpoint to add evidence to a dispute. Evidence can include comments and optional file attachments to support the dispute case. # Update a reconciliation context Source: https://docs.lerian.studio/en/reference/matcher/update-context en/openapi/v3-current/matcher.yaml patch /v1/contexts/{contextId} Use this endpoint to update the fields of a specific reconciliation context by its identifier. # Update a fee rule Source: https://docs.lerian.studio/en/reference/matcher/update-fee-rule en/openapi/v3-current/matcher.yaml patch /v1/fee-rules/{feeRuleId} Use this endpoint to update the fields of a specific fee rule by its identifier. If the referenced fee schedule changes, the caller must also be allowed to read that fee schedule. # Update a fee schedule Source: https://docs.lerian.studio/en/reference/matcher/update-fee-schedule en/openapi/v3-current/matcher.yaml patch /v1/fee-schedules/{scheduleId} Use this endpoint to update the fields of a specific fee schedule by its identifier. # Update a field map Source: https://docs.lerian.studio/en/reference/matcher/update-field-map en/openapi/v3-current/matcher.yaml patch /v1/field-maps/{fieldMapId} Use this endpoint to update the fields of a specific field map by its identifier. # Update a match rule Source: https://docs.lerian.studio/en/reference/matcher/update-match-rule en/openapi/v3-current/matcher.yaml patch /v1/contexts/{contextId}/rules/{ruleId} Use this endpoint to update the fields of a specific match rule by its identifier. # Update a schedule Source: https://docs.lerian.studio/en/reference/matcher/update-schedule en/openapi/v3-current/matcher.yaml patch /v1/contexts/{contextId}/schedules/{scheduleId} Use this endpoint to update an existing reconciliation schedule. You can modify the cron expression and enabled status. # Update a reconciliation source Source: https://docs.lerian.studio/en/reference/matcher/update-source en/openapi/v3-current/matcher.yaml patch /v1/contexts/{contextId}/sources/{sourceId} Use this endpoint to update the fields of a specific reconciliation source by its identifier. # Update a source binding Source: https://docs.lerian.studio/en/reference/matcher/update-source-binding en/openapi/v3-current/matcher.yaml patch /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId} Updates fields on a source binding by ID. The rail (kind) is immutable. # Upload an evidence file to a dispute Source: https://docs.lerian.studio/en/reference/matcher/upload-evidence en/openapi/v3-current/matcher.yaml post /v1/disputes/{disputeId}/evidence/upload Streams an evidence file (pdf, png, jpeg, or csv) to tenant-scoped object storage and records the resulting object key on the dispute as a new evidence item. The comment arrives as a query parameter and the file bytes as the request body. The tenant is resolved from the JWT and the dispute from the path — never from the body. Fails closed (503) when object storage is not configured; rejects oversize bodies (413) and unsupported content types (415). # Upsert an actor mapping Source: https://docs.lerian.studio/en/reference/matcher/upsert-actor-mapping en/openapi/v3-current/matcher.yaml put /v1/governance/actor-mappings/{actorId} Use this endpoint to create or update an actor mapping. Actor mappings associate system actor identifiers with human-readable display names and email addresses for audit log readability. # Verify audit log chain integrity Source: https://docs.lerian.studio/en/reference/matcher/verify-audit-log-chain en/openapi/v3-current/matcher.yaml get /v1/governance/audit-logs/verify Re-verifies the calling tenant's tamper-evident audit hash chain and returns a structured verdict (intact, verified count, and the first broken tenant_seq when a break is found). The check is strictly read-only: it recomputes nothing into storage and never mutates an audit record. Use the optional maxRecords query parameter to bound how many records are inspected. # Activate a Workflow Source: https://docs.lerian.studio/en/reference/flowker/activate-workflow en/openapi/v3-current/flowker.yaml post /v1/workflows/{id}/activate Use this endpoint to activate a workflow. Transitions the status from `draft` to `active`. # Clone a Workflow Source: https://docs.lerian.studio/en/reference/flowker/clone-workflow en/openapi/v3-current/flowker.yaml post /v1/workflows/{id}/clone Use this endpoint to create a copy of an existing workflow with a new name. The cloned workflow is created in `draft` status. # Create a Workflow Source: https://docs.lerian.studio/en/reference/flowker/create-workflow en/openapi/v3-current/flowker.yaml post /v1/workflows Use this endpoint to create a new workflow definition. New workflows are created in `draft` status. # Deactivate a Workflow Source: https://docs.lerian.studio/en/reference/flowker/deactivate-workflow en/openapi/v3-current/flowker.yaml post /v1/workflows/{id}/deactivate Use this endpoint to deactivate a workflow. Transitions the status from `active` to `inactive`. # Delete a Workflow Source: https://docs.lerian.studio/en/reference/flowker/delete-workflow en/openapi/v3-current/flowker.yaml delete /v1/workflows/{id} Use this endpoint to delete a workflow definition. Only workflows with `draft` or `inactive` status can be deleted. # Getting started with Flowker Source: https://docs.lerian.studio/en/reference/flowker/flowker-api-quick-start Run Flowker locally, create your first workflow, execute it, and retrieve results — a hands-on quick start for developers using the orchestration engine. **This guide is intended for developers.** If you're looking for a business-level overview of what Flowker does, see [What is Flowker?](/en/flowker/what-is-flowker). Flowker is a workflow orchestration engine designed to help you model, execute, and scale business processes with precision. In this guide, you will run Flowker locally and execute your first workflow—from creation to result retrieval. By the end, you will have a working environment to validate automation flows and integrate them into your systems. ## Prerequisites *** Before you begin, make sure your environment is ready: | Tool | Minimum version | Check command | | -------------- | --------------- | ------------------------ | | Go | 1.26.5+ | `go version` | | Docker | 24+ | `docker --version` | | Docker Compose | 2.20+ | `docker compose version` | | Make | Installed | `make --version` | Flowker runs locally using Docker for its database (MongoDB). No external infrastructure is needed for this guide. ## Step 1: Obtain Flowker and set up the project *** Flowker is available to licensed customers; its repository is maintained internally. The steps below assume you already have access to the required Flowker project files. From the Flowker project directory, prepare the development environment: ```bash theme={null} cd flowker ``` Install development tools and create the environment file: ```bash theme={null} make dev-setup ``` Then start the local stack (MongoDB + Flowker on port 4021): ```bash theme={null} make dev ``` Once the output shows the server is running, Flowker is available at `http://localhost:4021`. The `make dev` command starts MongoDB, generates API documentation, and runs the Flowker application with authentication disabled—so you can test freely during development. ## Step 2: Create your first workflow *** Workflows define how your business process behaves—what steps run, in which order, and under which conditions. Each workflow is composed of **nodes** (the steps) and **edges** (the connections between them). Create a workflow with a webhook trigger and a log action: ```bash theme={null} curl -s -X POST http://localhost:4021/v1/workflows \ -H "Content-Type: application/json" \ -d '{ "name": "my-first-workflow", "description": "A simple workflow with one trigger and one action.", "nodes": [ { "id": "trigger-1", "type": "trigger", "name": "Start", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "webhook", "path": "my-first-workflow", "method": "POST", "input_contract": "open", "format": "json" } }, { "id": "log-event", "type": "action", "name": "Log event", "position": { "x": 200, "y": 0 }, "data": { "action": "log" } } ], "edges": [ { "id": "e1", "source": "trigger-1", "target": "log-event" } ] }' | jq . ``` The response confirms the workflow was created in `draft` status: ```json theme={null} { "id": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", "name": "my-first-workflow", "status": "draft" } ``` Save the `id` value — you will need it in the next steps. New workflows are always created in `draft` status. A workflow must have at least one node. In Flowker, **nodes** represent the individual steps of your workflow — what you might call tasks in business terms. **Edges** define the order in which those steps run. ## Step 3: Activate the workflow *** A workflow must be activated before it can be executed. This transitions the workflow from `draft` to `active`. ```bash theme={null} curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/activate \ -H "Content-Type: application/json" | jq . ``` Once activated, a workflow's structure is locked and cannot be edited directly. To make changes, clone it, modify the clone, and activate the new version. ## Step 4: Execute the workflow *** Trigger a workflow execution by sending input data. The `Idempotency-Key` header is required to ensure safe retries. ```bash theme={null} curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/executions \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b" \ -d '{ "inputData": { "message": "hello from my first workflow" } }' | jq . ``` The response confirms the execution has started: ```json theme={null} { "executionId": "019c96a0-10ce-75fc-a273-dc799079a99c", "workflowId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", "status": "running", "startedAt": "2026-03-18T14:35:00Z" } ``` Save the `executionId` for the next step. The `Idempotency-Key` header is required. Use a unique UUID per request to prevent duplicate executions on retry. ## Step 5: Check execution results *** Retrieve the outcome of a workflow execution: ```bash theme={null} curl -s http://localhost:4021/v1/executions/019c96a0-10ce-75fc-a273-dc799079a99c/results | jq . ``` The response includes the status of each step and the final output: ```json theme={null} { "executionId": "019c96a0-10ce-75fc-a273-dc799079a99c", "workflowId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", "status": "completed", "stepResults": [ { "stepNumber": 1, "stepName": "action_log-event", "nodeId": "log-event", "status": "completed", "output": { "action": "log" }, "executedAt": "2026-03-18T14:35:00Z", "durationMs": 12 } ], "finalOutput": { "workflow": { "message": "hello from my first workflow" } }, "startedAt": "2026-03-18T14:35:00Z", "completedAt": "2026-03-18T14:35:00Z" } ``` If the execution is still running, this endpoint returns a `422` status. Poll `/v1/executions/{executionId}` to check the current status before requesting results. ## Explore the API locally *** Flowker serves its OpenAPI 3.1 description and an interactive docs UI when `SWAGGER_ENABLED=true`. The sample environment file that `make dev-setup` copies into place sets it, so the surface is available on a local stack; anywhere the variable is unset the routes are not mounted and return `404`. | Surface | URL | | ---------------- | ------------------------------------------------------------------------ | | Interactive docs | [http://localhost:4021/openapi/docs](http://localhost:4021/openapi/docs) | | Spec (JSON) | `http://localhost:4021/openapi/openapi.json` | | Spec (YAML) | `http://localhost:4021/openapi/openapi.yaml` | Use the docs UI to: * Inspect all available endpoints * Test requests interactively * Understand request and response structures ## A note on authentication *** In the local development environment (`make dev`), authentication is disabled by default. In staging, production, or any environment with Access Manager enabled (`PLUGIN_AUTH_ENABLED=true`), all `/v1/*` endpoints require a Bearer token in the `Authorization` header: ```bash theme={null} curl -H "Authorization: Bearer " http://your-flowker-host/v1/workflows ``` ## What's next *** You now have a running Flowker environment and have executed your first workflow. From here, you can: * **Model real business processes** using different node types: `trigger`, `executor`, `conditional`, and `action` * **Integrate external systems** via provider configurations (connect to KYC providers, fraud engines, payment services) * **Design conditional flows** with conditional nodes that evaluate expressions on step outputs and route through the matching `sourceHandle` edge * **Monitor executions** using the execution status and results endpoints Flowker is designed to move from simple flows to production-grade orchestration without changing the core model. # Implementing observability in Flowker Source: https://docs.lerian.studio/en/reference/flowker/flowker-implementing-observability Enable and interpret Flowker's OpenTelemetry traces, metrics, and structured logs in Tempo, Prometheus, and Loki through a standard OTLP collector. Flowker emits traces, metrics, and structured logs using the **OpenTelemetry** standard. This guide explains what's available, how to enable it, and how to interpret the data in your observability stack. ## Overview *** Flowker's telemetry is built on three signals: | Signal | Backend | What it covers | | ------- | ---------- | ------------------------------------------------------ | | Traces | Tempo | Distributed spans across workflow executions and steps | | Metrics | Prometheus | HTTP request rates, latency, and system resource usage | | Logs | Loki | Structured JSON logs for every operation | All signals are exported via **OTLP (OpenTelemetry Protocol)** to a collector of your choice. ## Configuration *** Telemetry is controlled by environment variables. ```bash theme={null} # Enable telemetry (required to activate OTLP export) ENABLE_TELEMETRY=true # OTLP collector endpoint (required when ENABLE_TELEMETRY=true) OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 # Service identity OTEL_RESOURCE_SERVICE_NAME=flowker OTEL_RESOURCE_SERVICE_VERSION=1.0.0 OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT=production OTEL_LIBRARY_NAME=flowker # Log verbosity: debug | info | warn | error LOG_LEVEL=info ``` If `ENABLE_TELEMETRY=true` is set without `OTEL_EXPORTER_OTLP_ENDPOINT`, Flowker will fail to start. ## Distributed tracing *** Every HTTP request and internal operation creates an **OpenTelemetry span**. Spans are propagated through the full execution chain, so a single workflow run produces a connected trace from the HTTP handler down to individual executor steps. ### Span naming convention Spans follow a `..` pattern: **Execution spans** | Span name | Description | | ------------------------------------------------ | -------------------------------------------------------- | | `command.execution.execute` | Root span for a workflow execution | | `command.execution.execute_executor_node` | Span for each executor node processed | | `command.execution.execute_with_provider_config` | Span for a node resolved with a specific provider config | | `command.execution.recover` | Span for incomplete execution recovery at startup | **Workflow command spans** | Span name | Description | | -------------------------------- | ----------------------------- | | `command.workflow.create` | Create a new workflow | | `command.workflow.update` | Update an existing workflow | | `command.workflow.activate` | Activate a workflow | | `command.workflow.deactivate` | Deactivate a workflow | | `command.workflow.move_to_draft` | Move a workflow back to draft | | `command.workflow.clone` | Clone a workflow | | `command.workflow.delete` | Delete a workflow | **Executor configuration spans** | Span name | Description | | -------------------------------- | ----------------------------- | | `command.executor_config.update` | Update executor configuration | | `command.executor_config.delete` | Delete executor configuration | **Provider configuration spans** | Span name | Description | | --------------------------------- | ------------------------------ | | `command.provider_config.create` | Create provider configuration | | `command.provider_config.update` | Update provider configuration | | `command.provider_config.enable` | Enable provider configuration | | `command.provider_config.disable` | Disable provider configuration | | `command.provider_config.delete` | Delete provider configuration | **Query spans** | Span name | Description | | ----------------------------- | ------------------------- | | `query.execution.get` | Get execution by ID | | `query.execution.list` | List executions | | `query.execution.get_results` | Get execution results | | `query.workflow.get` | Get workflow by ID | | `query.workflow.list` | List workflows | | `query.executor_config.get` | Get executor config by ID | | `query.executor_config.list` | List executor configs | | `query.provider_config.get` | Get provider config by ID | | `query.provider_config.list` | List provider configs | In Grafana Tempo, search by service name (`flowker`) and filter by span name to isolate specific operations. Use `command.execution.execute` as the entry point to see a full workflow trace. ## Metrics *** Flowker exposes HTTP and system metrics automatically via the OpenTelemetry SDK. No additional configuration is needed beyond enabling telemetry. ### HTTP metrics (via otelfiber) Collected per route by the `otelfiber` middleware: | Metric | Type | Description | | ----------------------------- | ------------- | -------------------------------- | | `http.server.duration` | Histogram | Request duration in milliseconds | | `http.server.request.size` | Histogram | Request payload size in bytes | | `http.server.response.size` | Histogram | Response payload size in bytes | | `http.server.active_requests` | UpDownCounter | Number of in-flight requests | Each metric carries labels: `http.request.method`, `http.route`, `http.response.status_code`. ### System metrics | Metric | Type | Unit | Description | | ------------------ | ----- | ---------- | -------------------------------- | | `system.cpu.usage` | Gauge | percentage | CPU usage of the process host | | `system.mem.usage` | Gauge | percentage | Memory usage of the process host | ### Histogram buckets Latency histograms use the OpenTelemetry SDK default bucket boundaries. `http.server.duration` is recorded in milliseconds, so the boundaries are: ``` 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ``` Flowker does not expose a Prometheus scrape endpoint (`/metrics`) directly. Metrics are exported via OTLP to your collector, which then forwards to Prometheus. Configure your OTLP collector to include a `prometheusremotewrite` exporter. ## Structured logging *** Flowker uses **structured JSON logging** via Zap. Every log entry is enriched with contextual fields that can be indexed and queried in Loki. ### Log fields reference | Field | Description | Example | | --------------- | --------------------------------- | --------------------------- | | `operation` | Span/operation name | `command.execution.execute` | | `workflow.id` | Workflow identifier | `wf_abc123` | | `execution.id` | Execution identifier | `exec_xyz789` | | `node.id` | Node identifier within a workflow | `node-payment` | | `executor.id` | Executor identifier | `exec_cfg_001` | | `error.message` | Error description when applicable | `database ping failed: ...` | ### Log levels | Level | When used | | ------- | ---------------------------------------------------------------- | | `debug` | Detailed internal state — for development only | | `info` | Normal operation milestones (execution started, recovered, etc.) | | `warn` | Recoverable issues or unexpected but non-fatal conditions | | `error` | Operation failures that require attention | Set the `LOG_LEVEL` environment variable to control verbosity. ### Example log entries Workflow execution started: ```json theme={null} { "level": "info", "operation": "command.execution.execute", "workflow.id": "wf_abc123", "message": "Starting workflow execution" } ``` Incomplete execution recovery: ```json theme={null} { "level": "info", "operation": "command.execution.recover", "count": 3, "message": "Recovering incomplete executions" } ``` Executor node misconfigured: ```json theme={null} { "level": "error", "node.id": "node-payment", "execution.id": "exec_xyz789", "message": "Executor node missing providerConfigId" } ``` ## Health probes *** Flowker exposes Kubernetes-compatible liveness and readiness probes for operational monitoring. Liveness signals whether the process is running; readiness signals whether dependencies (notably the database) are reachable. Configure both at the cluster level as part of your deployment manifests so that orchestration can restart unhealthy pods and remove degraded instances from load balancers. ## Grafana dashboards *** Flowker's telemetry integrates directly with the Lerian observability stack. Pre-configured dashboards are available through the Lerian-managed Grafana instance. ### Recommended panels **Request throughput** * Query: `sum(rate(http_server_duration_count{service_name="flowker"}[5m])) by (http_route)` * Shows requests per second, broken down by route **P95 latency** * Query: `histogram_quantile(0.95, sum(rate(http_server_duration_bucket{service_name="flowker"}[5m])) by (le, http_route))` * Shows the 95th percentile response time per route **Error rate** * Query: `sum(rate(http_server_duration_count{service_name="flowker", http_response_status_code=~"5.."}[5m])) / sum(rate(http_server_duration_count{service_name="flowker"}[5m]))` * Shows the ratio of 5xx responses **Active executions (via logs)** * Loki query: `{service_name="flowker"} |= "Starting workflow execution" | count_over_time([1m])` For full observability stack setup, see [**Platform → Observability**](/en/platform/observability). # Get a Catalog Executor Source: https://docs.lerian.studio/en/reference/flowker/get-catalog-executor en/openapi/v3-current/flowker.yaml get /v1/catalog/executors/{id} Use this endpoint to retrieve the metadata and JSON Schema of a specific catalog executor. # Get a Catalog Provider Source: https://docs.lerian.studio/en/reference/flowker/get-catalog-provider en/openapi/v3-current/flowker.yaml get /v1/catalog/providers/{id} Use this endpoint to retrieve the metadata and JSON Schema of a specific catalog provider. # Get a Catalog Template Source: https://docs.lerian.studio/en/reference/flowker/get-catalog-template en/openapi/v3-current/flowker.yaml get /v1/catalog/templates/{id} Use this endpoint to retrieve the metadata and parameter JSON Schema of a specific catalog template. If the template references provider configurations, the schema is dynamically enriched with active provider configuration options. # Get a Catalog Trigger Source: https://docs.lerian.studio/en/reference/flowker/get-catalog-trigger en/openapi/v3-current/flowker.yaml get /v1/catalog/triggers/{id} Use this endpoint to retrieve the metadata and JSON Schema of a specific catalog trigger. # Get a Workflow Source: https://docs.lerian.studio/en/reference/flowker/get-workflow en/openapi/v3-current/flowker.yaml get /v1/workflows/{id} Use this endpoint to retrieve a specific workflow definition by its unique identifier. # List Catalog Executors Source: https://docs.lerian.studio/en/reference/flowker/list-catalog-executors en/openapi/v3-current/flowker.yaml get /v1/catalog/executors Use this endpoint to retrieve all built-in executors registered in the catalog. # List Catalog Providers Source: https://docs.lerian.studio/en/reference/flowker/list-catalog-providers en/openapi/v3-current/flowker.yaml get /v1/catalog/providers Use this endpoint to retrieve all registered providers from the catalog. # List Catalog Templates Source: https://docs.lerian.studio/en/reference/flowker/list-catalog-templates en/openapi/v3-current/flowker.yaml get /v1/catalog/templates Use this endpoint to retrieve all workflow templates registered in the catalog. # List Catalog Triggers Source: https://docs.lerian.studio/en/reference/flowker/list-catalog-triggers en/openapi/v3-current/flowker.yaml get /v1/catalog/triggers Use this endpoint to retrieve all built-in triggers registered in the catalog. # List Executors by Provider Source: https://docs.lerian.studio/en/reference/flowker/list-executors-by-provider en/openapi/v3-current/flowker.yaml get /v1/catalog/providers/{id}/executors Use this endpoint to retrieve all executors that belong to a specific provider. # List Parked Scheduled Occurrences Source: https://docs.lerian.studio/en/reference/flowker/list-parked-scheduled-occurrences en/openapi/v3-current/flowker.yaml get /v1/workflows/{id}/schedule/missed Use this endpoint to list a workflow's parked scheduled occurrences, both missed and pending review, that await a run or discard decision. # List Skipped Scheduled Occurrences Source: https://docs.lerian.studio/en/reference/flowker/list-skipped-scheduled-occurrences en/openapi/v3-current/flowker.yaml get /v1/workflows/{id}/schedule/skipped Use this endpoint to list a workflow's skipped scheduled occurrences, ordered oldest first. Each occurrence carries the reason it was skipped. # List Upcoming Scheduled Occurrences Source: https://docs.lerian.studio/en/reference/flowker/list-upcoming-scheduled-occurrences en/openapi/v3-current/flowker.yaml get /v1/workflows/{id}/schedule/upcoming Use this endpoint to compute the next occurrences of a workflow's schedule trigger, evaluated in the schedule's timezone. A workflow with no schedule trigger returns 422. # List Workflows Source: https://docs.lerian.studio/en/reference/flowker/list-workflows en/openapi/v3-current/flowker.yaml get /v1/workflows Use this endpoint to retrieve a paginated list of workflows. You can filter results by status. # Move a Workflow to Draft Source: https://docs.lerian.studio/en/reference/flowker/move-workflow-to-draft en/openapi/v3-current/flowker.yaml post /v1/workflows/{id}/draft Use this endpoint to move a workflow back to draft status for editing. Transitions the status from `inactive` to `draft`. # Update a Workflow Source: https://docs.lerian.studio/en/reference/flowker/update-workflow en/openapi/v3-current/flowker.yaml put /v1/workflows/{id} Use this endpoint to update an existing workflow definition. Only workflows with `draft` status can be updated. # Validate Catalog Template Parameters Source: https://docs.lerian.studio/en/reference/flowker/validate-catalog-template-params en/openapi/v3-current/flowker.yaml post /v1/catalog/templates/{id}/validate Use this endpoint to validate template parameters against the template's JSON Schema. # Validate a Node Configuration Source: https://docs.lerian.studio/en/reference/flowker/validate-executor-config en/openapi/v3-current/flowker.yaml post /v1/catalog/executors/{id}/validate Use this endpoint to check a workflow node's configuration against the JSON Schema of the catalog executor it invokes. The check is structural — it never calls the external service. # Approve an AI extraction review (ingest the candidates) Source: https://docs.lerian.studio/en/reference/matcher/approve-extraction-review en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}/approve Approves a PENDING_REVIEW review and runs the SINGLE deterministic handoff into the normal ingestion pipeline (dedup + outbox + match-trigger), linking 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. The approving principal is the reviewer (audit attribution). Tenant from the JWT, context from the path. # Approve an AI match-rule suggestion (create the rule) Source: https://docs.lerian.studio/en/reference/matcher/approve-match-rule-suggestion en/openapi/v3-current/matcher.yaml post /v1/matching/contexts/{contextId}/rule-suggestions/{suggestionId}/approve Approves a PENDING_REVIEW suggestion and 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. The approving principal is recorded for audit. Tenant from the JWT, context from the path. # Browse the supported format catalog Source: https://docs.lerian.studio/en/reference/matcher/browse-format-catalog en/openapi/v3-current/matcher.yaml get /v1/imports/formats Returns the read-only catalog of formats the ingestion engine can parse, namespaced region -> family -> variant. The catalog is global-first and static (built-in parsers carry no tenant), so the response is identical for every authenticated caller. No path, query, or body parameters — the tenant is irrelevant to the built-in catalog. # Count exceptions Source: https://docs.lerian.studio/en/reference/matcher/count-exceptions en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/exceptions/count Use this endpoint to retrieve the total number of exceptions for a specific context and date range. # Count matches Source: https://docs.lerian.studio/en/reference/matcher/count-matches en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/matches/count Use this endpoint to retrieve the total number of matched items for a specific context and date range. # Count transactions Source: https://docs.lerian.studio/en/reference/matcher/count-transactions en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/transactions/count Use this endpoint to retrieve the total number of transactions for a specific context and date range. # Count unmatched items Source: https://docs.lerian.studio/en/reference/matcher/count-unmatched en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/unmatched/count Use this endpoint to retrieve the total number of unmatched items for a specific context and date range. # Create an adjustment Source: https://docs.lerian.studio/en/reference/matcher/create-adjustment en/openapi/v3-current/matcher.yaml post /v1/matching/adjustments Use this endpoint to create a balancing journal entry (e.g., bank fee, FX difference) to resolve variance between matched transactions or on a single transaction. # Create aggregator connection Source: https://docs.lerian.studio/en/reference/matcher/create-aggregator-connection en/openapi/v3-current/matcher.yaml post /v1/discovery/aggregator-connections Creates an Open-Finance data-aggregator (Pluggy/Belvo) connection and seals its credential. The connection's config name is what the webhook token-mint endpoint binds a token to. The supplied credential is sealed before persistence and is never returned. # Create discovery connection Source: https://docs.lerian.studio/en/reference/matcher/create-discovery-connection en/openapi/v3-current/matcher.yaml post /v1/discovery/connections Provisions a Fetcher connection scoped to Matcher and synchronizes the local discovery cache. # Start a connection extraction Source: https://docs.lerian.studio/en/reference/matcher/create-extraction en/openapi/v3-current/matcher.yaml post /v1/discovery/connections/{connectionId}/extractions Use this endpoint to create an extraction request for a discovered connection and submit it to Fetcher for the authenticated tenant. # Create a manual match Source: https://docs.lerian.studio/en/reference/matcher/create-manual-match en/openapi/v3-current/matcher.yaml post /v1/matching/manual Use this endpoint to manually link multiple transactions (at least 2) into a match group with 100% confidence. Transactions must be UNMATCHED and belong to the specified context. # Delete aggregator connection Source: https://docs.lerian.studio/en/reference/matcher/delete-aggregator-connection en/openapi/v3-current/matcher.yaml delete /v1/discovery/aggregator-connections/{id} Soft-deletes an Open-Finance data-aggregator (Pluggy/Belvo) connection by id, freeing its config name for reuse. A non-aggregator connection id returns 404. # Delete discovery connection Source: https://docs.lerian.studio/en/reference/matcher/delete-discovery-connection en/openapi/v3-current/matcher.yaml delete /v1/discovery/connections/{connectionId} Deletes a Matcher-owned Fetcher connection and marks the local cache unavailable. # Delete an operator fixed-width layout template Source: https://docs.lerian.studio/en/reference/matcher/delete-layout-template en/openapi/v3-current/matcher.yaml delete /v1/imports/formats/templates/{templateId} Soft-deletes one active per-tenant fixed-width layout template by its id, freeing its format/variant key for reuse. Returns 404 when no active template matches. Tenant from the JWT; never the body. # Get discovery status Source: https://docs.lerian.studio/en/reference/matcher/discovery-status en/openapi/v3-current/matcher.yaml get /v1/discovery/status Use this endpoint to retrieve the current Fetcher integration status, including whether the Fetcher service is healthy and how many connections have been discovered. # Export exceptions report Source: https://docs.lerian.studio/en/reference/matcher/export-exceptions-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/exceptions/export Use this endpoint to export an exceptions report in CSV or PDF format for the specified date range. # Export matched transactions report Source: https://docs.lerian.studio/en/reference/matcher/export-matched-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/matched/export Use this endpoint to export a matched transactions report in CSV or PDF format for the specified date range. # Export summary report Source: https://docs.lerian.studio/en/reference/matcher/export-summary-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/summary/export Use this endpoint to export a reconciliation summary report in CSV or PDF format for the specified date range. # Export unmatched transactions report Source: https://docs.lerian.studio/en/reference/matcher/export-unmatched-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/unmatched/export Use this endpoint to export an unmatched transactions report in CSV or PDF format for the specified date range. # Export variance report Source: https://docs.lerian.studio/en/reference/matcher/export-variance-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/variance/export Use this endpoint to export a variance analysis report in CSV or PDF format for the specified date range. # Enqueue a document for AI extraction (human-review required) Source: https://docs.lerian.studio/en/reference/matcher/extract-document en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/sources/{sourceId}/extract-document Stores the uploaded source document tenant-scoped, runs deterministic + AI extraction, and enqueues the resulting transaction CANDIDATES in a human-review queue. AI output is NEVER authoritative: no transaction is reconciled until a human approves the review. The lane is gated by a global kill-switch AND a per-tenant opt-in (fail-closed: a non-opted-in tenant gets 403). The tenant is resolved from the JWT and the context/source from the path — never from the body. # Trigger a manual fetch-and-ingest from an external transport Source: https://docs.lerian.studio/en/reference/matcher/fetch-trigger en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/sources/{sourceId}/fetch Lists every object matching the supplied transport coordinates (SFTP today) and streams each into the trusted-content ingestion pipeline (dedup + outbox + match-trigger), returning a per-file outcome in fetch order. The tenant is resolved from the JWT and the context/source from the path — NEVER from the body. The body carries connection coordinates plus an OPAQUE credential reference, never a secret. A transport-level fetch failure (the external endpoint is unreachable or rejects the credential) returns 503; per-file intake failures are reported in the response body without failing the batch. # Get aggregator connection Source: https://docs.lerian.studio/en/reference/matcher/get-aggregator-connection en/openapi/v3-current/matcher.yaml get /v1/discovery/aggregator-connections/{id} Returns a single Open-Finance data-aggregator (Pluggy/Belvo) connection by id. The result is secret-free by construction: no credential material (clientId/secret/ciphertext) is ever returned. A non-aggregator connection id returns 404. # Get cash impact summary Source: https://docs.lerian.studio/en/reference/matcher/get-cash-impact en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/dashboard/cash-impact Use this endpoint to retrieve a cash impact summary showing the total unmatched financial exposure. Includes breakdowns by currency and by age bucket to help prioritize resolution efforts. # Get connection schema Source: https://docs.lerian.studio/en/reference/matcher/get-connection-schema en/openapi/v3-current/matcher.yaml get /v1/discovery/connections/{connectionId}/schema Use this endpoint to retrieve all discovered table schemas for a specific connection. # Get all dashboard aggregates Source: https://docs.lerian.studio/en/reference/matcher/get-dashboard-aggregates en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/dashboard Use this endpoint to retrieve combined dashboard aggregates including volume, match rate, and SLA statistics. # Get comprehensive dashboard metrics Source: https://docs.lerian.studio/en/reference/matcher/get-dashboard-metrics en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/dashboard/metrics Use this endpoint to retrieve complete dashboard metrics including summary, trends, and breakdowns for the Command Center. # Get a single AI extraction review Source: https://docs.lerian.studio/en/reference/matcher/get-extraction-review en/openapi/v3-current/matcher.yaml get /v1/imports/contexts/{contextId}/extraction-reviews/{reviewId} Returns one extraction review with its candidates, provenance, and lifecycle. Tenant from the JWT, context from the path. # Get an operator fixed-width layout template Source: https://docs.lerian.studio/en/reference/matcher/get-layout-template en/openapi/v3-current/matcher.yaml get /v1/imports/formats/templates/{templateId} Retrieves one active per-tenant fixed-width layout template by its id. Returns 404 when no active template matches. Tenant from the JWT; never the body. # Get match rate statistics Source: https://docs.lerian.studio/en/reference/matcher/get-match-rate-stats en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/dashboard/match-rate Use this endpoint to retrieve match rate percentage and trend data for a reconciliation context within the specified date range. # Get SLA statistics Source: https://docs.lerian.studio/en/reference/matcher/get-sla-stats en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/dashboard/sla Use this endpoint to retrieve SLA compliance statistics for a reconciliation context within the specified date range. # Get source breakdown Source: https://docs.lerian.studio/en/reference/matcher/get-source-breakdown en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/dashboard/source-breakdown Use this endpoint to retrieve a breakdown of reconciliation metrics by source for a specific context and date range. Shows match rates, transaction counts, and unmatched amounts per source. # Get reconciliation summary report Source: https://docs.lerian.studio/en/reference/matcher/get-summary-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/summary Use this endpoint to retrieve an aggregated reconciliation summary for a specific context and date range, including matched and unmatched counts and amounts. # Get volume statistics Source: https://docs.lerian.studio/en/reference/matcher/get-volume-stats en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/dashboard/volume Use this endpoint to retrieve transaction volume statistics for a reconciliation context within the specified date range. # Ignore a transaction Source: https://docs.lerian.studio/en/reference/matcher/ignore-transaction en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/transactions/{transactionId}/ignore Use this endpoint to mark a transaction as "Do Not Match" with a required reason. Only UNMATCHED transactions can be ignored. # List aggregator connections Source: https://docs.lerian.studio/en/reference/matcher/list-aggregator-connections en/openapi/v3-current/matcher.yaml get /v1/discovery/aggregator-connections Returns a cursor-paginated list of the tenant's Open-Finance data-aggregator (Pluggy/Belvo) connections, ordered by config name. The list is secret-free by construction: no credential material (clientId/secret/ciphertext) is ever returned. # List discovered connections Source: https://docs.lerian.studio/en/reference/matcher/list-discovery-connections en/openapi/v3-current/matcher.yaml get /v1/discovery/connections Use this endpoint to retrieve all discovered Fetcher database connections available for the current tenant. # List connector types Source: https://docs.lerian.studio/en/reference/matcher/list-discovery-connector-types en/openapi/v3-current/matcher.yaml get /v1/discovery/connector-types Returns the connector types the engine registry has actually registered for this deployment, each tagged with a backend-derived category ('database' or 'rest'). The list reflects the live registry — only the connectors registered at boot appear, with no static enum of all possible types. Aggregator-vendor types (Pluggy/Belvo) are excluded — they are provisioned through the aggregator-connections surface. # List AI extraction reviews (the human-review queue) Source: https://docs.lerian.studio/en/reference/matcher/list-extraction-reviews en/openapi/v3-current/matcher.yaml get /v1/imports/contexts/{contextId}/extraction-reviews Cursor-paginated list of extraction reviews for a context, optionally filtered by lifecycle status (e.g. PENDING_REVIEW for the queue). Tenant from the JWT, context from the path. Reads already-stored config-only candidates; it does not egress anything. # List per-row import errors for a job Source: https://docs.lerian.studio/en/reference/matcher/list-ingestion-job-errors en/openapi/v3-current/matcher.yaml get /v1/imports/contexts/{contextId}/jobs/{jobId}/errors Returns the stored per-row parse/normalization errors for an ingestion job (capped at 100 per job), with the uncapped failure total and truncation accounting. Lets clients explain failed or partially-failed imports. # List transactions for a job Source: https://docs.lerian.studio/en/reference/matcher/list-job-transactions en/openapi/v3-current/matcher.yaml get /v1/imports/contexts/{contextId}/jobs/{jobId}/transactions Use this endpoint to retrieve all transactions extracted from an ingestion job. You can sort the results by various fields. The response uses cursor-based pagination. # List operator fixed-width layout templates Source: https://docs.lerian.studio/en/reference/matcher/list-layout-templates en/openapi/v3-current/matcher.yaml get /v1/imports/formats/templates Returns every active per-tenant fixed-width layout template. The list is unpaginated: layout templates are bounded operator config. Tenant from the JWT; never the body. # List match candidates for a transaction Source: https://docs.lerian.studio/en/reference/matcher/list-match-candidates en/openapi/v3-current/matcher.yaml get /v1/matching/candidates Returns a ranked list of opposite-side unmatched transactions the engine considers plausible counterparts for the target transaction, each with a confidence score, the rule that drove it, a per-component rationale (the "why"), and amount/date deltas. Powers the exception-resolution and manual-match candidate pickers. Scope: proposals are scored by the deterministic rule engine over raw transaction amounts and do NOT apply run-time fee normalization or the FX-variance band; only 1:1 pairwise counterparts are returned. # List AI match-rule suggestions (the human-review queue) Source: https://docs.lerian.studio/en/reference/matcher/list-match-rule-suggestions en/openapi/v3-current/matcher.yaml get /v1/matching/contexts/{contextId}/rule-suggestions Cursor-paginated list of rule suggestions for a context, optionally filtered by status (e.g. PENDING_REVIEW). Reads stored config-only candidates; egresses nothing. Tenant from the JWT, context from the path. # Get match run results Source: https://docs.lerian.studio/en/reference/matcher/list-match-run-groups en/openapi/v3-current/matcher.yaml get /v1/matching/runs/{runId}/groups Use this endpoint to retrieve all match groups from a specific matching run. Each group contains matched transaction pairs and confidence scores. The response uses cursor-based pagination. # List match runs Source: https://docs.lerian.studio/en/reference/matcher/list-match-runs en/openapi/v3-current/matcher.yaml get /v1/matching/contexts/{contextId}/runs Use this endpoint to retrieve all match runs for a specific reconciliation context. The results are sorted by creation time descending. # List matched transactions report Source: https://docs.lerian.studio/en/reference/matcher/list-matched-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/matched Use this endpoint to retrieve a paginated list of matched transactions for a specific context and date range. The response uses cursor-based pagination. # List open items Source: https://docs.lerian.studio/en/reference/matcher/list-open-items en/openapi/v3-current/matcher.yaml get /v1/matching/contexts/{contextId}/open-items Returns a cursor-paginated list of open-item residual ledger entries for a reconciliation context — the carried, partially-filled, and aged obligations the context carries forward across match runs. These are structurally distinct from never-matched (UNMATCHED) transactions. Optionally narrowed by lifecycle status. The listing is readable regardless of context status. # List unmatched transactions report Source: https://docs.lerian.studio/en/reference/matcher/list-unmatched-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/unmatched Use this endpoint to retrieve a paginated list of unmatched transactions for a specific context and date range. The response uses cursor-based pagination. # List variance report Source: https://docs.lerian.studio/en/reference/matcher/list-variance-report en/openapi/v3-current/matcher.yaml get /v1/reports/contexts/{contextId}/variance Use this endpoint to retrieve a paginated variance analysis report for a specific context and date range. The response uses cursor-based pagination. # Matcher error list Source: https://docs.lerian.studio/en/reference/matcher/matcher-error-list Matcher APIs return a structured error object with a stable code, HTTP status, and message so you can diagnose issues and route them to the right team. ### Error format ```json JSON theme={null} { "code": "", "title": "", "message": "" } ``` ### Field definitions * **`code`** – A stable product code that uniquely identifies the error (e.g., `MTCH-0001`). Useful for programmatic handling and support requests. * **`title`** – A short, human-readable summary of the issue. * **`message`** – Detailed guidance to help you resolve the error. * **`details`** – *(optional)* A structured object with additional context, such as field-level validation errors. This structure ensures you always get actionable feedback when something doesn't go as expected. ### Field-level error details For issues specific to individual fields, a `details` object provides additional context. Examples: ```json Example 1 theme={null} { "code": "MTCH-0001", "title": "Bad Request", "message": "The request body is invalid or malformed. Please verify the JSON format and try again.", "details": { "context_id": "context_id is a required field" } } ``` ```json Example 2 theme={null} { "code": "MTCH-0005", "title": "Not Found", "message": "The requested resource does not exist. Please verify the ID and try again." } ``` ## General errors *** These errors apply to all Matcher API endpoints. **400** | `code` | `title` | `message` | | ----------- | ----------- | ----------------------------------------------------------------------------------------- | | `MTCH-0001` | Bad Request | The request body is invalid or malformed. Please verify the request format and try again. | | `MTCH-0011` | Bad Request | The request failed. Please verify the request and try again. | | `MTCH-0013` | Bad Request | The context ID is invalid. Please verify the ID format and try again. | **401** | `code` | `title` | `message` | | ----------- | ------------ | ------------------------------------------------------------------------------------- | | `MTCH-0003` | Unauthorized | The request could not be authenticated. Please verify your credentials and try again. | **403** | `code` | `title` | `message` | | ----------- | --------- | ------------------------------------------------------------------------------------------------------- | | `MTCH-0004` | Forbidden | You do not have permission to perform this action. Please verify your access level. | | `MTCH-0012` | Forbidden | The reconciliation context is not active. Please activate the context before performing this operation. | **404** | `code` | `title` | `message` | | ----------- | --------- | -------------------------------------------------------------------------- | | `MTCH-0005` | Not Found | The requested resource does not exist. Please verify the ID and try again. | **409** | `code` | `title` | `message` | | ----------- | -------- | ------------------------------------------------------------------------------------------ | | `MTCH-0007` | Conflict | The request conflicts with the current state of the resource. Please verify and try again. | **413** | `code` | `title` | `message` | | ----------- | ------------------------ | ---------------------------------------------------------------------------------------------- | | `MTCH-0010` | Request Entity Too Large | The uploaded file exceeds the maximum allowed size. Please reduce the file size and try again. | **422** | `code` | `title` | `message` | | ----------- | -------------------- | --------------------------------------------------------------------------------------------------- | | `MTCH-0006` | Unprocessable Entity | The request was well-formed but could not be processed. Please verify the input data and try again. | **429** | `code` | `title` | `message` | | ----------- | ----------------- | ---------------------------------------------------------------------- | | `MTCH-0009` | Too Many Requests | The request rate limit has been exceeded. Please wait before retrying. | **500** | `code` | `title` | `message` | | ----------- | --------------------- | ---------------------------------------------------------------------------------------------- | | `MTCH-0002` | Internal Server Error | An unexpected error occurred. Please try again later or contact support if the issue persists. | **503** | `code` | `title` | `message` | | ----------- | ------------------- | ---------------------------------------------------------------------- | | `MTCH-0008` | Service Unavailable | A required service is temporarily unavailable. Please try again later. | | `MTCH-0019` | Service Unavailable | Object storage is temporarily unavailable. Please try again later. | ## Configuration errors *** These errors are returned when there is an issue with the reconciliation configuration (contexts, sources, field maps, match rules, fee rules, schedules). **400** | `code` | `title` | `message` | | ----------- | ----------- | ---------------------------------------------------------------------- | | `MTCH-0109` | Bad Request | A context name is required. Please provide a valid name and try again. | **404** | `code` | `title` | `message` | | ----------- | --------- | ------------------------------------------------------------------------------------------ | | `MTCH-0110` | Not Found | The specified context was not found. Please verify the context ID and try again. | | `MTCH-0111` | Not Found | The specified source was not found. Please verify the source ID and try again. | | `MTCH-0112` | Not Found | The specified field map was not found. Please verify the field map ID and try again. | | `MTCH-0113` | Not Found | The specified match rule was not found. Please verify the match rule ID and try again. | | `MTCH-0114` | Not Found | The specified fee rule was not found. Please verify the fee rule ID and try again. | | `MTCH-0115` | Not Found | The specified fee schedule was not found. Please verify the fee schedule ID and try again. | | `MTCH-0116` | Not Found | The specified schedule was not found. Please verify the schedule ID and try again. | **409** | `code` | `title` | `message` | | ----------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `MTCH-0101` | Conflict | A context with this name already exists. Please choose a different name and try again. | | `MTCH-0102` | Conflict | A resource with this priority already exists. Please choose a different priority value and try again. | | `MTCH-0103` | Conflict | The resource is in an invalid state for this operation. Please verify its current state and try again. | | `MTCH-0104` | Conflict | The context has been archived. Please use an active context for this operation. | | `MTCH-0105` | Conflict | This resource cannot be deleted because it has an associated field map. Please remove the field map first. | | `MTCH-0106` | Conflict | This resource cannot be deleted because it has dependent resources. Please remove the dependent resources first. | | `MTCH-0107` | Conflict | A resource with this priority already exists. Please choose a different priority value. | | `MTCH-0108` | Conflict | The fee schedule is currently in use and cannot be modified or deleted. | | `MTCH-0117` | Conflict | This resource cannot be deleted because it has associated transactions. Please remove the transactions first. | ## Discovery errors *** These errors are returned during data source discovery and connection management. Discovery relies on Fetcher, Lerian's internal data-extraction service — see [Discovery](/en/matcher/integrations/matcher-discovery) for how it works. **400** | `code` | `title` | `message` | | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | | `MTCH-0204` | Bad Request | The extraction request is invalid. Please verify the parameters and try again. | | `MTCH-0208` | Bad Request | The specified connector type is not registered on this deployment. Please verify the connector type and try again. | **404** | `code` | `title` | `message` | | ----------- | --------- | -------------------------------------------------------------------------------------- | | `MTCH-0201` | Not Found | The specified connection was not found. Please verify the connection ID and try again. | | `MTCH-0202` | Not Found | The specified extraction was not found. Please verify the extraction ID and try again. | **409** | `code` | `title` | `message` | | ----------- | -------- | ----------------------------------------------------------------------------------------------------- | | `MTCH-0205` | Conflict | A discovery refresh is already in progress. Please wait for it to complete before starting a new one. | **503** | `code` | `title` | `message` | | ----------- | ------------------- | ---------------------------------------------------------------------------------------------- | | `MTCH-0203` | Service Unavailable | The Fetcher service is temporarily unavailable. Please try again later. | | `MTCH-0206` | Service Unavailable | The connector's data for the requested day has not been validated yet. Please try again later. | | `MTCH-0207` | Service Unavailable | The connector's report did not complete within the allotted time. Please try again later. | ## Ingestion errors *** These errors are returned during transaction file upload and processing. **400** | `code` | `title` | `message` | | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | | `MTCH-0303` | Bad Request | The file format is required. Please specify the format and try again. | | `MTCH-0304` | Bad Request | The uploaded file is empty. Please provide a non-empty file and try again. | | `MTCH-0307` | Bad Request | The file was well-formed but contained no record elements to ingest. Please verify the file content and try again. | | `MTCH-0308` | Bad Request | The uploaded file schema is not supported. Please verify the file structure and try again. | | `MTCH-0317` | Bad Request | A mapping sample is required. Please provide a sample and try again. | | `MTCH-0322` | Bad Request | The document body is empty. Please provide a non-empty document and try again. | **403** | `code` | `title` | `message` | | ----------- | --------- | ------------------------------------------------------------------------------------- | | `MTCH-0319` | Forbidden | Document extraction is not enabled for this tenant. Please contact support to opt in. | **404** | `code` | `title` | `message` | | ----------- | --------- | ---------------------------------------------------------------------------------------------- | | `MTCH-0301` | Not Found | The specified ingestion source was not found. Please verify the source ID and try again. | | `MTCH-0302` | Not Found | The specified ingestion field map was not found. Please verify the field map ID and try again. | | `MTCH-0306` | Not Found | The specified ingestion job was not found. Please verify the job ID and try again. | **408** | `code` | `title` | `message` | | ----------- | --------------- | ------------------------------------------------------------------------------------------------- | | `MTCH-0311` | Request Timeout | The upload stalled because no data was received within the allowed time. Please retry the upload. | **409** | `code` | `title` | `message` | | ----------- | -------- | --------------------------------------------------------------------------------------------------- | | `MTCH-0305` | Conflict | The ingestion job is in an invalid state for this operation. Please verify its current state. | | `MTCH-0324` | Conflict | The extraction review is no longer pending and cannot be modified. Please verify its current state. | **422** | `code` | `title` | `message` | | ----------- | -------------------- | ------------------------------------------------------------------------------------------------------------ | | `MTCH-0309` | Unprocessable Entity | The ingestion job exceeded the configured error rate threshold. Please review the source data and try again. | | `MTCH-0310` | Unprocessable Entity | The source configuration is invalid. Please fix the source configuration and retry the upload. | | `MTCH-0312` | Unprocessable Entity | The source's field map is invalid. Please fix the field map and retry the upload. | | `MTCH-0321` | Unprocessable Entity | No transaction candidates could be extracted from the document. Please verify the document and try again. | **503** | `code` | `title` | `message` | | ----------- | ------------------- | -------------------------------------------------------------------------------------------- | | `MTCH-0313` | Service Unavailable | Fetch-and-ingest is not enabled on this deployment. Please upload the file manually. | | `MTCH-0314` | Service Unavailable | The external transport could not be reached or read. Please try again later. | | `MTCH-0315` | Service Unavailable | Mapping proposal is not enabled on this deployment. Please configure the field map manually. | | `MTCH-0316` | Service Unavailable | The mapping advisor is unavailable. Please configure a field map manually. | | `MTCH-0318` | Service Unavailable | Document extraction is not enabled on this deployment. Please ingest the file manually. | | `MTCH-0320` | Service Unavailable | The document extractor is unavailable. Please ingest the file manually. | | `MTCH-0323` | Service Unavailable | Extraction review is not enabled on this deployment. Please try again later. | ## Matching errors *** These errors are returned during the reconciliation matching process. **400** | `code` | `title` | `message` | | ----------- | ----------- | -------------------------------------------------------------------------------------------------------- | | `MTCH-0401` | Bad Request | No sources are configured for this context. Please configure at least one source before running a match. | | `MTCH-0402` | Bad Request | At least two sources are required for matching. Please configure additional sources. | | `MTCH-0403` | Bad Request | Each source must have a defined side. Please specify the source side and try again. | | `MTCH-0404` | Bad Request | The one-to-one topology configuration is invalid. Please verify the source configuration. | | `MTCH-0405` | Bad Request | The one-to-many topology configuration is invalid. Please verify the source configuration. | **403** | `code` | `title` | `message` | | ----------- | --------- | --------------------------------------------------------------------------------- | | `MTCH-0409` | Forbidden | Rule suggestion is not enabled for this tenant. Please contact support to opt in. | **409** | `code` | `title` | `message` | | ----------- | -------- | -------------------------------------------------------------------------------------------- | | `MTCH-0408` | Conflict | A matching run is already in progress for this context. Please wait for it to complete. | | `MTCH-0413` | Conflict | The suggestion is no longer pending and cannot be modified. Please verify its current state. | **422** | `code` | `title` | `message` | | ----------- | -------------------- | --------------------------------------------------------------------------------------- | | `MTCH-0406` | Unprocessable Entity | Fee rules are required but not configured. Please add fee rules before running a match. | | `MTCH-0407` | Unprocessable Entity | Fee rules are misconfigured. Please review the fee rule settings and try again. | | `MTCH-0412` | Unprocessable Entity | The approved suggestion failed validation. Please review the suggestion and try again. | **503** | `code` | `title` | `message` | | ----------- | ------------------- | -------------------------------------------------------------------------------------- | | `MTCH-0410` | Service Unavailable | Rule suggestion is not enabled on this deployment. Please author match rules manually. | | `MTCH-0411` | Service Unavailable | The rule advisor is unavailable. Please author match rules manually. | ## Exception errors *** These errors are returned during exception handling, disputes, and callback processing. **404** | `code` | `title` | `message` | | ----------- | --------- | ------------------------------------------------------------------------------------ | | `MTCH-0501` | Not Found | The specified exception was not found. Please verify the exception ID and try again. | | `MTCH-0502` | Not Found | The specified dispute was not found. Please verify the dispute ID and try again. | | `MTCH-0507` | Not Found | The specified comment was not found. Please verify the comment ID and try again. | **409** | `code` | `title` | `message` | | ----------- | -------- | ----------------------------------------------------------------------------------------- | | `MTCH-0504` | Conflict | A callback for this exception is already being processed. Please wait for it to complete. | | `MTCH-0505` | Conflict | The callback encountered a transient error and can be retried. Please retry the request. | **422** | `code` | `title` | `message` | | ----------- | -------------------- | ----------------------------------------------------------------------------------------- | | `MTCH-0503` | Unprocessable Entity | The exception is in an invalid state for this operation. Please verify its current state. | | `MTCH-0508` | Unprocessable Entity | The dispatch target type is not supported. Please verify the target configuration. | | `MTCH-0509` | Unprocessable Entity | The dispatch connector is not configured. Please configure the connector and try again. | **429** | `code` | `title` | `message` | | ----------- | ----------------- | ----------------------------------------------------------------------- | | `MTCH-0506` | Too Many Requests | The callback rate limit has been exceeded. Please wait before retrying. | ## Governance errors *** These errors are returned during audit log and governance operations. **404** | `code` | `title` | `message` | | ----------- | --------- | -------------------------------------------------------------------------------------------- | | `MTCH-0601` | Not Found | The specified audit log was not found. Please verify the audit log ID and try again. | | `MTCH-0602` | Not Found | The specified actor mapping was not found. Please verify the actor mapping ID and try again. | | `MTCH-0603` | Not Found | The specified archive was not found. Please verify the archive ID and try again. | ## Reporting errors *** These errors are returned during report export operations. **400** | `code` | `title` | `message` | | ----------- | ----------- | ------------------------------------------------------------------------------- | | `MTCH-0705` | Bad Request | The specified export format is invalid. Please verify the format and try again. | **404** | `code` | `title` | `message` | | ----------- | --------- | ------------------------------------------------------------------------------- | | `MTCH-0701` | Not Found | The specified export job was not found. Please verify the job ID and try again. | **409** | `code` | `title` | `message` | | ----------- | -------- | --------------------------------------------------------------------------------- | | `MTCH-0703` | Conflict | The export is not yet ready for download. Please wait for the export to complete. | **410** | `code` | `title` | `message` | | ----------- | ------- | -------------------------------------------------------------------------------------- | | `MTCH-0704` | Gone | The export has expired and is no longer available. Please create a new export request. | **503** | `code` | `title` | `message` | | ----------- | ------------------- | ----------------------------------------------------------------------------- | | `MTCH-0702` | Service Unavailable | The export worker is currently disabled. Please contact support to enable it. | ## Idempotency errors *** These errors are returned when there is an issue with idempotency key handling. **400** | `code` | `title` | `message` | | ----------- | ----------- | ------------------------------------------------------------------------------------ | | `MTCH-0015` | Bad Request | The provided idempotency key is invalid. Please verify the key format and try again. | **409** | `code` | `title` | `message` | | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------- | | `MTCH-0018` | Conflict | A request with this idempotency key is currently being processed. Please wait for it to complete before retrying. | **500** | `code` | `title` | `message` | | ----------- | --------------------- | ------------------------------------------------------------------------------------------ | | `MTCH-0016` | Internal Server Error | The idempotency configuration is invalid. Please contact support if the issue persists. | | `MTCH-0017` | Internal Server Error | An unexpected error occurred while processing the idempotency key. Please try again later. | # Mint an aggregator webhook token Source: https://docs.lerian.studio/en/reference/matcher/mint-aggregator-webhook-token en/openapi/v3-current/matcher.yaml post /v1/discovery/webhooks/tokens Mints a webhook token bound to an existing aggregator connection and returns the raw token plus its provider-facing webhook URL ONCE. Only the token's SHA-256 hash is stored. # Preview a transaction file Source: https://docs.lerian.studio/en/reference/matcher/preview-upload en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/sources/{sourceId}/preview Use this endpoint to preview a transaction file before importing it. Returns sample rows and detected columns to help verify the file structure and field mapping. # Propose a config-only field-map/dialect mapping for a source sample Source: https://docs.lerian.studio/en/reference/matcher/propose-mapping en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/sources/{sourceId}/mapping-proposal Inspects a representative sample of a source file and returns a CONFIG-ONLY mapping/dialect proposal (column→canonical-key suggestions, dialect, per-field confidence and rationale). It is ADVISORY and side-effect-free: producing a proposal PERSISTS NOTHING. The operator reviews the proposal and confirms it through the EXISTING field-map declaration path. The tenant is resolved from the JWT and the context/source from the path — never from the body. The response never carries parsed values, amounts, or transactions. # Receive an aggregator webhook Source: https://docs.lerian.studio/en/reference/matcher/receive-aggregator-webhook en/openapi/v3-current/matcher.yaml post /v1/discovery/webhooks/{provider}/{webhookToken} Receives a Pluggy/Belvo aggregator webhook and asynchronously pulls the signaled data into the ingestion pipeline. This route carries no operator JWT. It is authenticated by an opaque token in the URL path (resolved to a tenant binding server-side, never claimed) PLUS a per-provider source check: a valid HMAC-SHA256 of the raw body in the X-Webhook-Signature header, OR membership in the provider's source-IP allowlist. Both layers fail closed; the tenant is derived solely from the resolved token binding. # Refresh discovery Source: https://docs.lerian.studio/en/reference/matcher/refresh-discovery en/openapi/v3-current/matcher.yaml post /v1/discovery/refresh Use this endpoint to force an immediate sync with the Fetcher service, updating the list of connections and their schemas. # Reject an AI extraction review (discard the candidates) Source: https://docs.lerian.studio/en/reference/matcher/reject-extraction-review en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}/reject Rejects a PENDING_REVIEW review and discards its candidates — nothing is ingested or reconciled. The rejecting principal is recorded for audit. Tenant from the JWT, context from the path. # Reject an AI match-rule suggestion (discard it) Source: https://docs.lerian.studio/en/reference/matcher/reject-match-rule-suggestion en/openapi/v3-current/matcher.yaml post /v1/matching/contexts/{contextId}/rule-suggestions/{suggestionId}/reject Rejects a PENDING_REVIEW suggestion — nothing is created. The rejecting principal is recorded for audit. Tenant from the JWT, context from the path. # Get a discovered connection Source: https://docs.lerian.studio/en/reference/matcher/retrieve-discovery-connection en/openapi/v3-current/matcher.yaml get /v1/discovery/connections/{connectionId} Use this endpoint to retrieve a single discovered Fetcher connection by its internal identifier. # Get an extraction request Source: https://docs.lerian.studio/en/reference/matcher/retrieve-extraction en/openapi/v3-current/matcher.yaml get /v1/discovery/extractions/{extractionId} Use this endpoint to retrieve the details of a specific extraction request by its identifier. # Get ingestion job status Source: https://docs.lerian.studio/en/reference/matcher/retrieve-ingestion-job en/openapi/v3-current/matcher.yaml get /v1/imports/contexts/{contextId}/jobs/{jobId} Use this endpoint to retrieve the status and details of a specific ingestion job by its identifier. The response includes progress metrics and error information. # Get match run Source: https://docs.lerian.studio/en/reference/matcher/retrieve-match-run en/openapi/v3-current/matcher.yaml get /v1/matching/runs/{runId} Use this endpoint to retrieve the details of a specific match run by its identifier. The response includes status, timing, and aggregate statistics. # Trigger a matching run Source: https://docs.lerian.studio/en/reference/matcher/run-match en/openapi/v3-current/matcher.yaml post /v1/matching/contexts/{contextId}/run Use this endpoint to trigger a matching run for a reconciliation context. You can use DRY_RUN mode to test rules without committing results, or COMMIT mode to persist matches. # Search transactions Source: https://docs.lerian.studio/en/reference/matcher/search-transactions en/openapi/v3-current/matcher.yaml get /v1/imports/contexts/{contextId}/transactions/search Use this endpoint to search transactions within a reconciliation context. Supports free-text search, amount range filters, date filters, and status filters with offset-based pagination. # Simulate a match rule against a context (read-only dry run) Source: https://docs.lerian.studio/en/reference/matcher/simulate-match-rule en/openapi/v3-current/matcher.yaml post /v1/matching/simulate Previews how a single rule — an existing configured rule (ruleId) OR an inline candidate rule (rule) — would match a context's unmatched transactions, WITHOUT committing anything. Returns the number of 1:1 groups the rule would form, a bounded sample of would-match pairs (each with a confidence score, a per-component rationale (the "why"), and matched composite keys), and the per-side unmatched counts. Powers the rule-authoring "will this rule actually match?" preview. Scope: scored by the deterministic rule engine over raw transaction amounts; it does NOT apply run-time fee normalization or the FX-variance band, and previews only 1:1 pairwise grouping (no 1:N/N:M allocation). Nothing is persisted; the tenant is taken from the JWT, never the body. # Submit an operator fixed-width layout template Source: https://docs.lerian.studio/en/reference/matcher/submit-layout-template en/openapi/v3-current/matcher.yaml post /v1/imports/formats/templates Validates and stores a per-tenant fixed-width layout template (region/family/variant-namespaced). The layout is run through the well-formedness gate BEFORE storage: overrun, overlap, missing-required, zero-field, or a mis-marked money column (money MUST be kind=decimal) rejects with 422 and the template is never stored. An approved template is resolved by the parse path as an additive layout source. Tenant from the JWT; never the body. # Produce AI match-rule suggestions (human-review required) Source: https://docs.lerian.studio/en/reference/matcher/suggest-match-rules en/openapi/v3-current/matcher.yaml post /v1/matching/contexts/{contextId}/rule-suggestions Builds AGGREGATE, privacy-safe history features for the context, asks the AI advisor for config-only candidate rules, and enqueues each surviving candidate in a human-review queue. AI output is NEVER authoritative: producing a suggestion CREATES NO rule. A rule is created only when a human APPROVES a suggestion. The lane is gated by a global advisor kill-switch AND a per-tenant opt-in (fail-closed: a non-opted-in tenant gets 403). Tenant from the JWT, context from the path. The egress payload is aggregates only — no raw transaction, money, or PII. # Test aggregator connection credentials Source: https://docs.lerian.studio/en/reference/matcher/test-aggregator-connection en/openapi/v3-current/matcher.yaml post /v1/discovery/aggregator-connections/test Runs a live connectivity check for an existing Open-Finance data-aggregator (Pluggy/Belvo) connection using its already-sealed credential, addressed by (vendor, configName). No credential is supplied or returned: the result is the secret-free boolean health of the connection. # Test a connection Source: https://docs.lerian.studio/en/reference/matcher/test-discovery-connection en/openapi/v3-current/matcher.yaml post /v1/discovery/connections/{connectionId}/test Use this endpoint to test connectivity for a specific discovered connection owned by the current tenant. # Break/Unmatch a match group Source: https://docs.lerian.studio/en/reference/matcher/unmatch-group en/openapi/v3-current/matcher.yaml delete /v1/matching/groups/{matchGroupId} Use this endpoint to break an incorrect match group. This rejects the match with a reason and reverts all associated transactions to UNMATCHED status. # Update aggregator connection Source: https://docs.lerian.studio/en/reference/matcher/update-aggregator-connection en/openapi/v3-current/matcher.yaml put /v1/discovery/aggregator-connections/{id} Edits an existing Open-Finance data-aggregator (Pluggy/Belvo) connection by id so a mistyped baseUrl is not permanent. The vendor is immutable. clientId/secret are optional: supply both to rotate the sealed credential, or omit both to leave the stored secret intact. The supplied credential is sealed before persistence and is never returned. A non-aggregator connection id returns 404. # Update discovery connection Source: https://docs.lerian.studio/en/reference/matcher/update-discovery-connection en/openapi/v3-current/matcher.yaml patch /v1/discovery/connections/{connectionId} Updates a Matcher-owned Fetcher connection and synchronizes the local discovery cache. # Replace an operator fixed-width layout template Source: https://docs.lerian.studio/en/reference/matcher/update-layout-template en/openapi/v3-current/matcher.yaml put /v1/imports/formats/templates/{templateId} Fully replaces a stored per-tenant fixed-width layout template (records/fields/discriminator/recordWidths/requiredFields) by its id — a PUT full replace, not a sparse patch, since the byte-range invariants are whole-layout properties. The replacement is run through the SAME well-formedness gate the submission path enforces BEFORE storage: overrun, overlap, missing-required, zero-field, or a mis-marked money column (money MUST be kind=decimal) rejects with 422 and the stored template is left unchanged. Returns 404 when no active template matches, 409 when the new format key collides with another active template. Tenant from the JWT; never the body. # Upload transaction file Source: https://docs.lerian.studio/en/reference/matcher/upload-transaction-file en/openapi/v3-current/matcher.yaml post /v1/imports/contexts/{contextId}/sources/{sourceId}/upload Use this endpoint to upload a transaction file (CSV, JSON, or XML) for ingestion into a reconciliation context. The file is parsed, validated, and transactions are extracted for matching. # Count Pending-Review Occurrences Source: https://docs.lerian.studio/en/reference/flowker/count-pending-review-occurrences en/openapi/v3-current/flowker.yaml get /v1/workflows/schedule/pending-review-counts Use this endpoint to count, per workflow, the scheduled occurrences parked in the pending-review state across the caller's tenant. The counts map is sparse: a workflow with no pending-review occurrence is absent from the response. # Create a Provider Configuration Source: https://docs.lerian.studio/en/reference/flowker/create-provider-configuration en/openapi/v3-current/flowker.yaml post /v1/provider-configurations Use this endpoint to create a new provider configuration. The configuration is validated against the provider's JSON Schema. # Delete an Executor Configuration Source: https://docs.lerian.studio/en/reference/flowker/delete-executor-configuration en/openapi/v3-current/flowker.yaml delete /v1/executors/{id} Use this endpoint to delete an executor configuration. Only configurations with `unconfigured`, `configured`, or `disabled` status can be deleted. # Delete an External OpenAPI Schema Source: https://docs.lerian.studio/en/reference/flowker/delete-openapi-schema en/openapi/v3-current/flowker.yaml delete /v1/openapi-schemas/{id} Use this endpoint to delete an external OpenAPI schema. An active provider configuration or an active workflow that references the schema blocks the delete with 409. References from inactive workflows or disabled provider configurations do not block: the delete proceeds and the response carries the warnings. A schema with no references at all returns 204. # Delete a Provider Configuration Source: https://docs.lerian.studio/en/reference/flowker/delete-provider-configuration en/openapi/v3-current/flowker.yaml delete /v1/provider-configurations/{id} Use this endpoint to delete a provider configuration. # Delete an XSD Schema Source: https://docs.lerian.studio/en/reference/flowker/delete-xsd-schema en/openapi/v3-current/flowker.yaml delete /v1/xsd-schemas/{id} Use this endpoint to delete an XSD schema. An active provider configuration or an active workflow that references the schema blocks the delete with 409. References from inactive workflows or disabled provider configurations do not block: the delete proceeds and the response carries the warnings. A schema with no references at all returns 204. # Derive an Operation Schema Source: https://docs.lerian.studio/en/reference/flowker/derive-openapi-operation-schema en/openapi/v3-current/flowker.yaml get /v1/openapi-schemas/{id}/operation-schema Use this endpoint to derive the input and output contract of a single operation in an external OpenAPI schema. The response carries the dereferenced request-body and 2xx response schemas, the input parameters merged by location, and the body descriptor. Identify the operation with the `path` query parameter, which is the URL-encoded OpenAPI path template, and the `method` query parameter, which is case-insensitive. # Disable a Provider Configuration Source: https://docs.lerian.studio/en/reference/flowker/disable-provider-configuration en/openapi/v3-current/flowker.yaml post /v1/provider-configurations/{id}/disable Use this endpoint to disable a provider configuration. Transitions the status from `active` to `disabled`. # Discard All Parked Occurrences Source: https://docs.lerian.studio/en/reference/flowker/discard-all-parked-occurrences en/openapi/v3-current/flowker.yaml post /v1/workflows/{id}/schedule/missed/discard-all Use this endpoint to discard every parked occurrence of a workflow that is in the pending-review state. The operation is idempotent: when no occurrence is pending review, the response reports zero discarded. # Discard a Parked Occurrence Source: https://docs.lerian.studio/en/reference/flowker/discard-parked-occurrence en/openapi/v3-current/flowker.yaml post /v1/workflows/{id}/schedule/missed/{occurrenceId}/discard Use this endpoint to move a parked occurrence to the terminal discarded state. Only an occurrence in the pending-review state can be discarded; any other state returns 422. # Enable a Provider Configuration Source: https://docs.lerian.studio/en/reference/flowker/enable-provider-configuration en/openapi/v3-current/flowker.yaml post /v1/provider-configurations/{id}/enable Use this endpoint to enable a provider configuration. Transitions the status from `disabled` to `active`. # Execute a Workflow Source: https://docs.lerian.studio/en/reference/flowker/execute-workflow en/openapi/v3-current/flowker.yaml post /v1/workflows/{workflowId}/executions Use this endpoint to start a new workflow execution. The request returns immediately with a `pending` status. Use the Idempotency-Key header to ensure safe retries. # Flowker error list Source: https://docs.lerian.studio/en/reference/flowker/flowker-error-list Flowker APIs return RFC 9457 problem documents with a stable error code, so you can diagnose issues and route them to the right team. ### Error format The `/v1` management API returns errors as an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem document, served with the `application/problem+json` content type. Webhook routes answer in the format their trigger is configured for — see [Errors on webhook routes](#errors-on-webhook-routes). ```json theme={null} { "type": "https://errors.lerian.studio/v1/FLK-0110", "title": "Bad Request", "status": 400, "detail": "name is a required field", "instance": "/v1/workflows", "code": "FLK-0110" } ``` ### Field definitions * **`code`** – The stable Flowker error code. Branch your integration on this field. * **`type`** – A URI that identifies the error. It is always `https://errors.lerian.studio/v1/` followed by the `code`. * **`title`** – The standard HTTP reason phrase for `status`, such as `Not Found` or `Conflict`. It does not change per error code. * **`status`** – The HTTP status code, repeated in the body. * **`detail`** – A human-readable explanation of this occurrence. Responses with a status of 500 or above carry a fixed generic message instead, so use `code` to tell them apart. * **`instance`** – A URI that identifies this specific occurrence, when the endpoint provides one. * **`errors`** – An optional array of per-field entries. See [Field-level error details](#field-level-error-details). ### Field-level error details When a request fails validation on specific fields, the problem document carries an `errors` array. Each entry names the offending input. ```json Example 1 theme={null} { "type": "https://errors.lerian.studio/v1/FLK-0301", "title": "Unprocessable Entity", "status": 422, "detail": "validation failed", "code": "FLK-0301", "errors": [ { "location": "body.name", "message": "expected required property name to be present" } ] } ``` ```json Example 2 theme={null} { "type": "https://errors.lerian.studio/v1/FLK-0001", "title": "Bad Request", "status": 400, "detail": "request body is invalid or malformed", "code": "FLK-0001", "errors": [ { "location": "body.nodes", "message": "expected array length >= 1", "value": [] } ] } ``` Each entry carries a `location` (where the problem is, such as `body.nodes` or `path.id`), a `message`, and the offending `value` when it is safe to echo. ### Errors on webhook routes A webhook route answers in the format its trigger is configured for. A JSON webhook route returns a compact error object with the `application/json` content type: ```json theme={null} { "code": "FLK-0361", "title": "Not Found", "message": "no webhook registered for this path and method" } ``` * **`code`** – The stable Flowker error code, taken from the tables below. Branch your integration on this field. * **`title`** – The HTTP reason phrase for the response status, such as `Not Found` or `Payload Too Large`. * **`message`** – A human-readable explanation of this occurrence. An XML webhook route returns an `` document instead, because the whole route works in XML. ```xml theme={null} XML_VALIDATION_FAILED element 'valor' is not a valid decimal
line 12, column 5
018f3e2a-1c4d-7b9e-a1b2-c3d4e5f6a7b8
```
The `code` element carries either a Flowker code from the tables below or one of two XML-specific codes: | Code | What it means | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `XML_MALFORMED` | The request body is not well-formed XML. Returned with HTTP 400. | | `XML_VALIDATION_FAILED` | The body is well-formed XML but does not conform to the route's XSD. Returned with HTTP 422, and `details` carries the line and column of the first finding. | ## General errors *** These errors apply to all Flowker API endpoints. | Code | What it means | | -------- | ----------------------------------------------------------------------------------------------- | | FLK-0001 | The request body is invalid or malformed. Verify the JSON format and try again. | | FLK-0002 | The provided ID is not a valid UUID. Verify the ID and try again. | | FLK-0003 | Authentication is required to access this resource. Send a valid bearer token. | | FLK-0004 | You do not have permission to perform this action. Check the permissions granted to your token. | | FLK-9999 | An unexpected error occurred. Try again later, or contact support if the issue persists. | ## Request validation errors *** These errors are returned when the API request does not meet validation requirements. | Code | What it means | | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | FLK-0300 | The request contains unexpected fields. Remove the extra fields and try again. | | FLK-0301 | One or more required fields are missing. Provide all required fields and try again. | | FLK-0302 | The request is invalid. Verify the request format and try again. | | FLK-0303 | A field used in a calculation has an invalid type. Verify the field types and try again. | | FLK-0304 | One or more query parameters are invalid. Verify the parameters and try again. | | FLK-0305 | The date must be in RFC 3339 format with a timezone, such as `2026-01-28T10:30:00Z`. Date-only values are rejected. | | FLK-0306 | The end date must be after the start date. Provide a valid date range. | | FLK-0307 | The requested date range exceeds the maximum allowed period. Reduce the date range. | | FLK-0308 | The date range is invalid. Verify the start and end dates. | | FLK-0309 | The `limit` parameter exceeds the maximum allowed value. Reduce the limit and try again. | | FLK-0310 | Sort order must be `ASC` or `DESC`. Provide a valid value. | | FLK-0311 | The provided path parameter is invalid. Verify the parameter value and try again. | | FLK-0312 | The metadata key exceeds the maximum allowed length of 64 characters. Reduce the key size. | | FLK-0313 | The metadata value exceeds the maximum allowed length. Reduce the value size. | | FLK-0314 | Metadata values cannot be nested objects. Provide flat key-value pairs. | | FLK-0315 | The metadata exceeds the maximum number of entries. Reduce the number of metadata entries. | | FLK-0321 | The pagination cursor is malformed or could not be decoded. Request the first page again and follow the cursor returned by the API. | ## Entity errors *** | Code | What it means | | -------- | ------------------------------------------------------------------------------------------- | | FLK-0400 | The requested entity does not exist. Verify the ID and try again. | | FLK-0401 | This action is not permitted in the current state. Check the resource status and try again. | | FLK-0402 | The referenced parent entity does not exist. Verify the parent ID and try again. | ## Workflow errors *** | Code | What it means | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FLK-0100 | The requested workflow does not exist. Verify the workflow ID and try again. | | FLK-0101 | A workflow with this name already exists. Choose a different name and try again. | | FLK-0102 | The requested status transition is not allowed. Check the current workflow status and the valid transitions. | | FLK-0103 | Non-draft workflows cannot be modified. Create a new version, or revert the workflow to draft. | | FLK-0104 | A node references a catalog executor that does not exist. Verify the `executorId` on the node. | | FLK-0105 | The conditional expression is invalid or could not be evaluated. Verify the expression syntax. | | FLK-0110 | The `name` field is required. Provide a name for the workflow and try again. | | FLK-0111 | The workflow name exceeds the maximum allowed length. Reduce the name size. | | FLK-0112 | A workflow must have at least one node. Add nodes to the workflow and try again. | | FLK-0113 | The workflow exceeds the maximum number of nodes allowed. Reduce the node count. | | FLK-0114 | The workflow exceeds the maximum number of edges allowed. Reduce the edge count. | | FLK-0115 | An edge references a node that does not exist in the workflow. Verify all edge source and target values. | | FLK-0116 | The workflow must have at least one trigger node. Add a trigger and try again. | | FLK-0117 | The schedule trigger carries an invalid cron expression. Use a standard 5-field cron. Sub-minute schedules are not supported. | | FLK-0118 | The workflow has no schedule trigger, so it has no upcoming occurrences to list or manage. | | FLK-0120 | Each node must have an ID. Provide an ID for all nodes. | | FLK-0121 | Each node must have a type. Provide a type for all nodes. | | FLK-0122 | An action node's `set_output` `responseStatusCode` override must be an integer between 200 and 599. Correct the value and try again. | | FLK-0130 | Each edge must have an ID. Provide an ID for all edges. | | FLK-0131 | Each edge must specify a source node. Provide the source node ID. | | FLK-0132 | Each edge must specify a target node. Provide the target node ID. | | FLK-0140 | The input mapping configuration is invalid. Verify the mapping syntax and field references. | | FLK-0141 | The output mapping configuration is invalid. Verify the mapping syntax and field references. | | FLK-0142 | The transformation configuration is invalid. Verify the transform expressions. | | FLK-0143 | A node's `data.body` template could not be resolved against the workflow context, because a `${...}` reference is malformed or does not resolve. The node fails without calling the external service. | | FLK-0150 | The node's `providerConfigId` is missing, is not a valid UUID, or names a provider configuration that does not exist or is not active. Verify the `providerConfigId` on every executor node, and the existence and status of the configuration it names. | | FLK-0151 | A node pairs a provider configuration of one provider with an executor of another. Verify that the `providerId` on the configuration and the `executorId` on the node name the same catalog provider. | ## Workflow condition errors *** These errors are returned when a node's structured `condition` object fails validation as the workflow is created or updated. | Code | What it means | | -------- | --------------------------------------------------------------------------------------------------------------------- | | FLK-0160 | The condition's `version` field is missing or is not `2`. | | FLK-0161 | The condition object is structurally malformed, such as a `cases` value that is not an array. | | FLK-0162 | The condition has no cases. Add at least one case. | | FLK-0163 | Two cases, or a case and the default branch, share the same output handle ID. Give each branch a unique handle. | | FLK-0164 | A case has an empty output handle ID. Provide a handle for every case. | | FLK-0165 | The condition `mode` must be `rules` or `value`. | | FLK-0166 | A `value` mode condition needs a subject operand. Provide the subject. | | FLK-0167 | A group's `combinator` must be `and` or `or`. | | FLK-0168 | A group carries neither rules nor nested groups. Add at least one rule, or remove the group. | | FLK-0169 | The rule operator is not recognized. Use an operator from the supported taxonomy. | | FLK-0170 | The rule `valueType` is not recognized. Use a value type from the supported taxonomy. | | FLK-0171 | The operator is not allowed for the selected `valueType`. Change the operator or the value type. | | FLK-0172 | An operand has an empty `source` path. Provide the path to read. | | FLK-0173 | A transform has an invalid operation, or invalid parameters for its operation. | | FLK-0174 | A right operand's `kind` must be `literal` or `source`, and a `source` right operand needs a source path. | | FLK-0175 | The default branch has an empty output handle ID. Provide a handle. | | FLK-0176 | The condition exceeds the maximum number of cases. Reduce the number of cases. | | FLK-0177 | A group exceeds the maximum number of rules. Split the rules across nested groups. | | FLK-0178 | An operand's transform pipeline exceeds the maximum number of transforms. Reduce the pipeline. | | FLK-0179 | A `matchesRegex` or `notMatchesRegex` rule carries a regular expression that exceeds the maximum length. | | FLK-0180 | An `isIn` or `notIn` rule carries a list that exceeds the maximum number of items. | | FLK-0181 | The condition is structurally valid but could not be compiled. Review the operands and literals on the reported node. | | FLK-0182 | The condition tree is too large across all its cases. Reduce the number of groups and rules. | ## Catalog, executor, and trigger errors *** | Code | What it means | | -------- | -------------------------------------------------------------------------------------------------------- | | FLK-0200 | The requested executor does not exist in the catalog. Verify the executor ID and try again. | | FLK-0201 | A node's configuration does not match the catalog executor's JSON Schema. Verify it against that schema. | | FLK-0210 | The requested trigger does not exist in the catalog. Verify the trigger ID and try again. | | FLK-0211 | The trigger configuration is invalid. Verify the configuration format. | | FLK-0220 | The requested runner does not exist. Verify the runner ID and try again. | | FLK-0230 | The requested provider does not exist in the catalog. Verify the provider ID and try again. | | FLK-0231 | A provider with this identifier already exists. Use the existing provider, or choose a different ID. | | FLK-0232 | The provider configuration is invalid. Verify the configuration format. | | FLK-0240 | The requested template does not exist in the catalog. Verify the template ID and try again. | | FLK-0242 | One or more template parameters are invalid. Verify the parameters against the template schema. | ## Executor configuration errors *** | Code | What it means | | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | FLK-0250 | The requested executor configuration does not exist. Verify the ID and try again. | | FLK-0251 | An executor configuration with this name already exists. Choose a different name and try again. | | FLK-0252 | The executor configuration cannot be modified in its current status. Check the status and try again. | | FLK-0260 | The `name` field is required. Provide a name for the executor configuration. | | FLK-0261 | The executor configuration name exceeds the maximum allowed length. Reduce the name size. | | FLK-0262 | The `baseUrl` field is required. Provide a base URL for the executor configuration. | | FLK-0263 | The provided base URL is not a valid URL. Verify the URL format. | | FLK-0264 | The base URL exceeds the maximum allowed length. Reduce the URL size. | | FLK-0265 | At least one endpoint must be configured. Provide endpoint definitions. | | FLK-0266 | Authentication configuration is required. Provide auth settings. | | FLK-0267 | The description exceeds the maximum allowed length. Reduce the description size. | | FLK-0268 | The auth type field is required. Provide an authentication type. | | FLK-0269 | The auth type is not recognized. Provide a valid authentication type. | | FLK-0270 | Each endpoint must have a name. Provide names for all endpoints. | | FLK-0271 | Each endpoint must have a path. Provide paths for all endpoints. | | FLK-0272 | Each endpoint must specify an HTTP method. Provide methods for all endpoints. | | FLK-0273 | An endpoint's HTTP method is not recognized. Verify the method on every endpoint and try again. | | FLK-0284 | The base URL resolves to an address Flowker refuses to call, such as a loopback or link-local host. Use a routable external host. | | FLK-0285 | The base URL host is not permitted by the configuration's allowed-hosts list. Add the host to the list, or change the URL. | ## Provider configuration errors *** | Code | What it means | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FLK-0290 | The requested provider configuration does not exist. Verify the ID and try again. | | FLK-0291 | A provider configuration with this name already exists. Choose a different name and try again. | | FLK-0292 | The provider configuration cannot be modified in its current status. Check the status and try again. | | FLK-0293 | The `config` object does not match the provider's required schema, or a `schemaBindings` entry is malformed — an unknown `type`, a `schemaId` that is empty, is not a UUID, or is all zeros, `operations` on a binding that is not `openapi`, or an operation whose path or HTTP method is missing or invalid. Verify the configuration fields and each schema binding. | | FLK-0294 | The referenced provider does not exist in the catalog. Verify the provider ID. | | FLK-0295 | The `name` field is required. Provide a name for the provider configuration. | | FLK-0296 | The provider configuration name exceeds the maximum allowed length. Reduce the name size. | | FLK-0297 | The `providerId` field is required for a `catalog` provider configuration. Provide a provider ID. | | FLK-0298 | The `config` field is required. Provide the provider configuration object. | | FLK-0299 | The description exceeds the maximum allowed length. Reduce the description size. | | FLK-0316 | The provider configuration ID is required for this operation. Provide the ID. | | FLK-0317 | An entry in the allowed-hosts list is not a valid host. Verify each entry. | | FLK-0320 | A base URL in the configuration points at a host its own allowed-hosts list rejects. Align the URL and the list. | | FLK-0323 | A configuration for the generic HTTP provider must declare an allowed-hosts list. An empty list would allow calls to any address, so it is rejected. | | FLK-0952 | The update changed the auth type without supplying a credential the new type requires and the previous one did not. Send that value with the type change. | ## External OpenAPI provider configuration errors *** These errors apply to provider configurations of kind `external_openapi`, which call an operation declared by a stored OpenAPI schema. | Code | What it means | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | FLK-0946 | The configuration omits the required `config.openapi_schema_id` reference. Provide the schema ID. | | FLK-0947 | The referenced OpenAPI schema does not exist in your tenant. Verify the schema ID. | | FLK-0948 | The `config.auth` block is malformed — an unknown type, a missing required field, or the wrong shape. | | FLK-0949 | Neither `config.base_url` nor a usable `servers` entry in the referenced schema resolves a base URL. The node fails without calling the service. | | FLK-0950 | The bound operation is absent from the referenced schema, or the stored schema no longer parses. The node fails without calling the service. | | FLK-0951 | The HMAC request-signing scheme could not resolve its secret. The request is never sent unsigned. | ## Provider schema binding errors *** | Code | What it means | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FLK-0942 | A schema binding references an XSD or OpenAPI schema ID that does not exist in your tenant. Verify the schema ID. | | FLK-0943 | An OpenAPI schema binding restricts to an operation that the referenced document does not declare. Verify the path and method. | | FLK-0944 | The configuration lists the same binding twice. Remove the duplicate. | | FLK-0945 | The schema cannot be deleted because at least one active provider configuration binds it. The response lists the referring provider configurations and workflows. | ## Workflow execution errors *** | Code | What it means | | -------- | ------------------------------------------------------------------------------------------------------------------------- | | FLK-0500 | The requested execution does not exist. Verify the execution ID and try again. | | FLK-0501 | The workflow is not in an active state and cannot be executed. Activate the workflow and try again. | | FLK-0502 | A previous execution for this workflow is still in progress. Wait for it to complete. | | FLK-0503 | The workflow execution timed out before completing. Check the workflow logic and the response times of external services. | | FLK-0504 | A node within the workflow failed during execution. Check the execution details for the specific node error. | | FLK-0505 | An execution with this idempotency key already exists. Use a new key for distinct executions. | | FLK-0506 | The request body must not exceed 1 MB. Reduce the input payload size. | | FLK-0507 | The circuit breaker for this provider configuration is open after repeated failures. Wait before retrying. | | FLK-0508 | A cycle was detected in the workflow graph. Review the edges and remove the circular references. | | FLK-0509 | The `Idempotency-Key` header is required for execution requests. Include a unique key in the header. | ## Outbound request errors *** These errors are raised while a node calls an external service. They surface as node failures in the execution details. | Code | What it means | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | FLK-0322 | The provider's response body exceeds the configured size cap. Retrying does not help — the response will not shrink. | | FLK-0324 | The request body Flowker would send exceeds the configured size cap. The request is rejected before any network call. | | FLK-0937 | An HTTP node declares `request_format` of type `xml_converted` without a valid `root_element`. Provide a syntactically valid XML element name. | | FLK-0938 | An `xml_passthrough` node has no XML trigger payload to forward. Passthrough forwards the trigger's original bytes, so it needs an XML trigger. | | FLK-0939 | An `xml_passthrough` node's retained trigger payload is not eligible for byte-exact forwarding, such as a payload that is not valid UTF-8. | | FLK-0940 | An `xml_converted` node's mapped body is not a structured object. Converted XML needs an object to serialize under `root_element`. | | FLK-0941 | The mapped body could not be encoded as XML. Review the structure the mappings produce. | ## Concurrency errors *** | Code | What it means | | -------- | ----------------------------------------------------------------------- | | FLK-0350 | The resource state changed concurrently. Retry with the latest version. | ## Webhook errors *** | Code | What it means | | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | FLK-0360 | The webhook path is already registered by another active workflow. Choose a different path, or deactivate the conflicting workflow. | | FLK-0361 | No webhook is registered for this path and method. Verify the webhook path and method, and try again. | | FLK-0363 | The webhook request body must not exceed 1 MB. Reduce the payload size. | | FLK-0364 | The webhook request body is not well-formed XML. XML routes return this as `XML_MALFORMED` in the `` envelope. | ## Webhook trigger contract errors *** These errors are returned when a webhook trigger's `input_contract` is saved, when the workflow is activated, or when an incoming payload is validated against it. | Code | What it means | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | FLK-0930 | The trigger's `input_contract` is `xsd`, but its `xsd_schema_id` does not resolve to a stored XSD schema. | | FLK-0931 | The trigger's `input_contract` is `openapi`, but its `openapi_schema_id` does not resolve to a stored OpenAPI schema. | | FLK-0932 | The referenced OpenAPI schema declares no operation matching the trigger's `operation_path` and `operation_method`. | | FLK-0933 | The matched OpenAPI operation declares no JSON request body, so it cannot validate an incoming payload. | | FLK-0934 | The webhook trigger's configuration does not match the trigger schema. The schema is closed, so the trigger fails when it omits `path`, `method` or `input_contract`, misses a field its `input_contract` mode requires, names another mode's schema id or operation field, or carries a key or a value the schema does not accept. Check this error first when you configure a webhook trigger. | | FLK-0935 | The incoming payload is well-formed JSON but does not conform to the operation's request-body schema. | | FLK-0936 | The OpenAPI schema cannot be deleted because one or more active workflows reference it from a webhook trigger. References from draft or inactive workflows do not block the delete. | ## XSD schema errors *** | Code | What it means | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | FLK-0700 | The requested XSD schema does not exist. Verify the schema ID and try again. | | FLK-0701 | An XSD schema with this name and version already exists. Choose a different name or version. | | FLK-0702 | The uploaded content failed XSD validation. Verify the schema document. | | FLK-0703 | The XSD schema file exceeds the 1 MiB limit. Reduce the file size. | | FLK-0704 | The XSD schema cannot be deleted because one or more workflow nodes reference it. | | FLK-0710 | The `name` field is required. Provide a name for the XSD schema. | | FLK-0711 | The `version` field is required. Provide a version for the XSD schema. | | FLK-0712 | The schema content is required. Upload the XSD document. | | FLK-0720 | The XML validation service is unreachable or returned a fault, so the payload could not be validated. Flowker rejects the request rather than accepting unvalidated XML. | ## External OpenAPI schema errors *** These errors apply to the OpenAPI schemas you store per tenant and reference from triggers, provider configurations, and nodes. | Code | What it means | | -------- | ----------------------------------------------------------------------------------------------------------- | | FLK-0807 | The `name` field is required. Provide a name for the OpenAPI schema. | | FLK-0808 | The `version` field is required. Provide a version for the OpenAPI schema. | | FLK-0809 | The OpenAPI schema name exceeds the maximum allowed length. Reduce the name size. | | FLK-0810 | The OpenAPI schema version exceeds the maximum allowed length. Reduce the version size. | | FLK-0811 | The requested OpenAPI schema does not exist. Verify the schema ID and try again. | | FLK-0812 | An OpenAPI schema with this name and version already exists. Choose a different name or version. | | FLK-0813 | The schema content is required. Upload the OpenAPI document. | | FLK-0900 | The uploaded bytes do not parse as a valid OpenAPI 3.x document, or the document declares no operation. | | FLK-0901 | The uploaded OpenAPI schema exceeds the 8 MiB limit. Reduce the file size. | | FLK-0902 | The stored schema declares no operation matching the requested path and method. Verify the path and method. | ## OpenAPI spec registry errors *** These errors apply to the shared OpenAPI spec registry, which pins the spec version Flowker uses to enrich a service's output schemas. | Code | What it means | | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | FLK-0801 | The version-pin request is missing the service or the version. Provide both. | | FLK-0802 | The uploaded bytes do not parse as a valid OpenAPI 3.x document. Verify the spec. | | FLK-0803 | A spec is already stored for this service and version. Versions are immutable — upload a new version. | | FLK-0804 | The schema registry has no storage configured, so the upload cannot be accepted. | | FLK-0805 | The uploaded spec exceeds the registry size cap. Reduce the file size. | | FLK-0806 | The service or version path identifier is malformed. Remove empty values, slashes, `..`, control characters, and leading dots. | ## Scheduled occurrence errors *** | Code | What it means | | -------- | ------------------------------------------------------------------------------------------ | | FLK-0755 | The occurrence is not in a state that can be run or discarded. Check its current state. | | FLK-0760 | The requested scheduled occurrence does not exist. Verify the occurrence ID and try again. | # Get Dashboard Execution Summary Source: https://docs.lerian.studio/en/reference/flowker/get-dashboard-execution-summary en/openapi/v3-current/flowker.yaml get /v1/dashboards/executions Use this endpoint to retrieve an aggregated summary of workflow executions. You can optionally filter by time range and status. # Get Dashboard Workflow Summary Source: https://docs.lerian.studio/en/reference/flowker/get-dashboard-workflow-summary en/openapi/v3-current/flowker.yaml get /v1/dashboards/workflows/summary Use this endpoint to retrieve an aggregated summary of workflows, including totals and breakdowns by status. # Get Execution Results Source: https://docs.lerian.studio/en/reference/flowker/get-execution-results en/openapi/v3-current/flowker.yaml get /v1/executions/{id}/results Use this endpoint to retrieve the results of a completed workflow execution. The execution must be in a terminal state. # Get an Execution Status Source: https://docs.lerian.studio/en/reference/flowker/get-execution-status en/openapi/v3-current/flowker.yaml get /v1/executions/{id} Use this endpoint to retrieve the current status of a specific workflow execution. # Get an Executor Configuration Source: https://docs.lerian.studio/en/reference/flowker/get-executor-configuration en/openapi/v3-current/flowker.yaml get /v1/executors/{id} Use this endpoint to retrieve a specific executor configuration by its unique identifier. # Get an External OpenAPI Schema Source: https://docs.lerian.studio/en/reference/flowker/get-openapi-schema en/openapi/v3-current/flowker.yaml get /v1/openapi-schemas/{id} Use this endpoint to retrieve an external OpenAPI schema by its ID, including the raw specification content and a summary of each operation it declares. # Get a Provider Configuration Source: https://docs.lerian.studio/en/reference/flowker/get-provider-configuration en/openapi/v3-current/flowker.yaml get /v1/provider-configurations/{id} Use this endpoint to retrieve a specific provider configuration by its unique identifier. # Get an XSD Schema Source: https://docs.lerian.studio/en/reference/flowker/get-xsd-schema en/openapi/v3-current/flowker.yaml get /v1/xsd-schemas/{id} Use this endpoint to retrieve an XSD schema by its ID, including its full content. # List Executions Source: https://docs.lerian.studio/en/reference/flowker/list-executions en/openapi/v3-current/flowker.yaml get /v1/executions Use this endpoint to retrieve a paginated list of workflow executions. You can filter results by workflow ID or status. # List Executor Configurations Source: https://docs.lerian.studio/en/reference/flowker/list-executor-configurations en/openapi/v3-current/flowker.yaml get /v1/executors Use this endpoint to retrieve a paginated list of executor configurations. You can filter results by status. # List External OpenAPI Schema References Source: https://docs.lerian.studio/en/reference/flowker/list-openapi-schema-references en/openapi/v3-current/flowker.yaml get /v1/openapi-schemas/{id}/references Use this endpoint to list the provider configurations and workflows that reference an external OpenAPI schema. Both groups are always present in the response, and every entry is status-tagged: an active reference blocks a delete, a disabled one only warns. # List External OpenAPI Schemas Source: https://docs.lerian.studio/en/reference/flowker/list-openapi-schemas en/openapi/v3-current/flowker.yaml get /v1/openapi-schemas Use this endpoint to retrieve a paginated list of the tenant's external OpenAPI schemas. The response carries metadata only, without the specification content. # List OpenAPI Spec Versions Source: https://docs.lerian.studio/en/reference/flowker/list-openapi-spec-versions en/openapi/v3-current/flowker.yaml get /v1/catalog/openapi-versions/{service} Use this endpoint to list the published OpenAPI specification versions available for a native service. The response marks the version the current tenant has pinned. # List Provider Configurations Source: https://docs.lerian.studio/en/reference/flowker/list-provider-configurations en/openapi/v3-current/flowker.yaml get /v1/provider-configurations Use this endpoint to retrieve a paginated list of provider configurations. You can filter results by status or provider ID. # List XSD Schema References Source: https://docs.lerian.studio/en/reference/flowker/list-xsd-schema-references en/openapi/v3-current/flowker.yaml get /v1/xsd-schemas/{id}/references Use this endpoint to list the provider configurations and workflows that reference an XSD schema. Both groups are always present in the response, and every entry is status-tagged: an active reference blocks a delete, a disabled one only warns. # List XSD Schemas Source: https://docs.lerian.studio/en/reference/flowker/list-xsd-schemas en/openapi/v3-current/flowker.yaml get /v1/xsd-schemas Use this endpoint to retrieve a paginated list of the tenant's XSD schemas. The response carries metadata only, without the schema content. # Pin an OpenAPI Spec Version Source: https://docs.lerian.studio/en/reference/flowker/pin-openapi-spec-version en/openapi/v3-current/flowker.yaml put /v1/catalog/openapi-version-pins/{service} Use this endpoint to select which published OpenAPI specification version the current tenant resolves against for a native service. The operation is idempotent: pinning again updates the version in place. # Preview an Executor Request Source: https://docs.lerian.studio/en/reference/flowker/preview-executor-request en/openapi/v3-current/flowker.yaml post /v1/workflows/preview-request Use this endpoint to assemble the outgoing HTTP request an executor node would send, without sending it. The preview applies the same interpolation, input mapping, and authentication assembly used at execution time. It never opens a network connection and never reads the vault. Secret material, including write-only configuration fields and authentication material, is masked with `***`. # Run a Parked Occurrence Source: https://docs.lerian.studio/en/reference/flowker/run-parked-occurrence en/openapi/v3-current/flowker.yaml post /v1/workflows/{id}/schedule/missed/{occurrenceId}/run Use this endpoint to re-run a parked occurrence. The occurrence moves to the queued state and is enqueued once. Only an occurrence in the pending-review state can be run; any other state returns 422. The operation is idempotent. # Trigger a Webhook Source: https://docs.lerian.studio/en/reference/flowker/trigger-webhook en/openapi/v3-current/flowker.yaml post /v1/webhooks/{path} Receives a webhook callback and triggers the associated workflow execution. The path is dynamically resolved against the webhook registry — each workflow can register its own webhook path. At runtime, GET, PUT, DELETE, and PATCH methods are also accepted, as well as nested paths with multiple segments. # Trigger a Webhook (DELETE) Source: https://docs.lerian.studio/en/reference/flowker/trigger-webhook-delete en/openapi/v3-current/flowker.yaml delete /v1/webhooks/{path} Receives a webhook callback via HTTP DELETE and triggers the associated workflow execution. The path is dynamically resolved against the webhook registry — each workflow can register its own webhook path. The runtime accepts GET, POST, PUT, PATCH, and DELETE on this route, as well as nested paths with multiple segments. # Trigger a Webhook (GET) Source: https://docs.lerian.studio/en/reference/flowker/trigger-webhook-get en/openapi/v3-current/flowker.yaml get /v1/webhooks/{path} Receives a webhook callback via HTTP GET and triggers the associated workflow execution. The path is dynamically resolved against the webhook registry — each workflow can register its own webhook path. The runtime accepts GET, POST, PUT, PATCH, and DELETE on this route, as well as nested paths with multiple segments. # Trigger a Webhook (PATCH) Source: https://docs.lerian.studio/en/reference/flowker/trigger-webhook-patch en/openapi/v3-current/flowker.yaml patch /v1/webhooks/{path} Receives a webhook callback via HTTP PATCH and triggers the associated workflow execution. The path is dynamically resolved against the webhook registry — each workflow can register its own webhook path. The runtime accepts GET, POST, PUT, PATCH, and DELETE on this route, as well as nested paths with multiple segments. # Trigger a Webhook (PUT) Source: https://docs.lerian.studio/en/reference/flowker/trigger-webhook-put en/openapi/v3-current/flowker.yaml put /v1/webhooks/{path} Receives a webhook callback via HTTP PUT and triggers the associated workflow execution. The path is dynamically resolved against the webhook registry — each workflow can register its own webhook path. The runtime accepts GET, POST, PUT, PATCH, and DELETE on this route, as well as nested paths with multiple segments. # Update an Executor Configuration Source: https://docs.lerian.studio/en/reference/flowker/update-executor-configuration en/openapi/v3-current/flowker.yaml patch /v1/executors/{id} Use this endpoint to update an existing executor configuration. Only configurations with `unconfigured` or `configured` status can be updated. # Update a Provider Configuration Source: https://docs.lerian.studio/en/reference/flowker/update-provider-configuration en/openapi/v3-current/flowker.yaml patch /v1/provider-configurations/{id} Use this endpoint to update an existing provider configuration. If the configuration payload changes, it is re-validated against the provider's JSON Schema. # Upload an External OpenAPI Schema Source: https://docs.lerian.studio/en/reference/flowker/upload-openapi-schema en/openapi/v3-current/flowker.yaml post /v1/openapi-schemas Use this endpoint to upload an external OpenAPI 3.x schema as multipart/form-data. The file must not exceed 8 MiB. # Upload an OpenAPI Spec Version Source: https://docs.lerian.studio/en/reference/flowker/upload-openapi-spec-version en/openapi/v3-current/flowker.yaml post /v1/catalog/openapi-specs/{service}/{version} Use this endpoint to publish a global OpenAPI 3.x specification version for a native service. Every tenant resolves against the published versions. Versions are immutable: uploading a version that already exists returns 409. Send the specification as multipart/form-data; the file must not exceed 8 MiB. The caller needs the create permission on the catalog resource. # Upload an XSD Schema Source: https://docs.lerian.studio/en/reference/flowker/upload-xsd-schema en/openapi/v3-current/flowker.yaml post /v1/xsd-schemas Use this endpoint to upload an XSD schema file as multipart/form-data. The file must not exceed 1 MiB. # Activate loan product Source: https://docs.lerian.studio/en/reference/lender/activate-loan-product en/openapi/v3-current/lender.yaml post /api/v1/loan-products/{id}/activate Transitions a loan product to active status and pins the active product version. # Activate loan product (BR alias) Source: https://docs.lerian.studio/en/reference/lender/activate-loan-product-br en/openapi/v3-current/lender.yaml post /api/v1/br/loan-products/{id}/activate BR-aliased path for activating a loan product. Resolves to the same handler as the core endpoint. # Apply loan account PDD stage transition Source: https://docs.lerian.studio/en/reference/lender/apply-loan-account-pdd-stage-transition en/openapi/v3-current/lender.yaml post /api/v1/br/loan-accounts/{id}/pdd-stage/transitions Records a PDD transition or cure and updates prospective accrual eligibility. # Approve loan application Source: https://docs.lerian.studio/en/reference/lender/approve-loan-application en/openapi/v3-current/lender.yaml post /api/v1/loan-applications/{id}/approve Transitions a pending-approval loan application to approved. # Create accrual run Source: https://docs.lerian.studio/en/reference/lender/create-accrual-run en/openapi/v3-current/lender.yaml post /api/v1/accrual-runs Queues an accrual batch for a business date and returns available journal/outbox reference data. # Create loan application Source: https://docs.lerian.studio/en/reference/lender/create-loan-application en/openapi/v3-current/lender.yaml post /api/v1/loan-applications Creates a pending approval loan application from an already-previewed schedule snapshot. # Create loan product Source: https://docs.lerian.studio/en/reference/lender/create-loan-product en/openapi/v3-current/lender.yaml post /api/v1/loan-products Creates a new loan product definition. # Create loan product accounting profile Source: https://docs.lerian.studio/en/reference/lender/create-loan-product-accounting-profile en/openapi/v3-current/lender.yaml post /api/v1/loan-products/{id}/accounting-profiles Configures the accounting profile for a loan product version that belongs to the path loan product. # Create loan product (BR alias) Source: https://docs.lerian.studio/en/reference/lender/create-loan-product-br en/openapi/v3-current/lender.yaml post /api/v1/br/loan-products BR-aliased path for creating a loan product. Resolves to the same handler as the core endpoint. # Apply inherited loan product charge Source: https://docs.lerian.studio/en/reference/lender/create-loan-product-charges en/openapi/v3-current/lender.yaml post /api/v1/loan-products/{id}/charges Applies one product-version charge template to a loan account and persists an immutable charge fact. # Create loan product version Source: https://docs.lerian.studio/en/reference/lender/create-loan-product-version en/openapi/v3-current/lender.yaml post /api/v1/loan-products/{id}/versions Creates a new version snapshot of a loan product with updated regulatory parameters. # Create loan product version (BR alias) Source: https://docs.lerian.studio/en/reference/lender/create-loan-product-version-br en/openapi/v3-current/lender.yaml post /api/v1/br/loan-products/{id}/versions BR-aliased path for creating a loan product version. Resolves to the same handler as the core endpoint. # Create prepayment quote Source: https://docs.lerian.studio/en/reference/lender/create-prepayment-quote en/openapi/v3-current/lender.yaml post /api/v1/br/loan-accounts/{id}/prepayment-quote Creates a time-limited, auditable prepayment quote with settlement and rebate breakdown. # Disburse loan application Source: https://docs.lerian.studio/en/reference/lender/disburse-loan-application en/openapi/v3-current/lender.yaml post /api/v1/loan-applications/{id}/disburse Validates disbursement eligibility, dispatches the resolving jurisdictional disbursement pipeline inside the parent transaction, and moves an approved application to disbursed. Idempotent: the lib-commons middleware enforces replay detection via X-Idempotency. # Get active loan account Source: https://docs.lerian.studio/en/reference/lender/get-active-loan-account en/openapi/v3-current/lender.yaml get /api/v1/loan-accounts/{id} Returns the active loan account detail read model. A malformed UUID yields 422 (schema error), distinct from 404 when the account does not exist. # Get active loan schedule Source: https://docs.lerian.studio/en/reference/lender/get-active-loan-schedule en/openapi/v3-current/lender.yaml get /api/v1/loan-accounts/{id}/schedule Returns ordered active loan installment schedule entries. A malformed UUID yields 422, distinct from 404. # Get journal reference by correlation id Source: https://docs.lerian.studio/en/reference/lender/get-journal-reference en/openapi/v3-current/lender.yaml get /api/v1/journal-references Returns journal/outbox reference data resolved by correlation ID. # Get journal reference by id Source: https://docs.lerian.studio/en/reference/lender/get-journal-reference-by-id en/openapi/v3-current/lender.yaml get /api/v1/journal-references/{id} Returns journal/outbox reference data by journal reference ID. # Get jurisdiction by code Source: https://docs.lerian.studio/en/reference/lender/get-jurisdiction en/openapi/v3-current/lender.yaml get /api/v1/jurisdictions/{code} Returns the descriptor for a single registered jurisdiction profile. Public (no tenant scope). # Get loan account CET disclosure Source: https://docs.lerian.studio/en/reference/lender/get-loan-account-cet-disclosure en/openapi/v3-current/lender.yaml get /api/v1/br/loan-accounts/{id}/cet-disclosure Returns the latest immutable CET disclosure artifact frozen for the active loan account. # Get credit operation descriptor Source: https://docs.lerian.studio/en/reference/lender/get-loan-account-credit-operation-descriptor en/openapi/v3-current/lender.yaml get /api/v1/br/loan-accounts/{id}/credit-descriptor Returns the Res. CMN 5.004/2022 credit operation descriptor, optionally reconstructed as of a business date. # Get loan account PDD stage Source: https://docs.lerian.studio/en/reference/lender/get-loan-account-pdd-stage en/openapi/v3-current/lender.yaml get /api/v1/br/loan-accounts/{id}/pdd-stage Returns the current Brazilian PDD delinquency stage and accrual eligibility state. # Get loan product Source: https://docs.lerian.studio/en/reference/lender/get-loan-product en/openapi/v3-current/lender.yaml get /api/v1/loan-products/{id} Returns a loan product by ID. # Get loan product (BR alias) Source: https://docs.lerian.studio/en/reference/lender/get-loan-product-br en/openapi/v3-current/lender.yaml get /api/v1/br/loan-products/{id} BR-aliased path for retrieving a loan product. Resolves to the same handler as the core endpoint. # Get payoff statement Source: https://docs.lerian.studio/en/reference/lender/get-payoff-statement en/openapi/v3-current/lender.yaml get /api/v1/br/loan-accounts/{id}/payoff-statements/{quote_id} # Ingest BR capitalization clause consent Source: https://docs.lerian.studio/en/reference/lender/ingest-capitalization-clause-consent en/openapi/v3-current/lender.yaml post /api/v1/br/loan-applications/{id}/capitalization-consents Hashes clause text server-side and stores an append-only consent artifact for BR-profile disbursement guards. # List active loan charges Source: https://docs.lerian.studio/en/reference/lender/list-active-loan-charges en/openapi/v3-current/lender.yaml get /api/v1/loan-accounts/{id}/charges Returns assessed charges for an active loan account. A malformed UUID yields 422, distinct from 404. # List active loan transactions Source: https://docs.lerian.studio/en/reference/lender/list-active-loan-transactions en/openapi/v3-current/lender.yaml get /api/v1/loan-accounts/{id}/transactions Returns recorded repayment transactions for an active loan account. A malformed UUID yields 422, distinct from 404. # List registered jurisdictions Source: https://docs.lerian.studio/en/reference/lender/list-jurisdictions en/openapi/v3-current/lender.yaml get /api/v1/jurisdictions Returns the set of jurisdiction profiles compiled into the running service. Public (no tenant scope). # List loan account audit events Source: https://docs.lerian.studio/en/reference/lender/list-loan-account-audit-events-huma en/openapi/v3-current/lender.yaml get /api/v1/loan-accounts/{id}/audit-events Returns immutable correction, replay, and operator-visible audit history for a loan account. Schema-bounded limit (1–100, default 50); out-of-range values yield 422. A malformed UUID yields 422 (schema error). # Get loan product floating rate table Source: https://docs.lerian.studio/en/reference/lender/list-loan-product-floating-rates en/openapi/v3-current/lender.yaml get /api/v1/loan-products/{id}/floating-rates Resolves the product-version rate mode and effective floating-rate period for the requested as-of date. # List loan products Source: https://docs.lerian.studio/en/reference/lender/list-loan-products en/openapi/v3-current/lender.yaml get /api/v1/loan-products Returns a paginated list of loan products. # List loan products (BR alias) Source: https://docs.lerian.studio/en/reference/lender/list-loan-products-br en/openapi/v3-current/lender.yaml get /api/v1/br/loan-products BR-aliased path for listing loan products. Resolves to the same handler as the core endpoint. # Prepay loan account Source: https://docs.lerian.studio/en/reference/lender/prepay-loan-account en/openapi/v3-current/lender.yaml post /api/v1/loan-accounts/{id}/prepayments Appends an immutable prepayment schedule version and recalculation trigger. BR accounts require quoteId; amount-only prepayment is available only for non-BR accounts. When both are provided, amount must equal the quote's net settlement amount. Idempotency is enforced by the lib-commons idempotency middleware via the X-Idempotency header; a reused key with different facts yields 409. A missing required BR quote, malformed UUID, or invalid amount yields 422. # Prepay loan account (BR alias) Source: https://docs.lerian.studio/en/reference/lender/prepay-loan-account-br en/openapi/v3-current/lender.yaml post /api/v1/br/loan-accounts/{id}/prepayments BR-aliased path for prepaying a loan account. Resolves to the same handler as the core endpoint, with the jurisdiction resolver inserted into the chain. BR accounts require quoteId; when amount is also provided, it must equal the quote's net settlement amount. Idempotency is enforced by the lib-commons idempotency middleware via the X-Idempotency header; a reused key with different facts yields 409. A missing quote, malformed UUID, or invalid amount yields 422. # Preview amortisation schedule Source: https://docs.lerian.studio/en/reference/lender/preview-loan-schedule en/openapi/v3-current/lender.yaml post /api/v1/loan-applications/preview-schedule Computes a deterministic declining-balance schedule for the supplied loan parameters without persisting any data. # Preview repayment allocation Source: https://docs.lerian.studio/en/reference/lender/preview-repayment en/openapi/v3-current/lender.yaml post /api/v1/loan-accounts/{id}/preview-repayment Returns a deterministic repayment allocation preview without recording a transaction. Not idempotency-gated. A malformed UUID yields 422 (schema error). A malformed amount string yields 422. # Preview jurisdiction tax amounts Source: https://docs.lerian.studio/en/reference/lender/preview-tax en/openapi/v3-current/lender.yaml post /api/v1/br/tax/preview Computes disbursement IOF tax amounts using the active compiled jurisdiction profile without persisting data. # Record repayment Source: https://docs.lerian.studio/en/reference/lender/record-repayment en/openapi/v3-current/lender.yaml post /api/v1/loan-accounts/{id}/repayments Records a repayment allocation for an active loan account. Idempotency is enforced in the use case via the X-Request-ID header (X-Idempotency fallback): a reused request ID with different facts yields 409. A malformed UUID or amount yields 422. # Reject loan application Source: https://docs.lerian.studio/en/reference/lender/reject-loan-application en/openapi/v3-current/lender.yaml post /api/v1/loan-applications/{id}/reject Transitions a pending-approval loan application to rejected. # Reschedule loan account Source: https://docs.lerian.studio/en/reference/lender/reschedule-loan-account en/openapi/v3-current/lender.yaml post /api/v1/loan-accounts/{id}/reschedules Appends an immutable rescheduled schedule version and recalculation trigger. Idempotency is enforced by the lib-commons idempotency middleware via the X-Idempotency header; a reused key with different facts yields 409. A malformed UUID yields 422. # Reverse loan account transaction Source: https://docs.lerian.studio/en/reference/lender/reverse-loan-account-transaction en/openapi/v3-current/lender.yaml post /api/v1/loan-accounts/{id}/transactions/{transactionId}/reverse Creates an immutable compensating transaction and preserves reversal lineage. Idempotency is enforced in the use case via the X-Request-ID header (X-Idempotency fallback): a reused request ID with different facts yields 409. A malformed UUID yields 422; an unknown transaction yields 404. # Withdraw loan application Source: https://docs.lerian.studio/en/reference/lender/withdraw-loan-application en/openapi/v3-current/lender.yaml post /api/v1/loan-applications/{id}/withdraw Transitions a loan application to withdrawn. # Create a subscription Source: https://docs.lerian.studio/en/reference/streaming-hub/create-subscription en/openapi/v3-current/streaming-hub.yaml post /v1/subscriptions Creates a delivery subscription for the authenticated tenant. The tenant is taken from the validated JWT claims only — a `tenant_id` in the body is ignored. A `webhook` subscription is born `pending_verification` and mints a signing secret that is returned exactly once in this response (see `signingSecret`); it stays undeliverable until a successful probe via `POST /v1/subscriptions/{id}/ping` moves it to `active`. A queue subscription (`sqs`, `rabbitmq`, `eventbridge`) is also born `pending_verification` and mints no secret; it stays undeliverable until a probe succeeds. Every queue kind activates on an outbound credential supplied and probed via `PUT /v1/subscriptions/{id}/credential`. An AWS kind (`sqs`, `eventbridge`) has a second path that stores no credential: register a delegated grant, then call `POST /v1/subscriptions/{id}/verify`. Inline `sink_config` or `credential` at create is rejected — queue credentials arrive only on the credential PUT. # Delete a subscription Source: https://docs.lerian.studio/en/reference/streaming-hub/delete-subscription en/openapi/v3-current/streaming-hub.yaml delete /v1/subscriptions/{id} Soft-deletes a subscription scoped to the authenticated tenant. The operation is idempotent: an absent, already-deleted, or cross-tenant id is a no-op that still answers `204` (no existence oracle). # List the event catalog Source: https://docs.lerian.studio/en/reference/streaming-hub/get-event-catalog en/openapi/v3-current/streaming-hub.yaml get /v1/catalog Returns the manifest-fed catalog snapshot — the last-known-good aggregation of the producer manifests Streaming Hub tracks. The catalog is global in v1 (byte-identical regardless of which tenant authenticates) and control-plane only; the data plane never consults it. An empty snapshot returns `200` with `events: []` (fail-closed to last-known-good — never an error on an empty catalog). # Get a subscription Source: https://docs.lerian.studio/en/reference/streaming-hub/get-subscription en/openapi/v3-current/streaming-hub.yaml get /v1/subscriptions/{id} Returns one subscription scoped to the authenticated tenant. An absent, soft-deleted, or cross-tenant id returns a uniform `404 not_found` (no existence oracle). The response never carries the signing secret. # Get subscription delivery health Source: https://docs.lerian.studio/en/reference/streaming-hub/get-subscription-health en/openapi/v3-current/streaming-hub.yaml get /v1/subscriptions/{id}/health Returns a consolidated, non-secret delivery-health rollup for one subscription — the "why did this tenant stop receiving events" view. The existence gate runs first, so an absent, soft-deleted, or cross-tenant id returns a uniform `404 not_found`. Outcome counts are a recent-window view, not an all-time tally. # Get AWS delegated-grant setup artifacts Source: https://docs.lerian.studio/en/reference/streaming-hub/get-subscription-setup-artifacts en/openapi/v3-current/streaming-hub.yaml get /v1/subscriptions/{id}/setup-artifacts Generates the AWS delegated-grant onboarding artifacts for an AWS-sink subscription (`sqs`, `eventbridge`): the cross-account IAM trust policy and a CloudFormation quick-create console link the customer applies in their own AWS account to create the delivery role Streaming Hub assumes. On first fetch a non-secret `sts:ExternalId` correlation value is minted and embedded in both artifacts; a re-fetch reuses it (stable artifacts). No AWS credential is ever stored or returned — the role is assumed per delivery, guarded by the `ExternalId` condition. A non-AWS sink returns `422 validation_error`. # List dead-letter observations Source: https://docs.lerian.studio/en/reference/streaming-hub/list-dlq-observations en/openapi/v3-current/streaming-hub.yaml get /admin/dlq Cross-tenant operator forensics: returns a keyset-paginated, reverse-chronological page of dead-letter observations captured from the delivery pipeline. This is an operator surface — it authorizes against an operator scope and carries no tenant context. The projection carries no secret, no customer endpoint URL, and no signing material. The `tenant_id_raw` field is an untrusted, best-effort capture surfaced for forensic value only. # List subscriptions Source: https://docs.lerian.studio/en/reference/streaming-hub/list-subscriptions en/openapi/v3-current/streaming-hub.yaml get /v1/subscriptions Lists the authenticated tenant's live subscriptions, newest-first, keyset-paginated. No read projection ever carries the signing secret. # Ping a webhook subscription Source: https://docs.lerian.studio/en/reference/streaming-hub/ping-subscription en/openapi/v3-current/streaming-hub.yaml post /v1/subscriptions/{id}/ping Runs a synchronous, signed webhook probe through the production delivery path and returns the classified result. A successful ping flips a `webhook` subscription from `pending_verification` to `active`. A classified probe failure is still a `200` (the probe ran and produced a verdict). This route is naturally idempotent and requires no `X-Idempotency` header. A queue subscription has no ping probe and returns `422 probe_unsupported_for_sink_kind`. # Pull entitled events Source: https://docs.lerian.studio/en/reference/streaming-hub/pull-events en/openapi/v3-current/streaming-hub.yaml get /v1/events Fetches a page of a pull subscription's entitled events in arrival-order `seq`. The read doubles as the acknowledgment (cursor-as-ack): after a successful fetch the highest returned `seq` is persisted as the subscription's cursor, monotonically. An explicit `after` cursor overrides the persisted cursor for the read; a forward seek advances the durable cursor past the unread gap (the skipped range is not re-delivered — caller-owns-the-gap), while a backward seek never rewinds it. The read is rate-limited per tenant. Events are at-least-once; deduplicate on `ceId`. # Rotate the signing secret Source: https://docs.lerian.studio/en/reference/streaming-hub/rotate-subscription-secret en/openapi/v3-current/streaming-hub.yaml post /v1/subscriptions/{id}/secret/rotate Mints a new webhook signing secret, returns it exactly once, persists it as the current secret, and demotes the previous secret with a 24-hour overlap during which both remain valid for verification. Delivery signs with the current secret. A subscription with no signing secret at rest (a `pull` sink) returns `422 no_secret_to_rotate`. # Set or rotate a queue credential Source: https://docs.lerian.studio/en/reference/streaming-hub/set-subscription-credential en/openapi/v3-current/streaming-hub.yaml put /v1/subscriptions/{id}/credential Sets or rotates the outbound credential for a queue subscription (`sqs`, `rabbitmq`, `eventbridge`) with probe-on-write: the candidate credential is held in memory, probed (connect + auth) immediately, and persisted encrypted only on probe success — a failed probe stores nothing. A successful probe sets `verification_state` to `active`: from `pending_verification` on the first write, and from `degraded` when a rotation fixes a destination that had started failing. It leaves the `enabled` flag exactly as it found it. The credential is write-only: it is supplied only here and is never returned, not even masked, on any read path. A classified probe failure is still a `200`. This route is naturally idempotent and requires no `X-Idempotency` header. # Register an AWS delegated grant Source: https://docs.lerian.studio/en/reference/streaming-hub/set-subscription-delegated-grant en/openapi/v3-current/streaming-hub.yaml put /v1/subscriptions/{id}/delegated-grant Registers the AWS delegated-grant coordinates for an AWS-sink subscription (`sqs`, `eventbridge`) — the non-secret cross-account delivery role ARN, region, and destination the dispatcher assumes at delivery time. This is the write that follows the customer applying the setup-artifacts trust policy in their AWS account. It is write-only: it runs no inline probe and does not change the verification state (a registered grant stays `pending_verification` until a later `verify`). No AWS credential crosses this body. The destination host is validated before the write. A non-AWS sink returns `422 validation_error`. # Re-pin a subscription's schema major Source: https://docs.lerian.studio/en/reference/streaming-hub/update-subscription-schema-pin en/openapi/v3-current/streaming-hub.yaml patch /v1/subscriptions/{id} Narrowly re-pins a subscription's `schema_major` — the only mutable field. Send exactly one field, `schema_major`: an integer `>= 1` re-pins to the versioned topic; `null` clears the pin to follow-base. Any other field, an out-of-range value, or an empty body is rejected with `422 validation_error` (out-of-scope fields are rejected, not ignored). A re-pin is not a destination change: it does not reset the verification state and echoes no secret. Success returns `200` with no body. # Re-verify and re-enable a subscription Source: https://docs.lerian.studio/en/reference/streaming-hub/verify-subscription en/openapi/v3-current/streaming-hub.yaml post /v1/subscriptions/{id}/verify Re-runs the sink-appropriate probe (dispatched by sink kind) and, on a probe success, transitions the subscription back to `active` and clears any auto-disable in the same transaction. The row is transitioned in place — the id and signing secret are preserved. A classified probe failure is still a `200`. This route is naturally idempotent and requires no `X-Idempotency` header. The `webhook`, `sqs` and `eventbridge` sinks have registered verify probes. For an AWS sink this is the delegated-grant activation path, the alternative to an outbound credential on `PUT /v1/subscriptions/{id}/credential`, which probes every queue kind at write time. Any other sink kind returns `422 probe_unsupported_for_sink_kind`: a `rabbitmq` sink is probed on that credential PUT, which moves `verification_state` alone, and a `pull` subscription has no destination to probe. # Acknowledge claim Source: https://docs.lerian.studio/en/reference/rails/spi/dict/acknowledge-claim en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/claims/{claimID}/acknowledge Donor acknowledges the claim, transitioning it from OPEN to WAITING_RESOLUTION. # Acknowledge DICT infraction Source: https://docs.lerian.studio/en/reference/rails/spi/dict/acknowledge-infraction en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/infractions/{id}/acknowledge Transitions an infraction OPEN→ACKNOWLEDGED and reports the transition to BACEN. # Cancel claim Source: https://docs.lerian.studio/en/reference/rails/spi/dict/cancel-claim en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/claims/{claimID}/cancel Cancels a claim. The cancelling side is derived from the authenticated participant; the reason (USER_REQUESTED or FRAUD) is validated against the claim type per the BACEN DICT reason×type matrix. # Cancel fraud marker Source: https://docs.lerian.studio/en/reference/rails/spi/dict/cancel-fraud-marker en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/fraud-markers/{id}/cancel Transitions a fraud marker ACTIVE→INACTIVE and reports the transition to BACEN. # Cancel DICT funds recovery Source: https://docs.lerian.studio/en/reference/rails/spi/dict/cancel-funds-recovery en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/funds-recoveries/{id}/cancel Transitions a funds recovery →CANCELLED (before REFUNDING) and reports the cancellation to BACEN. # Cancel DICT infraction Source: https://docs.lerian.studio/en/reference/rails/spi/dict/cancel-infraction en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/infractions/{id}/cancel Transitions an infraction →CANCELLED and reports the transition to BACEN. # Batch check PIX key existence Source: https://docs.lerian.studio/en/reference/rails/spi/dict/check-keys en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/keys/check Probes up to 200 PIX key values for existence in one call, backed by the BACEN DICT keys/check (dict-np host). Returns one existence-only result per submitted key, in submission order. Requires the dict-np endpoint; a 503 is returned when it is unconfigured. # Cleanup orphaned PIX keys Source: https://docs.lerian.studio/en/reference/rails/spi/dict/cleanup-orphaned-keys en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/internal/keys/cleanup-orphaned Finds PIX keys stuck in PENDING_BACEN_SYNC and retries BACEN registration or deletes them. Internal admin endpoint. # Close DICT infraction Source: https://docs.lerian.studio/en/reference/rails/spi/dict/close-infraction en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/infractions/{id}/close Transitions an infraction →CLOSED with an AGREED/DISAGREED analysis and reports it to BACEN; on AGREED, BACEN creates the fraud marker. # Complete claim Source: https://docs.lerian.studio/en/reference/rails/spi/dict/complete-claim en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/claims/{claimID}/complete Completes a confirmed claim, finalizing the key transfer. # Confirm claim Source: https://docs.lerian.studio/en/reference/rails/spi/dict/confirm-claim en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/claims/{claimID}/confirm Donor confirms the claim, allowing the key transfer to proceed. # Create fraud marker Source: https://docs.lerian.studio/en/reference/rails/spi/dict/create-fraud-marker en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/fraud-markers Persists a minimized fraud marker locally after hashing PII inputs for storage, and submits the corresponding BACEN fraud-marker report synchronously-after-persist, tracked by report_status (PENDING/SENT/FAILED) + request_id. # Create DICT funds recovery Source: https://docs.lerian.studio/en/reference/rails/spi/dict/create-funds-recovery en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/funds-recoveries Creates a local DICT MED 2.0 funds-recovery record and reports it to BACEN (createFundsRecovery). BACEN dedups a recovery per RootTransactionId. # Create a refund under a funds recovery Source: https://docs.lerian.studio/en/reference/rails/spi/dict/create-funds-recovery-refund en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/funds-recoveries/{id}/refund Creates a local DICT refund bound to the funds recovery (refund-under-recovery, MED 2.0) and reports it to BACEN. # Create DICT infraction Source: https://docs.lerian.studio/en/reference/rails/spi/dict/create-infraction en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/infractions Persists a DICT infraction locally and submits the corresponding BACEN infraction report synchronously-after-persist, tracked by report_status (PENDING/SENT/FAILED) + request_id. # Delete PIX key Source: https://docs.lerian.studio/en/reference/rails/spi/dict/delete-key en/openapi/v3-current/spi-dict.yaml delete /api/v1/dict/keys Deletes a PIX key by its value. Only ACTIVE or INACTIVE keys can be deleted. Keys under claim or pending BACEN sync are rejected. # Fraud marker statistics Source: https://docs.lerian.studio/en/reference/rails/spi/dict/fraud-marker-stats en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/fraud-markers/stats Returns fraud-marker counts. # Get claim Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-claim en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/claims/{claimID} Retrieves a single claim by its UUID. # Get BACEN entry antifraud statistics Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-entry-statistics en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/entries/{key}/statistics Fetches BACEN's system-wide key-level antifraud statistics (owner + key blocks) by Pix key and projects them verbatim, with no local aggregation. # Get fraud marker Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-fraud-marker en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/fraud-markers/{id} Retrieves a minimized fraud marker by UUID. # Get DICT funds recovery Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-funds-recovery en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/funds-recoveries/{id} Retrieves a local DICT funds-recovery record by its local UUID. # Get DICT funds-recovery tracking graph Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-funds-recovery-tracking-graph en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/funds-recoveries/{id}/tracking-graph Fetches the AUTOMATED-flow fraud-tracking graph from BACEN and projects it verbatim. The manual createTrackingGraph POST is BACEN-deprecated; only this read is current. # Get DICT infraction Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-infraction en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/infractions/{id} Retrieves a local DICT infraction record by UUID. # Get BACEN person antifraud statistics Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-person-statistics en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/persons/{taxId}/statistics Fetches BACEN's system-wide person-level antifraud statistics by tax id and projects them verbatim, with no local aggregation (distinct from local fraud-marker stats). # Initiate claim Source: https://docs.lerian.studio/en/reference/rails/spi/dict/initiate-claim en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/participants/{participantISPB}/claims Initiates a new PIX key ownership or portability claim for the participant identified in the route. Claim deadlines are seven calendar days and claimer/donor ISPB equality is rejected by domain validation. # List claims Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-claims en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/claims Returns a paginated list of claims. # List fraud markers Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-fraud-markers en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/fraud-markers Returns paginated minimized fraud markers. # List DICT funds recoveries Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-funds-recoveries en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/funds-recoveries Returns paginated local DICT funds-recovery records. # List a funds recovery's infraction reports Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-funds-recovery-infraction-reports en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/funds-recoveries/{id}/infraction-reports Lists the local DICT infraction reports cross-linked to a funds recovery (local read of our persisted reports, not a BACEN fetch). # List a funds recovery's refunds Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-funds-recovery-refunds en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/funds-recoveries/{id}/refunds Lists the local DICT refunds cross-linked to a funds recovery (local read of our persisted refunds, not a BACEN fetch). # List DICT infractions Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-infractions en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/infractions Returns paginated local DICT infraction records. # List PIX keys Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-keys en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/keys Returns a paginated list of PIX keys. Optionally filter by participant ISPB. # Lookup PIX key Source: https://docs.lerian.studio/en/reference/rails/spi/dict/lookup-key en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/keys/lookup Looks up a PIX key by JSON body value. Uses cache when available, falls back to database and BACEN DICT, and returns ownerTaxId full and unmasked per BACEN API-DICT. # Process claim deadlines Source: https://docs.lerian.studio/en/reference/rails/spi/dict/process-claim-deadlines en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/internal/claims/process-deadlines Internal endpoint that processes expired claim deadlines. Intended for cron/scheduler invocation. # Register PIX key Source: https://docs.lerian.studio/en/reference/rails/spi/dict/register-key en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/keys Registers a new PIX key after validating the request, enforcing owner limits, and successfully synchronizing with BACEN DICT. Registration fails closed if BACEN DICT is unavailable or rejects the key. # Reject claim (decline) Source: https://docs.lerian.studio/en/reference/rails/spi/dict/reject-claim en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/claims/{claimID}/reject Declines a claim. BACEN DICT has no reject verb; this is a USER_REQUESTED cancellation that aborts the key transfer while keeping the existing key link unchanged. The cancelling side is derived from the authenticated participant; the reason×type matrix then constrains the cell (a donor declining an OWNERSHIP claim via USER_REQUESTED is rejected — the donor's ownership lever is FRAUD). # Reconcile DICT keys against a BACEN event-list Source: https://docs.lerian.studio/en/reference/rails/spi/dict/run-events-reconciliation en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/internal/reconciliation/run-events CONTINGENCY reconciliation: reconciles an operator-supplied BACEN DICT event-list (CID) against local key state, flagging divergences. Used when the live CID/VSync inventory channel is unavailable. Enumeration/divergence only — no BACEN call, no money, no position. # Trigger DICT reconciliation Source: https://docs.lerian.studio/en/reference/rails/spi/dict/trigger-reconciliation en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/internal/reconciliation/run Triggers an on-demand reconciliation run against BACEN DICT. # Provision participant ISPB Source: https://docs.lerian.studio/en/reference/rails/spi/dict/upsert-participant-settings en/openapi/v3-current/spi-dict.yaml put /api/v1/dict/internal/participant-settings Provisions (idempotent upsert) the participant ISPB used as the authoritative inventory for FULL DICT reconciliation. # Request a Conta PI account report Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-account-report-request en/openapi/v3-current/spi.yaml post /api/v1/spi/account-reports/requests Prerequisite: this is the request step of the account-reporting chain (camt.060 request → camt.052/053/054 result read); the camt.060 request MUST be submitted and correlated first — the camt.05x result is read back only after this request is acknowledged. Builds a camt.060 account-reporting request (BALANCE → camt.053 the SALDO/balance, STATEMENT → camt.052 the entry-list file pointer, ENTRIES → camt.054 the entry detail), records an ACCOUNT_INFORMATION_REQUEST operation keyed on the generated correlation id, and submits the signed payload to the BACEN catalogue surface. # Create approval request Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-approval en/openapi/v3-current/spi.yaml post /api/v1/spi/approvals Creates a new approval_decisions row in REQUESTED state. # Initiate SPI connectivity test Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-connectivity-test en/openapi/v3-current/spi.yaml post /api/v1/spi/connectivity-tests Prerequisite: this is the rail-liveness step of the onboarding chain (connectivity-test → submit); a passing connectivity test is the prerequisite for a submit operation (see createPayment, createStructuredOperation). Builds a pibr.001 echo request, records a CONNECTIVITY_TEST operation keyed on the generated correlation id, and submits the signed payload to the BACEN catalogue surface. # Create evidence export job Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-evidence-export en/openapi/v3-current/spi.yaml post /api/v1/spi/evidence/exports Creates an evidence_export_jobs row in QUEUED state. Idempotent — a duplicate Idempotency-Key returns the existing job. # Cancel a Pix Automático mandate (pain.011) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-mandate-cancellation en/openapi/v3-current/spi.yaml post /api/v1/spi/mandates/cancellations Builds a pain.011 cancellation for an EXISTING mandate, validates the mandate state machine, and submits the signed payload to BACEN. # Request confirmation of a Pix Automático mandate (pain.009) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-mandate-request en/openapi/v3-current/spi.yaml post /api/v1/spi/mandates Builds a pain.009 mandate-confirmation request, validates the mandate state machine, records the PIX_AUTOMATIC_AUTHORIZATION operation, and submits the signed payload to the BACEN catalogue surface. # Accept or reject a Pix Automático mandate (pain.012) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-mandate-resolution en/openapi/v3-current/spi.yaml post /api/v1/spi/mandates/resolutions Builds a pain.012 acceptance report for a mandate the platform holds, validates the mandate state machine, and submits the signed payload to BACEN. # Create payment Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-payment en/openapi/v3-current/spi.yaml post /api/v1/spi/payments Prerequisite: a passing connectivity test (see createConnectivityTest) confirms the rail is reachable before a submit. Initiates a new BRL-only SPI payment via BACEN. Generates an EndToEndID if not provided. The EndToEndID format is E{ISPB}{YYYYMMDD}{uniqueID}, exactly 32 characters. # Request cancellation of a Pix Automático schedule (camt.055) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-schedule-cancellation en/openapi/v3-current/spi.yaml post /api/v1/spi/scheduled-payments/cancellations Builds a camt.055 cancellation request for a schedule the platform tracks, validates the schedule state machine, and submits the signed payload to BACEN. # Resolve a received cancellation request (camt.029) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-schedule-cancellation-resolution en/openapi/v3-current/spi.yaml post /api/v1/spi/scheduled-payments/cancellations/resolutions Builds a camt.029 resolution (ACCR/RJCR) for a camt.055 the platform actually RECEIVED; unknown correlations are rejected. # Schedule a Pix Automático payment instruction (pain.013) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-scheduled-payment en/openapi/v3-current/spi.yaml post /api/v1/spi/scheduled-payments Builds a pain.013 scheduled payment instruction, enforces the ACTIVE-mandate precondition, records the SCHEDULED_PAYMENT_INSTRUCTION operation, and submits the signed payload to BACEN. # Accept or reject a Pix Automático scheduled payment (pain.014) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-scheduled-payment-resolution en/openapi/v3-current/spi.yaml post /api/v1/spi/scheduled-payments/resolutions Builds a pain.014 status report (ACSP/RJCT) for a schedule the platform holds, validates the schedule state machine, and submits the signed payload to BACEN. # Report internally settled Pix transactions (MED 2.0) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-settlement-report en/openapi/v3-current/spi.yaml post /api/v1/spi/settlement-reports Builds a trck.002 internal-settlement report, records a TRACKING_REPORT operation keyed on the generated correlation id, and submits the signed payload to the BACEN catalogue surface. # Create structured operation Source: https://docs.lerian.studio/en/reference/rails/spi/spi/create-structured-operation en/openapi/v3-current/spi.yaml post /api/v1/spi/operations Prerequisite: a passing connectivity test (see createConnectivityTest) confirms the rail is reachable before a submit. Submits a structured business operation to the SPI rail. Raw regulated payloads are forbidden anywhere in the request body. # Deactivate a Pix certificate Source: https://docs.lerian.studio/en/reference/rails/spi/spi/deactivate-pix-certificate en/openapi/v3-current/spi.yaml delete /api/v1/spi/certificates/{id} Flips an ACTIVE certificate to DEACTIVATED and appends a DEACTIVATE audit row. Never a hard delete — the inventory and its audit trail are retained. # Decide approval Source: https://docs.lerian.studio/en/reference/rails/spi/spi/decide-approval en/openapi/v3-current/spi.yaml post /api/v1/spi/approvals/{approvalId}/decision Records an Approve or Deny decision on an approval_decisions row. # Evaluate recovery eligibility Source: https://docs.lerian.studio/en/reference/rails/spi/spi/evaluate-recovery en/openapi/v3-current/spi.yaml post /api/v1/spi/operations/{operationId}/recovery/evaluate Read-only preview of recovery eligibility for an operation. No persistence; no state mutation. # Execute recovery action Source: https://docs.lerian.studio/en/reference/rails/spi/spi/execute-recovery en/openapi/v3-current/spi.yaml post /api/v1/spi/recovery/actions/{recoveryActionId}/execute Executes a previously requested recovery action. # Read a Conta PI balance (camt.053 result) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-account-report-balance en/openapi/v3-current/spi.yaml get /api/v1/spi/account-reports/{correlationId}/balance Result step of the account-reporting chain (camt.060 request → camt.053 balance read). Returns the Conta PI SALDO balances BACEN sent in the inbound camt.053, projected verbatim and keyed by the camt.060 correlation id. Reads PENDING until the camt.053 arrives, then ANSWERED with the typed balance set (raw BalanceTypeCode + verbatim amounts) plus a freshness timestamp. Lerian SPI projects the balance BACEN sends; it never computes a position. # Read a Conta PI extrato (camt.052 RCO lançamentos result) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-account-report-extract en/openapi/v3-current/spi.yaml get /api/v1/spi/account-reports/{correlationId}/extract Result step of the account-reporting chain (camt.060 SOLIC_REL_LANCAMENTO request → camt.052 extrato read). Returns the RCO lançamentos rows BACEN generated on the ARQ rail, fetched + SHA-256-verified + parsed + projected verbatim, keyed by the camt.060 correlation id. Reads PENDING until the camt.052 extrato is received and verified, then ANSWERED with the parsed rows plus the integrity and row-COUNT recon status (declaredEntryCount vs parsedEntryCount). Lerian SPI projects the extrato BACEN sends and reconciles its integrity + row count; it never sums an amount nor computes a position. # Get approval Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-approval en/openapi/v3-current/spi.yaml get /api/v1/spi/approvals/{approvalId} Retrieves an approval_decisions row by id. # Get evidence export job Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-evidence-export en/openapi/v3-current/spi.yaml get /api/v1/spi/evidence/exports/{exportId} Returns the export job state for the given id. # Inspect a recorded Pix Automático mandate operation (internal operator read) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-mandate en/openapi/v3-current/spi.yaml get /api/v1/spi/mandates/{mandateId} INTERNAL operator read. Returns the recorded PIX_AUTOMATIC_AUTHORIZATION operation for the mandate id, with its lifecycle state. Unknown mandate ids return 404 so existence is non-enumerable. Projects stored facts verbatim — no monetary value or position. This is an operational read surface, NOT a BACEN API-Pix conformant endpoint. # Inspect a message-log entry and its correspondent Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-message-log-detail en/openapi/v3-current/spi.yaml get /api/v1/spi/messages/{messageRecordId} Returns the requested message_records row plus its correspondent — the sibling row sharing operation_id (Mensagem Original + Mensagem Correspondente). Hash-only metadata; no raw XML and no payload body are returned, and the original<->correspondent pairing is a single indexed lookup, never a monetary aggregate. # Get operation evidence Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-operation-evidence en/openapi/v3-current/spi.yaml get /api/v1/spi/operations/{operationId}/evidence Returns a chronological page of normalized evidence records for an operation. Raw payload references are denied by default. # Get operation timeline Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-operation-timeline en/openapi/v3-current/spi.yaml get /api/v1/spi/operations/{operationId}/timeline Returns a chronological page of timeline events for one operation. Limit defaults to 50, capped at 200. # Get payment Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-payment en/openapi/v3-current/spi.yaml get /api/v1/spi/payments/{endToEndID} Retrieves a single payment by its EndToEndID, including event history when available. # Get payment history Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-payment-history en/openapi/v3-current/spi.yaml get /api/v1/spi/payments/{endToEndID}/history Retrieves the full event history for a payment, ordered chronologically. # Get payment return status Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-payment-return-status en/openapi/v3-current/spi.yaml get /api/v1/spi/payments/{endToEndID}/returns/status Alias of Get payment for clients polling return state. # Get a Pix certificate with its audit trail Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-pix-certificate en/openapi/v3-current/spi.yaml get /api/v1/spi/certificates/{id} Returns one certificate's structured public metadata plus its append-only audit trail (UPLOAD/DEACTIVATE), newest first. # Get a participant's last-known Conta-PI responsibles Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-responsibles en/openapi/v3-current/spi.yaml get /api/v1/spi/responsibles/{ispb} Returns the last-known Conta-PI responsibles snapshot (CONTATOPSP contact, DIRETORPSP director, palavra-chave) for the participant ISPB, plus its source (SUBMITTED / COMP_CONFIRMED) and recorded-at. reda.022 is fire-to-BACEN; this read-model is written on submit-success and refreshed on the inbound reda.016 COMP advice. Structured fields only — never signed bytes. The palavra-chave is a regulatory identifier returned verbatim; it is masked on display. # Inspect a recorded Pix Automático schedule operation (internal operator read) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-schedule en/openapi/v3-current/spi.yaml get /api/v1/spi/scheduled-payments/{scheduleId} INTERNAL operator read. Returns the recorded SCHEDULED_PAYMENT_INSTRUCTION operation for the schedule id, with its lifecycle state. Unknown schedule ids return 404 so existence is non-enumerable. Projects stored facts verbatim — no monetary value or position. This is an operational read surface, NOT a BACEN API-Pix conformant endpoint. # Get SPI capability by ID Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-spi-capability en/openapi/v3-current/spi.yaml get /api/v1/spi/capabilities/{capabilityId} Returns the detail of a single SPI/Pix capability identified by its message type (e.g. "pacs.008"). # Conta-PI balance-window report Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-spi-report-balances en/openapi/v3-current/spi.yaml get /api/v1/spi/reports/balances Lists the spi_account_balance snapshots (camt.053 saldo) received in a mandatory date window (max 92 days), projected verbatim. The amount is the BACEN-supplied figure projected as-is — never recomputed, never summed. # Conta-PI extract-list report Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-spi-report-extracts en/openapi/v3-current/spi.yaml get /api/v1/spi/reports/extracts Lists the spi_account_extract rows (camt.052/RCO) received in a mandatory date window (max 92 days) with their report_kind and integrity (verified) + COUNT-recon (countsMatch) flags. The RCO entries stay opaque and are not inlined; no arithmetic is performed over balances. # SPI rejected-payments report Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-spi-report-rejected en/openapi/v3-current/spi.yaml get /api/v1/spi/reports/rejected Returns the COUNT and a paginated list of payments in a terminal-rejected state (REJECTED / FAILED / RETURN_PENDING) over a mandatory date window (max 92 days). The detailed reject reason is not retained — only status + EndToEndID are returned, and the absence is labelled honestly. # SPI volumetria report (COUNT-only) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/get-spi-report-volumetria en/openapi/v3-current/spi.yaml get /api/v1/spi/reports/volumetria Returns COUNT(*) buckets grouped by message type, status, or participant over a mandatory date window (max 92 days). COUNT-only: the SPI payments record persists no amount, so there is no monetary sum on the SPI side. # Initiate payment return Source: https://docs.lerian.studio/en/reference/rails/spi/spi/initiate-payment-return en/openapi/v3-current/spi.yaml post /api/v1/spi/payments/{endToEndID}/returns Prerequisite: the parent payment must be SETTLED before a return — read its state via getPayment (GET /api/v1/spi/payments/{endToEndID}); the {endToEndID} path segment structurally ties the return to its parent. Initiates an idempotent full payment return via BACEN. # List recorded Pix Automático mandate operations (internal operator read) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/list-mandates en/openapi/v3-current/spi.yaml get /api/v1/spi/mandates INTERNAL operator read. Returns a page of the platform's PIX_AUTOMATIC_AUTHORIZATION operations with their mandate lifecycle state, optionally filtered by status. Projects stored operation facts verbatim — no monetary value or position is returned or computed. This is an operational read surface, NOT a BACEN API-Pix conformant endpoint. # List the SPI message log Source: https://docs.lerian.studio/en/reference/rails/spi/spi/list-message-log en/openapi/v3-current/spi.yaml get /api/v1/spi/messages Returns a chronological, filterable page of message_records rows (Data/Hora, Tipo, Fluxo, Situação). Each row carries metadata + the SHA-256 payload hash only — the raw signed XML is never persisted at rest — and no monetary or position field is returned. # List Pix certificates Source: https://docs.lerian.studio/en/reference/rails/spi/spi/list-pix-certificates en/openapi/v3-current/spi.yaml get /api/v1/spi/certificates Returns the managed Pix-side certificate inventory (PIC/PIA/QR-code/CERTPIC/signing) with type, origin, status, custody, and validity window — structured public metadata only. Key material is never exposed. # Consultar Pix recebidos Source: https://docs.lerian.studio/en/reference/rails/spi/spi/list-received-pix en/openapi/v3-current/spi.yaml get /api/v1/spi/pix Returns the BACEN PixConsultados envelope of received Pix (those settled to this FI as creditor) within the inicio/fim window, each projected with its devoluções. cpf and cnpj are mutually exclusive. # List recorded Pix Automático schedule operations (internal operator read) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/list-schedules en/openapi/v3-current/spi.yaml get /api/v1/spi/scheduled-payments INTERNAL operator read. Returns a page of the platform's SCHEDULED_PAYMENT_INSTRUCTION operations with their schedule lifecycle state, optionally filtered by status. Projects stored operation facts verbatim — no monetary value or position is returned or computed. This is an operational read surface, NOT a BACEN API-Pix conformant endpoint. # List SPI capabilities Source: https://docs.lerian.studio/en/reference/rails/spi/spi/list-spi-capabilities en/openapi/v3-current/spi.yaml get /api/v1/spi/capabilities Returns the public catalog of SPI/Pix capabilities. Every authenticated caller sees the same rows. Supports filtering by operation family, handling mode, direction, and availability. # List stuck operations (operator dashboard) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/list-stuck-operations en/openapi/v3-current/spi.yaml get /api/v1/spi/operations/stuck Returns the cross-rail operator dashboard page of operations needing attention, conformed to the shared recovery vocabulary. Each row carries only the verbs SPI can actually perform on it (a verb SPI cannot perform is absent) and the disposition ('what happens if I do nothing'). No monetary or position fields are returned. # Manually resolve operation Source: https://docs.lerian.studio/en/reference/rails/spi/spi/manual-resolve-operation en/openapi/v3-current/spi.yaml post /api/v1/spi/operations/{operationId}/recovery/manual-resolution Closes a stuck operation by operator decision, gated by an APPROVED approval_decisions row. # Process inbound message Source: https://docs.lerian.studio/en/reference/rails/spi/spi/process-inbound-message en/openapi/v3-current/spi.yaml post /api/v1/spi/messages/inbound/process Processes an inbound ISO 20022 payment message: validates, assesses credit feasibility, settles or rejects, and publishes domain events. Available in local/dev/test environments only. # Process pacs.002 callback Source: https://docs.lerian.studio/en/reference/rails/spi/spi/process-pacs002-callback en/openapi/v3-current/spi.yaml post /api/v1/spi/messages/callbacks/pacs002 Validates and applies an asynchronous BACEN pacs.002 payment status callback. Accepts raw signed XML (application/xml or text/xml) or a JSON {xml:...} wrapper. # Process pacs.004 callback Source: https://docs.lerian.studio/en/reference/rails/spi/spi/process-pacs004-callback en/openapi/v3-current/spi.yaml post /api/v1/spi/messages/callbacks/pacs004 Validates and applies an asynchronous BACEN pacs.004 return callback. Accepts raw signed XML or a JSON {xml:...} wrapper. # Process BACEN pacs.008 callback Source: https://docs.lerian.studio/en/reference/rails/spi/spi/process-pacs008-callback en/openapi/v3-current/spi.yaml post /api/v1/spi/messages/callbacks/pacs008 Processes a raw BACEN pacs.008 XML callback and returns a signed pacs.002 XML response. Raw signed XML only (no JSON wrapper, no caller credit knobs). # Internal rail inbound callback Source: https://docs.lerian.studio/en/reference/rails/spi/spi/rail-inbound-callback en/openapi/v3-current/spi.yaml post /internal/v1/spi/rail/inbound Internal-only inbound rail callback guarded by the X-Internal-Callback shared secret. Not a client API surface. # Register Conta PI responsible contacts Source: https://docs.lerian.studio/en/reference/rails/spi/spi/register-responsibles en/openapi/v3-current/spi.yaml post /api/v1/spi/responsibles Builds the reda.022 four-block envelope (CONTATOPSP contact, DIRETORPSP director, palavra-chave, CPFDIRETOR), records a PARTICIPANT_ADMINISTRATION operation keyed on the generated correlation id, and submits the signed payload to the BACEN catalogue surface. # Solicitar devolução Source: https://docs.lerian.studio/en/reference/rails/spi/spi/request-devolucao en/openapi/v3-current/spi.yaml put /api/v1/spi/pix/{e2eid}/devolucao/{id} Prerequisite: the parent payment must be SETTLED before a return — read its state via getPayment (GET /api/v1/spi/payments/{endToEndID}). Requests a (possibly partial) Pix return identified by the client-generated DevolucaoId {id}; the sum of all returns must not exceed the original Pix value. A repeat PUT with the same {id} replays the existing devolução. Emits a pacs.004 (reason MD06 for ORIGINAL, SL02 for RETIRADA). # Request recovery action Source: https://docs.lerian.studio/en/reference/rails/spi/spi/request-recovery en/openapi/v3-current/spi.yaml post /api/v1/spi/operations/{operationId}/recovery/actions Creates a new recovery action. ALLOWED → REQUESTED; BLOCKED → 409 RECOVERY_NOT_ALLOWED; APPROVAL_REQUIRED → APPROVAL_REQUIRED row. # Upload a Pix certificate (public.cer) Source: https://docs.lerian.studio/en/reference/rails/spi/spi/upload-pix-certificate en/openapi/v3-current/spi.yaml post /api/v1/spi/certificates Imports a PUBLIC certificate (.cer; base64-encoded PEM or DER). The private key NEVER crosses this boundary — a.p12/private-key payload is rejected with 400 (fail-closed). For an HSM-custody key (the KMIP signing leaf) the flow is CSR-generate-in-HSM then upload the issued.cer only. # Validate inbound message Source: https://docs.lerian.studio/en/reference/rails/spi/spi/validate-inbound-message en/openapi/v3-current/spi.yaml post /api/v1/spi/messages/inbound/validate Validates an inbound ISO 20022 XML message (schema and signature) without processing it. # Prometheus metrics Source: https://docs.lerian.studio/en/reference/streaming-hub/get-metrics en/openapi/v3-current/streaming-hub.yaml get /metrics Serves a Prometheus text-exposition document. Streaming Hub exports its runtime metrics over OTLP, so this endpoint serves only the static `streaming_hub_build_info` gauge — the spine metrics are scraped from the OTLP collector, not here. Unauthenticated. # Runtime snapshot Source: https://docs.lerian.studio/en/reference/streaming-hub/get-runtime en/openapi/v3-current/streaming-hub.yaml get /runtime Returns a cheap Go-runtime snapshot for incident triage (goroutine count, GC cycles, heap bytes). No tenant or high-cardinality data. Unauthenticated. # Build identity Source: https://docs.lerian.studio/en/reference/streaming-hub/get-version en/openapi/v3-current/streaming-hub.yaml get /version Returns the build identity injected at link time. Unauthenticated. # Liveness probe Source: https://docs.lerian.studio/en/reference/streaming-hub/liveness-probe en/openapi/v3-current/streaming-hub.yaml get /healthz Returns `200` unconditionally once the process is serving, independent of dependency health. Wire it to a Kubernetes liveness probe. Unauthenticated. # Readiness probe Source: https://docs.lerian.studio/en/reference/streaming-hub/readiness-probe en/openapi/v3-current/streaming-hub.yaml get /readyz Folds the full probe set (boot gates, Postgres reachability, consumer liveness, and the degraders) into one readiness verdict. `Healthy` and `Degraded` return `200` (kept in rotation); `Down` returns `503`. Wire it to a Kubernetes readiness probe. Unauthenticated. # Cancel claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/cancel-claim en/openapi/v3-current/pix.yaml DELETE /v1/claims/{claimId} Cancel an active claim request. # Change a Pix key entry Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/change-a-pix-key-entry en/openapi/v3-current/pix.yaml PUT /v1/entries/{entryId} Re-points a Pix key to a new transactional account and/or owner name within the same PSP. The key value is unchanged. # Complete claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/complete-claim en/openapi/v3-current/pix.yaml POST /v1/claims/{claimId}/conclusion Finalize and complete the claim process. # Confirm claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/confirm-claim en/openapi/v3-current/pix.yaml POST /v1/claims/{claimId}/confirmation Confirm ownership claim request. # Confirm key Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/confirm-key en/openapi/v3-current/pix.yaml POST /v1/entries/{entryId}/confirmation Confirm Pix key ownership using a validation token (typically sent via SMS or email). This endpoint is used when a key is in status 0 (FINALIZED) or 2 (WAITING_OWNERSHIP_CONFIRMATION) and needs to be activated. Upon successful confirmation, the key status changes to 1 (ACTIVE). # Create claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-claim en/openapi/v3-current/pix.yaml POST /v1/claims Initiate a portability or ownership claim to take control of a Pix key currently registered to another account. The claim status will initially be "OPEN" and must go through a confirmation process. The key status for the claimer will typically be 4 (IM_CLAIMER_PENDING_CONFIRM_CLAIM_REQUEST). # Create dynamic QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-dynamic-qr-code en/openapi/v3-current/pix.yaml POST /v1/qrcodes/dynamic Generate a single-use, dynamic, Pix QR code with advanced features like expiration, payer validation, and additional payment information. Ideal for invoicing and controlled payments. # Create key Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-key en/openapi/v3-current/pix.yaml POST /v1/entries Register a new Pix key for an account. Specify the key type (keyType: 0=CPF, 1=CNPJ, 2=EMAIL, 3=PHONE, 4=EVP). For EVP (type 4), the key value is automatically generated and can be omitted. The response returns the numeric status of the key (0=FINALIZED/pending confirmation, 1=ACTIVE, etc.). # Create transaction Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-transaction en/openapi/v3-current/pix.yaml POST /v1/transactions Initiate a new Pix transaction (payment). The transaction will initially have status PENDING and will transition to EXECUTED upon successful completion or to ERROR/REPROVED if it fails. The priority field must be numeric: 1=PRIMARY (high priority) or 2=SECONDARY (normal priority). # Decode a Pix QR code via its payload URL Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/decode-a-pix-qr-code-via-its-payload-url en/openapi/v3-current/pix.yaml POST /v1/qrcodes/decode/url Decodes a dynamic Pix QR code by the payee-published JWS payload URL, normalizing static / immediate-dynamic / cobv sub-types into one null-safe response. Unlike the scan decode it takes a urlPayloadJson the upstream fetches; the payer document is sourced from the CRM alias for the upstream decode header. # Decode QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/decode-qr-code en/openapi/v3-current/pix.yaml POST /v1/qrcodes/decode Decode and extract payment information from a Pix QR code string. Validates format, structure, and expiration status. # Delete key Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/delete-key en/openapi/v3-current/pix.yaml DELETE /v1/entries/{entryId} Remove a Pix key registration from an account. This permanently deletes the key from the Pix directory. Only keys in certain statuses can be deleted (typically status 1=ACTIVE or 0=FINALIZED). # List claims Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-claims en/openapi/v3-current/pix.yaml GET /v1/claims Retrieve all claims associated with an account. Optionally filter by claim status string values (OPEN, CONFIRMED, COMPLETED, CANCELLED, AWAITING_RESOLUTION). # List keys Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-keys en/openapi/v3-current/pix.yaml GET /v1/entries List all Pix keys registered to an account. Returns an array of Pix key entries, each with its current status code (see KeyStatusEnum for values: -1=CANCELLED, 0=FINALIZED, 1=ACTIVE, etc.). # List the PSP's Pix QR codes Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-the-psp-s-pix-qr-codes en/openapi/v3-current/pix.yaml GET /v1/qrcodes/list Lists the Pix QR codes the PSP created, filtered by type and a registration window (≤90 days). Returns the static and immediate-dynamic blocks; composite / cobv / recurrence QR codes are out of this context's scope and not projected. # List transactions Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-transactions en/openapi/v3-current/pix.yaml GET /v1/transactions Retrieve Pix transactions with optional filtering. You can filter by multiple criteria including account, status, type, and timestamps. # Register a holder-ownership removal Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-a-holder-ownership-removal en/openapi/v3-current/pix.yaml POST /v1/holder-removals Severs a holder's ownership of a transactional account, making the DICT bulk-delete every Pix key bound to that holder and account. Returns the per-key deletion outcomes. # Register a transactional account closure Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-a-transactional-account-closure en/openapi/v3-current/pix.yaml POST /v1/account-closures Registers a transactional-account closure, making the DICT bulk-delete every Pix key bound to that account. Returns the per-key deletion outcomes. # Retrieve available limits Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/retrieve-available-limits en/openapi/v3-current/pix.yaml GET /v1/limits/available Check available transaction limits with current usage. # Retrieve claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/retrieve-claim en/openapi/v3-current/pix.yaml GET /v1/claims/{claimId} Retrieve details of a specific claim. # Retrieve key Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/retrieve-key en/openapi/v3-current/pix.yaml GET /v1/entries/{entryId}/details Get detailed information about a Pix key including owner details. # Retrieve transaction limits Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/retrieve-transaction-limits en/openapi/v3-current/pix.yaml GET /v1/limits Retrieve all configured transaction limits for an account. Limits are defined by: - periodType: DAILY, NIGHTLY, MONTHLY, or YEARLY - limitType: DEBIT (outgoing) or CREDIT (incoming) # Retrieve transaction status Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/retrieve-transaction-status en/openapi/v3-current/pix.yaml GET /v1/transactions/{transactionId} Retrieve detailed information about a specific transaction. # Update an immediate dynamic Pix QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/update-an-immediate-dynamic-pix-qr-code en/openapi/v3-current/pix.yaml PUT /v1/qrcodes/dynamic/{documentId} Updates an existing immediate dynamic Pix QR code: the charge amount, conciliation, expiration, optional payer, and the charge status (0=Active, 1=Concluded, 2=Removed by recipient, 3=Removed by PSP). The recipient name + city are CRM-sourced (never the body). The service re-hosts the updated signed JWS so the QR stays end-to-end resolvable; the response carries the upstream revision and the resolvable payload URL. # Update transaction limit Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/update-transaction-limit en/openapi/v3-current/pix.yaml PATCH /v1/limits Modify transaction limits for an account. Specify the period type (DAILY, NIGHTLY, MONTHLY, YEARLY) and limit type (DEBIT for outgoing or CREDIT for incoming) to update the corresponding limit amount. # Verify Pix key existence Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/verify-pix-key-existence en/openapi/v3-current/pix.yaml POST /v1/entries/verification Bulk-checks whether each supplied Pix key exists in the DICT directory (up to 200 keys per request). # Create payload location Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/create-location en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/loc Creates a location (loc) for a cob- or cobv-type charge. Pix API POST /loc. # Create recurrence location Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/create-location-rec en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/locrec Creates a recurrence location (locrec). The request has no body. Pix API POST /locrec. # Decode BR Code Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/decode-br-code en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/decode Decodes a raw BR Code string into its structured payload components. # Unlink recurrence from a location Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/delete-location-rec-id-rec en/openapi/v3-current/spi-brcode.yaml delete /api/v1/brcode/locrec/{id}/idRec Unlinks the recurrence (idRec) from a recurrence location. Pix API DELETE /locrec/{id}/idRec. # Unlink charge from a location Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/delete-location-tx-id en/openapi/v3-current/spi-brcode.yaml delete /api/v1/brcode/loc/{id}/txid Unlinks the charge (txid) from a location. The charge loses its location; the status remains unchanged. Pix API DELETE /loc/{id}/txid. # Generate BR Code Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/generate-br-code en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/ Prerequisite: for a dynamic QR, the cob/cobv charge must be created first (see putCob, putCobV) — the payload links to it by txid/locator. Generates a new BR Code (QR code) payload following the EMV QRCPS-MPM standard. # Get BR Code info Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-br-code-info en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/info/{payload} Returns structured metadata for a raw BR Code payload. # BR Code payload JWK Set Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-br-code-jwks en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/.well-known/jwks.json Publishes the recebedor PSP's JWK Set (RFC 7517). A payer PSP resolves the JWS verification key for a public cob/cobv/rec payload by matching the payload's JWS header kid to an entry kid here. Returns 404 when JOSE payload signing is not configured. # Get BR Code payload Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-br-code-payload en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/{id} Retrieves a single BR Code payload by its UUID. # Retrieve immediate charge Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-cob en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/cob/{txid} Retrieves an immediate charge by its txid. Pix API v2.9 GET /cob/{txid}. # Retrieve recurring charge Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-cob-r en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/cobr/{txid} Retrieves a recurring charge by its txid. Pix API GET /cobr/{txid}. # Retrieve due-date charge Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-cob-v en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/cobv/{txid} Retrieves a due-date charge by its txid. Pix API v2.9 GET /cobv/{txid}. # Retrieve location Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-location en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/loc/{id} Retrieves a location by its id. Pix API GET /loc/{id}. # Retrieve recurrence location Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-location-rec en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/locrec/{id} Retrieves a recurrence location by its id. Pix API GET /locrec/{id}. # Retrieve due-date charge batch Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-lote-cob-v en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/lotecobv/{id} Retrieves a due-date charge batch by its id. Pix API GET /lotecobv/{id}. # Retrieve recurrence (Pix Automático) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-rec en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/rec/{idRec} Retrieves a recurrence by its idRec. Pix API GET /rec/{idRec}. Projects the stored Rec fields verbatim (D-4), including recebedor.convenio. The dadosQR.jornada/pixCopiaECola and loc (Composite QR) fields are not included in this response. An unknown idRec returns 404 (canonical Problem envelope). # Get recebedor profile Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-recebedor-profile en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/recebedores/{chave} Returns the FI-registered recebedor identity for a Pix key, or 404 when no profile is registered. # Retrieve recurrence request (Pix Automático) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/get-solic-rec en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/solicrec/{idSolicRec} Retrieves a request by its idSolicRec. Pix API GET /solicrec/{idSolicRec}. Projects the stored fields verbatim (D-4). The atualizacao[] (status history) and recPayload fields are not included in this response. An unknown idSolicRec returns 404 (canonical Problem envelope). # List BR Code payloads Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-br-code-payloads en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/ Returns a paginated list of BR Code payloads. # List immediate charges Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-cob en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/cob Lists immediate charges by start date, end date, cpf, cnpj, locationPresente, and status. Pix API v2.9 GET /cob. # List recurring charges Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-cob-r en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/cobr Lists recurring charges by start date, end date, idRec, and status. Pix API GET /cobr. # List due-date charges Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-cob-v en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/cobv Lists due-date charges by start date, end date, cpf, cnpj, locationPresente, status, and loteCobVId. Pix API v2.9 GET /cobv. # List locations Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-location en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/loc Lists locations by start date, end date, txIdPresente, and tipoCob. Pix API GET /loc. # List recurrence locations Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-location-rec en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/locrec Lists recurrence locations by start date, end date, and idRecPresente. Pix API GET /locrec. # List due-date charge batches Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-lote-cob-v en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/lotecobv Lists due-date charge batches by start date and end date. Pix API GET /lotecobv. # List recurrences (Pix Automático) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/list-rec en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/rec Lists recurrences by start date, end date, cpf, cnpj, convênio, and status. Pix API GET /rec. # Revise immediate charge Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/patch-cob en/openapi/v3-current/spi-brcode.yaml patch /api/v1/brcode/cob/{txid} Revises an immediate charge. status=REMOVIDA_PELO_USUARIO_RECEBEDOR removes the charge. Pix API v2.9 PATCH /cob/{txid}. # Revise recurring charge (cancel) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/patch-cob-r en/openapi/v3-current/spi-brcode.yaml patch /api/v1/brcode/cobr/{txid} Revises a recurring charge. status=CANCELADA cancels the charge (only before the first settlement attempt). Pix API PATCH /cobr/{txid}. # Revise due-date charge Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/patch-cob-v en/openapi/v3-current/spi-brcode.yaml patch /api/v1/brcode/cobv/{txid} Revises a due-date charge. status=REMOVIDA_PELO_USUARIO_RECEBEDOR removes the charge. Pix API v2.9 PATCH /cobv/{txid}. # Revise due-date charge batch Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/patch-lote-cob-v en/openapi/v3-current/spi-brcode.yaml patch /api/v1/brcode/lotecobv/{id} Revises specific charges within a batch. The cobsv array may contain fewer items than the originating batch, but it cannot add or remove charges. Pix API PATCH /lotecobv/{id}. # Revise recurrence (Pix Automático) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/patch-rec en/openapi/v3-current/spi-brcode.yaml patch /api/v1/brcode/rec/{idRec} Partially revises a recurrence (Rec) by its idRec: changes calendario.dataInicial (only while status == CRIADA) and/or the status (CANCELADA cancels the mandate). Pix API PATCH /rec/{idRec}. Rejects any change when the recurrence is EXPIRADA/CANCELADA/REJEITADA (422). Changes to rec.loc are not accepted on this endpoint. An unknown idRec returns 404 (canonical Problem envelope). # Revise (cancel) recurrence request (Pix Automático) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/patch-solic-rec en/openapi/v3-current/spi-brcode.yaml patch /api/v1/brcode/solicrec/{idSolicRec} Cancels a request by its idSolicRec (target status CANCELADA). Pix API PATCH /solicrec/{idSolicRec}. # Create immediate charge (PSP-generated txid) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/post-cob en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/cob Prerequisite: create this charge before generating its QR/payload (see generateBRCode and resolveCobPayload). Creates an immediate charge (Cob) with a server-generated txid. Pix API v2.9 POST /cob. # Create recurring charge (PSP-generated txid) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/post-cob-r en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/cobr Creates a recurring charge (CobR) with a server-generated txid. Pix API POST /cobr. # Request settlement retry Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/post-cob-r-retentativa en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/cobr/{txid}/retentativa/{data} Requests a new settlement attempt for the given date. Requires the PERMITE_3R_7D policy. Pix API POST /cobr/{txid}/retentativa/{data}. # Create recurrence (Pix Automático) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/post-rec en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/rec Creates a recebedor-side recurrence (Rec) with a server-generated idRec. Pix API POST /rec. # Create recurrence request (Pix Automático) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/post-solic-rec en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/solicrec Creates a recurrence confirmation request (SolicRec) with a server-generated idSolicRec. Pix API POST /solicrec. # Create immediate charge (idempotent txid) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/put-cob en/openapi/v3-current/spi-brcode.yaml put /api/v1/brcode/cob/{txid} Creates an immediate charge (Cob) identified by the path txid (idempotent). Pix API v2.9 PUT /cob/{txid}. # Create recurring charge (idempotent txid) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/put-cob-r en/openapi/v3-current/spi-brcode.yaml put /api/v1/brcode/cobr/{txid} Creates a recurring charge (CobR) identified by the path txid (idempotent). Pix API PUT /cobr/{txid}. # Create due-date charge (idempotent txid) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/put-cob-v en/openapi/v3-current/spi-brcode.yaml put /api/v1/brcode/cobv/{txid} Prerequisite: create this charge before generating its QR/payload (see generateBRCode and resolveCobVPayload). Creates a due-date charge (CobV) identified by the path txid (idempotent). Pix API v2.9 PUT /cobv/{txid}. # Create due-date charge batch Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/put-lote-cob-v en/openapi/v3-current/spi-brcode.yaml put /api/v1/brcode/lotecobv/{id} Prerequisite: create the batch charges before generating their QR/payloads (see generateBRCode and resolveCobVPayload). Creates a due-date charge batch identified by the path id. Items are processed synchronously: each results in CRIADA or NEGADA. Pix API PUT /lotecobv/{id}. # Reconcile charge Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/reconcile-charge en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/charges/reconcile Admin-only endpoint that marks a charge as paid by its transaction ID, recording the payment timestamp. # Resolve Cob payload (JWS) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/resolve-cob-payload en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/payload/{locator} Prerequisite: the referenced Cob/CobV charge must already exist (see postCob and putCobV). Resolves, by the opaque access token, the signed JSON payload (JWS, application/jose) of an immediate charge (CobPayload). valor.original is projected verbatim. The verification key is published at /.well-known/jwks.json. # Resolve CobV payload (JWS) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/resolve-cob-v-payload en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/payload/cobv/{locator} Resolves, by the opaque access token, the signed JSON payload (JWS, application/jose) of a due-date charge (CobVPayload). valor.final is calculated (fine/interest/rebate/discount, Annex III §4) for the expected payment date (dpp). The verification key is published at /.well-known/jwks.json. # Resolve Rec payload (JWS) Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/resolve-rec-payload en/openapi/v3-current/spi-brcode.yaml get /api/v1/brcode/payload/rec/{idRec} Resolves, by the idRec, the signed JSON payload (JWS, application/jose) of a recurrence (RecPayload). The recurrence configuration is projected verbatim. A terminal recurrence (REJEITADA/EXPIRADA/CANCELADA) returns 410 Gone (location permanently exhausted, §RecPayloadNaoEncontrado). The verification key is published at /.well-known/jwks.json. # Register recebedor profile Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/upsert-recebedor-profile en/openapi/v3-current/spi-brcode.yaml put /api/v1/brcode/recebedores/{chave} FI-admin surface (not a BACEN endpoint): upsert the FI-registered recebedor identity for a Pix key. CobV responses require a recebedor with a postal address (DadosRecebedor), which DICT cannot supply; the FI registers it here. Idempotent by nature. # Validate BR Code Source: https://docs.lerian.studio/en/reference/rails/spi/brcode/validate-br-code en/openapi/v3-current/spi-brcode.yaml post /api/v1/brcode/validate Validates a raw BR Code payload and returns stable EMV validation codes. # Activate participant Source: https://docs.lerian.studio/en/reference/rails/spi/core/activate-participant en/openapi/v3-current/spi-core.yaml post /api/v1/core/participants/{ispb}/activate Reactivates a suspended participant. # Analyze MED case Source: https://docs.lerian.studio/en/reference/rails/spi/core/analyze-med-case en/openapi/v3-current/spi-core.yaml post /api/v1/core/med/{caseID}/analyze Transitions a MED case from INITIATED to UNDER_ANALYSIS. # Attach evidence to MED case Source: https://docs.lerian.studio/en/reference/rails/spi/core/attach-med-case-evidence en/openapi/v3-current/spi-core.yaml post /api/v1/core/med/{caseID}/evidence Attaches an evidence reference (URL, text, or document reference) to a MED case. # Cancel MED case Source: https://docs.lerian.studio/en/reference/rails/spi/core/cancel-med-case en/openapi/v3-current/spi-core.yaml post /api/v1/core/med/{caseID}/cancel Cancels a MED case before it reaches COMPLETED state. # Close MED case Source: https://docs.lerian.studio/en/reference/rails/spi/core/close-med-case en/openapi/v3-current/spi-core.yaml post /api/v1/core/med/{caseID}/close Closes a resolved MED case (APPROVED, REJECTED, or EXPIRED -> COMPLETED). # Confirm portability relationship end (reda.031) Source: https://docs.lerian.studio/en/reference/rails/spi/core/confirm-portability-relationship-end en/openapi/v3-current/spi-core.yaml post /api/v1/spi/participants/{ispb}/portability/confirm Confirms the end of a portability relationship for an indirect participant and dispatches the reda.031 (PtyDeltnReq) message to the BACEN. Applies only to indirect participants; a direct participant is rejected. It does not enforce the open reda.017 confirmation window — the 24-hour PRAZOCONFI deadline is the operator's responsibility, not a precondition checked here. # Get MED case Source: https://docs.lerian.studio/en/reference/rails/spi/core/get-med-case en/openapi/v3-current/spi-core.yaml get /api/v1/core/med/{caseID} Retrieves a single MED case by its case ID. # Get participant Source: https://docs.lerian.studio/en/reference/rails/spi/core/get-participant en/openapi/v3-current/spi-core.yaml get /api/v1/core/participants/{ispb} Retrieves a single participant by their 8-digit ISPB code. # List MED cases Source: https://docs.lerian.studio/en/reference/rails/spi/core/list-med-cases en/openapi/v3-current/spi-core.yaml get /api/v1/core/med/ Returns a paginated list of MED cases. # List participants Source: https://docs.lerian.studio/en/reference/rails/spi/core/list-participants en/openapi/v3-current/spi-core.yaml get /api/v1/core/participants/ Returns a paginated list of participants; filterable by sponsor ISPB. # Offboard participant Source: https://docs.lerian.studio/en/reference/rails/spi/core/offboard-participant en/openapi/v3-current/spi-core.yaml post /api/v1/core/participants/{ispb}/offboard Permanently offboards a participant (terminal state). # Open MED case Source: https://docs.lerian.studio/en/reference/rails/spi/core/open-med-case en/openapi/v3-current/spi-core.yaml post /api/v1/core/med/ Opens a new MED (Mecanismo Especial de Devolução) dispute case for a PIX payment. # Orchestrate Core operation Source: https://docs.lerian.studio/en/reference/rails/spi/core/orchestrate-operation en/openapi/v3-current/spi-core.yaml post /api/v1/core/operations/ Resolves participant routing, consumes quota when indirect, records settlement tracking, attributes fees/commissions, persists saga state, and emits Core domain events. # Register participant Source: https://docs.lerian.studio/en/reference/rails/spi/core/register-participant en/openapi/v3-current/spi-core.yaml post /api/v1/core/participants/ Registers a new direct or indirect participant. # Resolve MED case Source: https://docs.lerian.studio/en/reference/rails/spi/core/resolve-med-case en/openapi/v3-current/spi-core.yaml post /api/v1/core/med/{caseID}/resolve Resolves a MED case by approving or rejecting it. # Resolve payment routing Source: https://docs.lerian.studio/en/reference/rails/spi/core/resolve-routing en/openapi/v3-current/spi-core.yaml post /api/v1/core/routing/resolve Resolves the routing path for a payment based on the sender's ISPB and amount. # Suspend participant Source: https://docs.lerian.studio/en/reference/rails/spi/core/suspend-participant en/openapi/v3-current/spi-core.yaml post /api/v1/core/participants/{ispb}/suspend Suspends an active participant. # Cancel DICT refund Source: https://docs.lerian.studio/en/reference/rails/spi/dict/cancel-refund en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/refunds/{id}/cancel Transitions a refund REQUESTED→CANCELLED (payer side, while still open) and reports it to BACEN. # Close DICT refund Source: https://docs.lerian.studio/en/reference/rails/spi/dict/close-refund en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/refunds/{id}/close Transitions a refund REQUESTED→ACCEPTED|REJECTED with an analysis and reports it to BACEN. # Create DICT refund Source: https://docs.lerian.studio/en/reference/rails/spi/dict/create-refund en/openapi/v3-current/spi-dict.yaml post /api/v1/dict/refunds Persists a DICT refund request locally and submits the corresponding BACEN refund synchronously-after-persist, tracked by report_status (PENDING/SENT/FAILED) + request_id. # DICT claims-summary report (COUNT-only) Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-dict-report-claims-summary en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/reports/claims-summary Returns COUNT(*) buckets grouped by (claimType, status) over claims requested in a mandatory date window (max 92 days), plus the COUNT of currently-open claims past their 7-day deadline. COUNT-only. Infraction summaries are out of scope. # DICT keys-summary report (COUNT-only) Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-dict-report-keys-summary en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/reports/keys-summary Returns COUNT(*) buckets grouped by (keyType, status) over PIX keys registered in a mandatory date window (max 92 days). COUNT-only — DICT holds no money. Raw key values are never returned. # Get DICT refund Source: https://docs.lerian.studio/en/reference/rails/spi/dict/get-refund en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/refunds/{id} Retrieves a local DICT refund record by UUID. # PIX key statistics Source: https://docs.lerian.studio/en/reference/rails/spi/dict/key-stats en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/keys/stats Returns PIX key counts grouped by key type and status. # List DICT refunds Source: https://docs.lerian.studio/en/reference/rails/spi/dict/list-refunds en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/refunds Returns paginated local DICT refund records. # Search PIX keys Source: https://docs.lerian.studio/en/reference/rails/spi/dict/search-keys en/openapi/v3-current/spi-dict.yaml get /api/v1/dict/keys/search Searches PIX keys by optional key type, status, and participant ISPB filters. # Update PIX key Source: https://docs.lerian.studio/en/reference/rails/spi/dict/update-key en/openapi/v3-current/spi-dict.yaml put /api/v1/dict/keys/{key} Updates a PIX key's mutable bindings (account and owner name/trade name) and reports the change to BACEN DICT (signed updateEntry). keyType and ownerTaxId are immutable; an attempt to change them is rejected. # Acknowledge a Claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/acknowledge-a-claim en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/claims/{claim_id}/acknowledge Use this endpoint to acknowledge receipt of a claim by the donor participant. This operation notifies that the donor participant has received the claim and moves it from OPEN to WAITING_RESOLUTION status. **Notes:** - This operation is performed by the donor participant (the participant that currently owns the key). - Acknowledging a claim starts the resolution period (D+7) for the donor to respond. - Claims in non-OPEN status can be acknowledged if the donor participant matches our ISPB. This operation is idempotent. # Acknowledge an Infraction Report Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/acknowledge-an-infraction-report en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/infraction-reports/{infraction_id}/acknowledge Use this endpoint to acknowledge receipt of an infraction report notification. This endpoint is used by the counterparty participant to acknowledge that they have received and are aware of the infraction report filed against a transaction. **Notes:** - The report must be in OPEN status to be acknowledged. - This operation is idempotent. # Cancel a Claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/cancel-a-claim en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/claims/{claim_id}/cancel Use this endpoint to cancel a portability or ownership claim. **Notes:** - For portability: Status must be OPEN or WAITING_RESOLUTION. If the cancellation reason is DEFAULT_OPERATION, the period defined by the ResolutionPeriodEnd field must have passed. - For ownership: Status must be OPEN or WAITING_RESOLUTION. If the cancellation reason is DEFAULT_OPERATION, the period defined by the ResolutionPeriodEnd field must have passed. # Cancel a Fraud Marker Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/cancel-a-fraud-marker en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/fraud-markers/{fraud_marker_id}/cancel Use this endpoint to cancel a previously registered fraud marker. **Notes:** - Cancellation is only allowed for markers in REGISTERED status. - Only the participant who created the fraud marker can cancel it. - For fraud markers created from infraction report closures, the counterparty participant who closed the infraction can cancel the marker. - Cancelled markers cannot be reactivated. # Cancel a Refund Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/cancel-a-refund-request en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/refund-requests/{refund_id}/cancel Use this endpoint to cancel a refund request before it is processed. **Notes:** - Cancellation is only allowed for requests in OPEN status. - Cancelled requests cannot be reopened or modified. - This operation notifies BACEN and the contested participant. # Cancel an Infraction Report Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/cancel-an-infraction-report en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/infraction-reports/{infraction_id}/cancel Use this endpoint to cancel an infraction report that is no longer valid. This endpoint is used to cancel infractions reported by your own PSP. **Notes:** - Cancellation is only allowed for reports in OPEN or ACKNOWLEDGED status. - Cancelled reports cannot be reopened or modified. - Cancellation reason should be provided for audit purposes. # Check Keys Existence Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/check-keys-existence en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/keys/check Use this endpoint to verify if the provided Pix keys exist in the DICT system. This operation allows batch verification of multiple keys in a single request. # Close a Refund Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/close-a-refund-request en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/refund-requests/{refund_id}/close Use this endpoint to provide the closure decision and outcome for a refund request. **Notes:** - This operation is typically triggered by BACEN's analysis completion. - Closing a request sets its status to CLOSED and records the analysis result. - For accepted refunds, a refund transaction ID is recorded. - Once closed, the request cannot be modified. # Close an Infraction Report Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/close-an-infraction-report en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/infraction-reports/{infraction_id}/close Use this endpoint to submit the closure analysis for an infraction report received. This endpoint must be used to respond to infractions reported to your PSP, meaning when the account was the receiver of the transaction reported as fraud, within 7 days with the analysis result. **Notes:** - Closing a report sets its status to CLOSED and records the analysis result. - Once closed, the report cannot be modified. - For `REFUND_REQUEST` infractions, the report must be closed by the payee's PSP. - For `REFUND_CANCELLED` infractions, the report must be closed by the payer's PSP. # Complete a Claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/complete-a-claim en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/claims/{claim_id}/complete Use this endpoint to complete a claim by the claimer. As a result, the link between the key and the claiming party is created. **Notes:** - For ownership claim, status must be CONFIRMED and deadline defined by field CompletionPeriodEnd must have passed. - For portability, status must be CONFIRMED. - This operation finalizes the key transfer and creates the entry in DICT. # Confirm a Claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/confirm-a-claim en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/claims/{claim_id}/confirm Use this endpoint to confirm a claim operation. As a consequence, the link between the key and the donor participant is removed. Status must be in WAITING_RESOLUTION. **Notes:** - For ownership claims, if the reason is DEFAULT_OPERATION, the resolution period (ResolutionPeriodEnd) must have passed. If the reason given is USER_REQUESTED, the closing period (CompletionPeriodEnd) will be advanced to allow for immediate termination by the claimant. # Create a Claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-a-claim en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/claims Use this endpoint to create a new key claim for portability or ownership. Not every type of key can be claimed or ported. The table below defines the possibilities: | **compatible?** | **OWNERSHIP** | **PORTABILITY** | |-------------------|---------------|-----------------| | CPF | | Yes | | CNPJ | | Yes | | PHONE | Yes | Yes | | EMAIL | | Yes | | EVP | | | **Notes:** - OWNERSHIP claims are only allowed for PHONE key types. - PORTABILITY claims are allowed for CPF, CNPJ, PHONE, and EMAIL key types. - EVP keys cannot be claimed or ported. - The claimer account information is automatically retrieved from CRM using the X-Account-Id header. - The claim enters OPEN status upon creation and awaits acknowledgment from the donor participant. # Create a Fraud Marker Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-a-fraud-marker en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/fraud-markers Use this endpoint to register a fraud marker for a tax ID or Pix key. **Notes:** - Fraud markers are used to flag documents (CPF/CNPJ) or Pix keys associated with fraudulent activity. - The taxIdNumber field is required and represents the person/entity being marked. - The key field is optional and can be used to associate the marker with a specific Pix key. - Fraud markers can also be automatically created when closing an infraction report with AGREED result. # Create a Refund Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-a-refund-request en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/refund-requests Use this endpoint to open a refund request for a Pix transaction. **Notes:** - Refund requests must be associated with a valid Pix transaction. - Requests can optionally be linked to an infraction report. - The contested participant must respond within the specified timeframe. # Create an Infraction Report Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-an-infraction-report en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/infraction-reports Use this endpoint to report infractions associated to Pix transfers. This endpoint must be used when there is a fraud related to a transaction in scenarios where the client is the payer PSP, meaning the transaction was initiated and sent from an account within your arrangement. **Notes:** - Transactions can only be reported within 90 days from the transaction date. # Create an Entry Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-entry en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/entries Use this endpoint to create a new Pix entry associated with an account and its data. This service does not perform the following validations and assumes they have been completed by the client before making the request: - **Receita Federal Validation**: The service does not validate data with Receita Federal. It is assumed that the client has already performed the necessary validation with a properly contracted provider integrated to RFB APIs. - **MFA Ownership Authentication**: The service assumes that Multi-Factor Authentication (MFA) for ownership verification has been previously completed before the request is made. # Delete an Entry Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/delete-an-entry en/openapi/v3-current/indirect-pix.yaml DELETE /v1/dict/entries/{entry_id} Use this endpoint to delete (inactivate) an existing Pix entry associated with an account. **Notes:** - This operation performs a logical deletion, changing the entry status to INACTIVE rather than physically removing it from the system. # List Claims Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-claims en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/claims Use this endpoint to list key claims with filters and pagination. Returns empty array [] if no claims match the criteria. # List Entries Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-entries en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/entries Use this endpoint to list all Pix entries for an account with optional filters. Returns empty array [] if no entries match the criteria. # List Fraud Markers Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-fraud-markers en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/fraud-markers Use this endpoint to list fraud markers with filters. Returns empty array [] if no markers match the criteria. **Notes:** - This endpoint is a proxy to BTG and does not persist data locally. - Uses cursor-based pagination via modified_after/modified_before filters. - When hasMoreElements is true, use the updatedAt of the last item as modified_after for the next request. # List Infraction Reports Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-infraction-reports en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/infraction-reports Use this endpoint to list infraction reports with optional filters and pagination. # List Refund Requests Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-refund-requests en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/refund-requests Use this endpoint to list refund requests with filters and pagination. Returns empty array [] if no requests match the criteria. # Retrieve a Claim Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-a-claim en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/claims/{claim_id} Use this endpoint to retrieve the details of a specific key claim. # Retrieve a Key Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-a-key en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/keys/{key} Use this endpoint to retrieve Pix key details for payment purposes. This operation queries the DICT system to get account information associated with a Pix key, enabling you to validate and prepare payment destinations. # Retrieve a Refund Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-a-refund-request en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/refund-requests/{refund_id} Use this endpoint to retrieve the details of a specific refund request. # Retrieve an Entry Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-an-entry en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/entries/{entry_id} Use this endpoint to retrieve a Pix entry by its unique identifier. # Retrieve an Infraction Report Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-an-infraction-report en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/infraction-reports/{infraction_id} Use this endpoint to retrieve the full details of a single infraction report. # Retrieve Key Statistics Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-key-statistics en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/statistics/keys/{key} Use this endpoint to fetch usage and risk statistics for a specific Pix key. **Notes:** - Returns both key-level statistics and owner-level statistics in a single response. - Key statistics track data specific to the Pix key as an entity independent of its current owner. - Owner statistics provide the same data as the person statistics endpoint for the key's current owner. - This endpoint queries the provider directly and does not store data locally. - Use this information for comprehensive risk assessment when processing payments to a Pix key. # Update an Entry Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/update-an-entry en/openapi/v3-current/indirect-pix.yaml PATCH /v1/dict/entries/{entry_id} Use this endpoint to update an existing Pix entry data associated with an account. **Notes:** - Only the account information, name, and trade name can be changed. - EVP keys (random) - Changes are allowed with the following reasons: BRANCH_TRANSFER, RECONCILIATION, and RFB_VALIDATION. # Amend a MED value recovery Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/amend-a-med-value-recovery en/openapi/v3-current/pix.yaml PUT /v1/med/recoveries/{idRecValores}/amend Records a field-only update to a value recovery. It NEVER changes status. The invariants are validated before any upstream call. An unknown id returns a coded 404. # Analyze a MED infraction Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/analyze-a-med-infraction en/openapi/v3-current/pix.yaml POST /v1/med/infractions/{idRelatoInfracao}/analyze Records the counterparty PSP's analysis result. The invariants (resultadoAnalise {0=Aceita,1=Rejeitada}, tpFraude {0..4} required when Aceita) are validated before any upstream call. Persists the upstream response entity (the advanced watermark + minted idMarcacaoFraude) and returns it. # Analyze a MED refund request Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/analyze-a-med-refund-request en/openapi/v3-current/pix.yaml POST /v1/med/refunds/{idSolDevolucao}/analyze Records the contestado PSP's analysis result. The conditional-required invariants are validated before any upstream call. Persists the upstream response entity (the advanced watermark) and returns it. # Cancel a MED fraud marker Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/cancel-a-med-fraud-marker en/openapi/v3-current/pix.yaml POST /v1/med/fraud-markers/{idMarcacaoFraude}/cancel Cancels a fraud marker as the creator PSP. An unknown id returns a coded 404. # Cancel a MED infraction Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/cancel-a-med-infraction en/openapi/v3-current/pix.yaml POST /v1/med/infractions/{idRelatoInfracao}/cancel Cancels an infraction as the creator PSP. Cancel is allowed at any time, even after Analisado. An unknown id returns a coded 404. Persists the upstream response entity and returns it. # Cancel a MED refund request Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/cancel-a-med-refund-request en/openapi/v3-current/pix.yaml POST /v1/med/refunds/{idSolDevolucao}/cancel Cancels a not-yet-Analisada refund request as the solicitante PSP. An unknown id returns a coded 404. Persists the upstream response entity and returns it. # Cancel a Pix Automático authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/cancel-a-pix-automatico-authorization en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/{idRecorrencia}/cancel Cancels a recurring-payment authorization, transitioning it to Cancelled and soft-deleting the local row. Returns the cancellation id (idCancelamento). # Cancel a Pix Automático schedule Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/cancel-a-pix-automatico-schedule en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/schedules/{endToEndId}/cancel Cancels a scheduled payment instruction, transitioning it to Cancelled and soft-deleting the local row. Returns the cancellation id (idCancelamento). # Confirm a Pix Automático authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/confirm-a-pix-automatico-authorization en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/{idRecorrencia}/confirm Confirms a recurring-payment authorization via QR journeys 2-4, transitioning it to Active (the live recurring-payment mandate). # Consult a Pix Automático authorization cancellation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/consult-a-pix-automatico-authorization-cancellation en/openapi/v3-current/pix.yaml GET /v1/pix-automatico/authorizations/{idRecorrencia}/cancel/{idCancelamento} Consults the live upstream situation of an authorization cancellation, keyed by (idRecorrencia, idCancelamento). # Consult a Pix Automático authorization situation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/consult-a-pix-automatico-authorization-situation en/openapi/v3-current/pix.yaml GET /v1/pix-automatico/authorizations/{idRecorrencia} Consults the live upstream situation of a recurring-payment authorization, projecting the upstream situation axes onto the local authorization status. # Consult a Pix Automático schedule cancellation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/consult-a-pix-automatico-schedule-cancellation en/openapi/v3-current/pix.yaml GET /v1/pix-automatico/schedules/{endToEndId}/cancel/{idCancelamento} Consults the live upstream situation of a schedule cancellation, keyed by (endToEndId, idCancelamento). # Consult a Pix Automático schedule situation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/consult-a-pix-automatico-schedule-situation en/openapi/v3-current/pix.yaml GET /v1/pix-automatico/schedules/{endToEndId} Consults the live upstream situation of a scheduled payment instruction, projecting the upstream situation axes onto the local schedule status. # Create a CobV (due-date) composite Pix Automático QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-a-cobv-due-date-composite-pix-automatico-qr-code en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/composite/cobv Generates a composite Pix QR code embedding due-date dynamic-QR data (cobrança com vencimento) and a recurring-payment authorization (Pix Automático). The recipient name and city are CRM-sourced from the account. # Create a MED value recovery Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-a-med-value-recovery en/openapi/v3-current/pix.yaml POST /v1/med/recoveries Opens a value recovery as the creator PSP. The creator ISPB is derived from configuration. grafoRastreamento.vlrMinTransacaoCents is a graph-traversal FILTER threshold, never a posting amount. # Create a recurrence-only composite Pix Automático QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-a-recurrence-only-composite-pix-automatico-qr-code en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/composite/recurrence Generates a composite Pix QR code carrying ONLY a recurring-payment authorization (Pix Automático), with no static or dynamic QR data. The recipient city is CRM-sourced from the account. # Create a static composite Pix Automático QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-a-static-composite-pix-automatico-qr-code en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/composite/static Generates a composite Pix QR code embedding static-QR data and a recurring-payment authorization (Pix Automático). The recipient name and city are CRM-sourced from the account, never the request body. # Create an immediate dynamic composite Pix Automático QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-an-immediate-dynamic-composite-pix-automatico-qr-code en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/composite/dynamic Generates a composite Pix QR code embedding immediate-dynamic-QR data and a recurring-payment authorization (Pix Automático). The recipient name and city are CRM-sourced from the account. # Create refund Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-refund en/openapi/v3-current/pix.yaml POST /v1/refunds Request a refund for a completed Pix transaction. You must provide a valid BACEN refund reason code (FR01=Fraud, AC03=Invalid account, AG01=Forbidden, MD06=Requested by receiver, BE01=Inconsistent data, AC07=Closed account). The original transaction must be in EXECUTED status to be eligible for refund. # Create static QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/create-static-qr-code en/openapi/v3-current/pix.yaml POST /v1/qrcodes/static Generate a static Pix QR code with fixed amount. These QR codes can be reused multiple times and are ideal for fixed pricing scenarios like product labels or invoices. # Decode a composite Pix Automático QR code from its URL Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/decode-a-composite-pix-automatico-qr-code-from-its-url en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/composite/decode Decodes a composite Pix QR code from its embedded recurrence URL (urlPayloadJsonRec), returning the recurring-payment authorization configuration. # Effect an inbound Pix Automático settlement debit Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/effect-an-inbound-pix-automatico-settlement-debit en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/settlements/inbound/debit Posts the real double-entry settlement debit on the payer's local ledger for a scheduled Pix Automático payment. Upstream calls this after it confirms SPI settlement. The amount was established at schedule/block time, so the body carries no value. Returns the settlement-gateway debit-operation id (idOperacaoSgct). A re-delivery resolves to a clean 200 with the prior confirmation, never a double post. # Effectuate a MED refund debit Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/effectuate-a-med-refund-debit en/openapi/v3-current/pix.yaml POST /v1/med/refunds/{idSolDevolucao}/effectuate Effectuates the refund DEBIT at SPI for an accepted refund: posts a balanced PENDING Midaz double-entry (contestado debit, clearing credit) and generates the pacs.004. The debit commits on the settlement reconcile poll. The precondition (accepted AND not-yet-effectuated) is checked before any ledger touch; a non-accepted or already-effectuated request returns a coded 422 and a re-delivery is an idempotent no-double-post echo. An unknown id returns a coded 404. # Get a MED fraud marker Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/get-a-med-fraud-marker en/openapi/v3-current/pix.yaml GET /v1/med/fraud-markers/{idMarcacaoFraude} Returns a single fraud marker by its idMarcacaoFraude GUID, read live from the upstream system. An unknown id returns a coded 404. # Get a MED infraction Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/get-a-med-infraction en/openapi/v3-current/pix.yaml GET /v1/med/infractions/{idRelatoInfracao} Returns a single detected infraction by its idRelatoInfracao GUID from the local projection. An unknown id returns a coded 404. # Get a MED refund request Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/get-a-med-refund-request en/openapi/v3-current/pix.yaml GET /v1/med/refunds/{idSolDevolucao} Returns a single detected refund request by its idSolDevolucao GUID from the local projection. An unknown id returns a coded 404. # Get a MED value recovery Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/get-a-med-value-recovery en/openapi/v3-current/pix.yaml GET /v1/med/recoveries/{idRecValores} Returns a single value recovery by its idRecValores GUID, read live from the upstream system. An unknown id returns a coded 404. # List MED fraud markers (DICT-direct) Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-med-fraud-markers-dict-direct en/openapi/v3-current/pix.yaml GET /v1/med/fraud-markers/dict Lists the fraud markers for a document via the DICT-direct query. cpf_cnpj is required; chave and limit are optional filters. # List MED fraud markers (local base) Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-med-fraud-markers-local-base en/openapi/v3-current/pix.yaml GET /v1/med/fraud-markers Lists the fraud markers where this institution is the creator. Read live from the upstream system. Supply cpf_cnpj to switch to the DICT-direct listing instead. # List MED infractions Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-med-infractions en/openapi/v3-current/pix.yaml GET /v1/med/infractions Lists the infractions detected by the worker pollers (filed by this institution or against it), filterable by status, role ISPB, and the suspect payment's endToEndId. Read projection — no upstream round-trip. # List MED recoveries awaiting devolver Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-med-recoveries-awaiting-devolver en/openapi/v3-current/pix.yaml GET /v1/med/recoveries/awaiting-devolver Lists the creator-side Analisada value recoveries this institution may still trigger devolver on, ordered by devolver deadline (earliest first). The creator ISPB is config-derived. Each row surfaces devolverDeadline and a computed breached flag. Read projection — no upstream round-trip; no action taken and no money moved. # List MED refund requests Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-med-refund-requests en/openapi/v3-current/pix.yaml GET /v1/med/refunds Lists the refund requests detected by the worker pollers (requested by this institution or against it), filterable by status, role ISPB, and the contested payment's endToEndId. Read projection — no upstream round-trip. # List MED refund requests pending my analysis Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-med-refund-requests-pending-my-analysis en/openapi/v3-current/pix.yaml GET /v1/med/refunds/pending-analysis Lists the OPEN refund requests this institution must analyse (contested against it), ordered by analysis deadline (earliest first). The contested ISPB is config-derived. Each row surfaces analysisDeadline and a computed breached flag. Read projection — no upstream round-trip. # List Pix Automático authorizations Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-pix-automatico-authorizations en/openapi/v3-current/pix.yaml GET /v1/pix-automatico/authorizations Lists live upstream recurring-payment authorizations, scoped by flow + update window, projecting each onto the local authorization status. # List Pix Automático schedules Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-pix-automatico-schedules en/openapi/v3-current/pix.yaml GET /v1/pix-automatico/schedules/list Lists live upstream scheduled payment instructions, scoped by flow + due-date window, projecting each onto the local schedule status. The response is an object wrapper carrying the upstream timestamp and the schedule snapshots. # List refund reasons Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/list-refund-reasons en/openapi/v3-current/pix.yaml GET /v1/refunds/reasons Retrieve available refund reason codes. # Open a MED refund request Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/open-a-med-refund-request en/openapi/v3-current/pix.yaml POST /v1/med/refunds Opens a refund request as the solicitante PSP. The requesting ISPB is derived from configuration. Persists the upstream response entity and returns it. # Process a payee-bank-initiated Pix devolução Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/process-a-payee-bank-initiated-pix-devolu-o en/openapi/v3-current/pix.yaml POST /v1/webhooks/refunds Processes a payee-bank-initiated Pix devolução pushed by the upstream system; this endpoint does not initiate the return. Credits the original payer the returned amount, subject to a cumulative limit on returned amounts, dedups on the upstream-assigned return end-to-end id, and returns the settlement-gateway credit acknowledgement. # Process an inbound upstream cash-in Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/process-an-inbound-upstream-cash-in en/openapi/v3-current/pix.yaml POST /v1/webhooks/cash-ins Processes an inbound upstream cash-in: resolves the credit account by the receiver document, applies the Bacen (ISPB 99999) special-case handling, dedups on the end-to-end id, and credits the customer exactly-once (Midaz idempotency keyed on the end-to-end id). An unresolved account or a Bacen payer returns a success-shaped account-not-found acknowledgement with no credit. # Receive a Pix Automático authorization cancellation registration event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-authorization-cancellation-registration-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/authorization-cancellation-registered Receives the upstream push event: an async re-delivery of the cancellation registration. It reuses the inbound 5.2.5 register use case (Aceito cancels + soft-deletes; Rejeitado is ack-only). Idempotent: a re-delivery on an already-cancelled row is a no-op. No response body. # Receive a Pix Automático authorization cancellation status event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-authorization-cancellation-status-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/authorization-cancellation-status Receives the upstream push event. The pushed stCancelamento Aceito/Rejeitado outcome is projected onto the local authorization. Idempotent: a duplicate re-delivery is a clean 200 no-op. No response body. # Receive a Pix Automático authorization status event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-authorization-status-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/authorization-status Receives the upstream push event. The pushed upstream situation is projected onto the local authorization keyed on idRecorrencia. Idempotent: a duplicate re-delivery is a clean 200 no-op. No response body. # Receive a Pix Automático payer-side authorization registration event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-payer-side-authorization-registration-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/authorization-payer-registered Receives the upstream push event: an async re-delivery of the payer-side solicitation registration. It reuses the inbound 5.2.3 register use case (payer-side genesis). Idempotent: a re-delivered solicitation resolves to a 409 conflict, never a 500. No response body. # Receive a Pix Automático recipient-side authorization registration event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-recipient-side-authorization-registration-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/authorization-recipient-registered Receives the upstream push event: an async re-delivery of the recipient-side authorization registration. It reuses the inbound 5.2.4 register use case (load + transition to Accepted/Rejected). Idempotent: a re-delivery converging on the same status is a no-op. No response body. # Receive a Pix Automático schedule cancellation registration event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-schedule-cancellation-registration-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/schedule-cancellation-registered Receives the upstream push event: an async re-delivery of the schedule cancellation registration. It reuses the inbound 5.3.4 register use case (Aceito cancels + soft-deletes; Rejeitado is ack-only). Idempotent: a re-delivery on an already-cancelled row is a no-op. No response body. # Receive a Pix Automático schedule cancellation status event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-schedule-cancellation-status-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/schedule-cancellation-status Receives the upstream push event. The pushed stCancelamento Aceito/Rejeitado outcome is projected onto the local schedule. Idempotent: a duplicate re-delivery is a clean 200 no-op. No response body. # Receive a Pix Automático schedule registration event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-schedule-registration-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/schedule-registered Receives the upstream push event: an async re-delivery of the payer-side schedule registration. It reuses the inbound 5.3.3 register use case (inbound genesis). Idempotent: a re-delivered instruction resolves to a 409 conflict, never a 500. No response body. # Receive a Pix Automático schedule status event Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/receive-a-pix-automatico-schedule-status-event en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/events/schedule-status Receives the upstream push event. The pushed upstream situation is projected onto the local schedule keyed on endToEndId. Idempotent: a duplicate re-delivery is a clean 200 no-op. No response body. # Register a MED fraud marker Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-a-med-fraud-marker en/openapi/v3-current/pix.yaml POST /v1/med/fraud-markers Opens a fraud marker as the creator PSP. The creator ISPB is derived from configuration. tpFraude is validated against the create domain {0..3} before any upstream call. # Register a MED inbound refund credit Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-a-med-inbound-refund-credit en/openapi/v3-current/pix.yaml POST /sgct/jdpi/spi/api/v2/credito/devolucao Posts the receiving-side refund CREDIT on the local Midaz ledger when SPI settles a MED devolucao. It posts a balanced COMMITTED double-entry (clearing credit, recipient debit-mirror) and mints the idCreditoSgct. The Authorization (Bearer) header is mandatory on this financial ingress: a missing/invalid token is a coded 401. A re-delivered credit for the same endToEndIdDevolucao echoes the prior idCreditoSgct with no second post (idempotent). # Register a Pix Automático authorization validation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-a-pix-automatico-authorization-validation en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/{idRecorrencia}/validation Registers the deferred async validation reply for a recurring-payment authorization, the outbound reply to an inbound 5.2.1 that returned 202-async. # Register an inbound Pix Automático authorization on the PSP Recebedor Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-an-inbound-pix-automatico-authorization-on-the-psp-recebedor en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/inbound/recipient/register Registers the payer's authorization result delivered by upstream to the PSP Recebedor. It loads the existing recebedor-side authorization row and transitions it to Accepted or Rejected per resultadoAutorizacao, returning the local settlement-gateway id. # Register an inbound Pix Automático cancellation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-an-inbound-pix-automatico-cancellation en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/inbound/cancellation/register Registers a recurring-payment cancellation outcome delivered by the upstream system. An accepted cancellation (stCancelamento=0) transitions the authorization to Cancelled and soft-deletes the row; a rejected cancellation (stCancelamento=1) is acknowledged with no state change. Returns the local settlement-gateway id. # Register an inbound Pix Automático payment instruction Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-an-inbound-pix-automatico-payment-instruction en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/schedules/inbound/register Registers an inbound scheduled payment instruction delivered by upstream to the PSP Pagador. This is inbound genesis: it creates the payer-side schedule row (Requested on an accepted registration, Rejected on a rejected one) and returns the local settlement-gateway id. A re-delivered instruction resolves to a 409 conflict. # Register an inbound Pix Automático schedule cancellation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-an-inbound-pix-automatico-schedule-cancellation en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/schedules/inbound/cancellation/register Registers a schedule cancellation outcome delivered by the upstream system. An accepted cancellation (stCancelamento=0) transitions the schedule to Cancelled and soft-deletes the row; a rejected cancellation (stCancelamento=1) is acknowledged with no state change. Returns the local settlement-gateway id. # Register an inbound Pix Automático solicitation on the PSP Pagador Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/register-an-inbound-pix-automatico-solicitation-on-the-psp-pagador en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/inbound/payer/register Registers an inbound recurring-payment solicitation delivered by upstream to the PSP Pagador. This is payer-side genesis: it creates the payer-side authorization row in the Requested state and returns the local settlement-gateway id. A re-delivered solicitation resolves to a 409 conflict. # Request a Pix Automático authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/request-a-pix-automatico-authorization en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations Requests a recurring-payment authorization (recorrencia) as the PSP Recebedor. Persists the authorization in the Requested state and returns the upstream request id. # Request a Pix Automático schedule send Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/request-a-pix-automatico-schedule-send en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/schedules Requests the send of a recurring-payment instruction (agendamento) as the PSP Recebedor. Persists the schedule in the Requested state and returns the upstream acknowledgement. # Reserve an inbound Pix Automático debit hold Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/reserve-an-inbound-pix-automatico-debit-hold en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/settlements/inbound/block Reserves a debit HOLD on the payer's transactional account for a scheduled Pix Automático payment. Upstream calls this when it begins processing the scheduled payment. The reserve is parameterizable and disabled by default. On insufficient funds the result is returned inline (200) as a resultado=0 outcome with motivo SGCTPIX001, never an HTTP error. # Respond to a Pix Automático authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/respond-to-a-pix-automatico-authorization en/openapi/v3-current/pix.yaml PUT /v1/pix-automatico/authorizations/{idRecorrencia}/respond Accepts or rejects a recurring-payment authorization as the PSP Pagador. An acceptance transitions the authorization to Accepted; a rejection to Rejected. # Reverse an inbound Pix Automático settlement debit Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/reverse-an-inbound-pix-automatico-settlement-debit en/openapi/v3-current/pix.yaml DELETE /v1/pix-automatico/settlements/inbound/debit/{endToEndId} Reverses the prior settlement debit (or releases a still-pending hold) on a negative SPI return. The reverse is parameterizable and disabled by default. Returns the settlement-gateway reversal-operation id (idOperacaoSgct). # Start a MED value recovery refund stage Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/start-a-med-value-recovery-refund-stage en/openapi/v3-current/pix.yaml POST /v1/med/recoveries/{idRecValores}/devolver Starts the refund stage of a value recovery. It MOVES NO MONEY — it transitions stRecValores->4 and makes DICT emit refund requests. An unknown id returns a coded 404. # Update a CobV (due-date) composite Pix Automático QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/update-a-cobv-due-date-composite-pix-automatico-qr-code en/openapi/v3-current/pix.yaml PUT /v1/pix-automatico/composite/cobv/{id} Updates a previously generated CobV (cobrança com vencimento) composite Pix QR code, keyed by its document id (idDocumento). The response carries the upstream-computed revision (revisao). # Update a Pix Automático authorization max value Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/update-a-pix-automatico-authorization-max-value en/openapi/v3-current/pix.yaml PUT /v1/pix-automatico/authorizations/{idRecorrencia}/max-value Updates the payer max-value cap of an Active recurring-payment authorization. This is not a status transition; an omitted/zero value removes the cap. # Update a recurrence-only composite Pix Automático QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/update-a-recurrence-only-composite-pix-automatico-qr-code en/openapi/v3-current/pix.yaml PUT /v1/pix-automatico/composite/recurrence/{id} Updates a previously generated recurrence-only composite Pix QR code, keyed by its document id (idDocumento), carrying the recurrence status transition (stRecorrencia). # Update an immediate dynamic composite Pix Automático QR code Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/update-an-immediate-dynamic-composite-pix-automatico-qr-code en/openapi/v3-current/pix.yaml PUT /v1/pix-automatico/composite/dynamic/{id} Updates a previously generated immediate-dynamic composite Pix QR code, keyed by its document id (idDocumento). The response carries the upstream-computed revision (revisao). # Validate a receiving account synchronously Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/validate-a-receiving-account-synchronously en/openapi/v3-current/pix.yaml POST /v1/webhooks/validates Validates a receiving account synchronously and returns a non-exception {resultado, motivo, motivoComplemento, dtHrValidacao} envelope on every outcome (HTTP 200 even for an invalid account). Runs the conciliation 24h-duplicate + payer-divergence checks, the Bacen (ISPB 99999) special-case handling, and the account-document consistency check. # Validate an inbound Pix Automático authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/validate-an-inbound-pix-automatico-authorization en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/inbound/validate Validates an inbound recurring-payment authorization request pushed by the upstream system. A synchronous validation returns the result inline (200); a Jornada-3 authorization returns 202-async with the eventual result delivered via the outbound 4.3.10 validation reply. # Validate an inbound Pix Automático cancellation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/validate-an-inbound-pix-automatico-cancellation en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/authorizations/inbound/cancellation/validate Validates an inbound recurring-payment cancellation request pushed by the upstream system. The cancellation is valid only when an active authorization exists and its current status allows cancellation; the result is returned inline (200). # Validate an inbound Pix Automático payment instruction Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/validate-an-inbound-pix-automatico-payment-instruction en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/schedules/inbound/validate Validates an inbound scheduled payment instruction (pain.013) pushed by the upstream system. The settlement-gateway validates the charge data against the previously-confirmed authorization; the result is returned inline (200) as a Valido=1/Invalido=0 outcome, never an HTTP error. # Validate an inbound Pix Automático schedule cancellation Source: https://docs.lerian.studio/en/reference/midaz/plugins/pix/validate-an-inbound-pix-automatico-schedule-cancellation en/openapi/v3-current/pix.yaml POST /v1/pix-automatico/schedules/inbound/cancellation/validate Validates an inbound schedule cancellation request (camt.055) pushed by the upstream system. The cancellation is valid only when an active schedule exists and its current status allows cancellation; the result is returned inline (200). # Cancel a Funds Recovery Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/cancel-a-funds-recovery-request en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/funds-recoveries/{funds_recovery_id}/cancel Use this endpoint to cancel a funds recovery request. Cancellation is allowed before refund has been initiated (CREATED, TRACKED, AWAITING_ANALYSIS, or ANALYSED status). **Notes:** - Cancelled requests cannot be reopened or modified - Cancellation is not allowed for requests in REFUNDING, COMPLETED, or CANCELLED status # Cancel a Pix Automático Authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/cancel-a-pix-automatico-authorization en/openapi/v3-current/indirect-pix.yaml POST /v1/recurrences/payer/authorizations/{id}/cancel Cancel an active authorization. `reasonKind=PAYER` cancels the caller's own mandate; `reasonKind=FRAUD` records the caller as the operator and requires `reasonText`. The BACEN-effective date is D+1 (D+2 after 22:00 BRT) for PAYER and immediate for FRAUD. `X-Idempotency` is required; a repeat call replays the same response with `X-Idempotency-Replayed: true`. # Cancel a Pix Schedule Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/cancel-a-pix-schedule en/openapi/v3-current/indirect-pix.yaml DELETE /v1/schedules/{schedule_id} Cancel a schedule that is still in SCHEDULED status. If the schedule has already started processing, the cancel returns 409 (PIX-0809); if it is already in a terminal state, it returns 409 (PIX-0814). Requests for a schedule owned by another account return 404 (PIX-0800) without revealing whether it exists. # Confirm a Scanned Pix Automático Recurrence Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/confirm-a-scanned-pix-automatico-recurrence en/openapi/v3-current/indirect-pix.yaml POST /v1/recurrences/payer/qr/confirm Approve or reject a recurrence scanned from a QR Code (BACEN journeys J2 and J4, step 2). The recurrence payload captured when the QR Code was decoded (keyed by `idRec`) is used to build an authorization request, and the decision is confirmed with BTG synchronously. `X-Idempotency` is required. # Create a Dynamic Charge with Due Date Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-a-dynamic-charge-with-due-date en/openapi/v3-current/indirect-pix.yaml POST /v1/collections/duedate Use this endpoint to create a due date Pix collection (cobrança com vencimento) with complex payment rules including fines, interest, discounts, and rebates. **Notes:** - Similar to traditional boletos with Pix payment capability - Due date defines when payment is expected - Supports complex amount calculations with fines, interest, discounts, and rebates - Requires complete debtor and receiver information - Transaction ID (txId) must be unique across all collections - Collection remains valid for a configurable period after the due date # Create a Funds Recovery Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-a-funds-recovery-request en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/funds-recoveries Use this endpoint to create a new funds recovery request for a Pix transaction suspected of fraud. **Notes:** - The rootTransactionId must be a valid 32-character Pix End-to-End ID - situationType indicates the type of fraud situation (SCAM, ACCOUNT_TAKEOVER, COERCION, FRAUDULENT_ACCESS, OTHER, UNKNOWN) - Contact information (email and phone) is required for communication during the recovery process # Create a Pix Schedule Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-a-pix-schedule en/openapi/v3-current/indirect-pix.yaml POST /v1/schedules Create a single-shot scheduled Pix instruction. The schedule is created with status SCHEDULED and is dispatched automatically as a cashout at its scheduled fire time. **Notes:** - The destination is taken from an existing payment initiation referenced by `initiationId`. Call `POST /v1/transfers/cashout/initiate` first to obtain that id. - Provide a fresh `X-Idempotency` header to guarantee at-most-once creation per account. - `scheduledFor` must be at least 60 seconds in the future and within the 180-day window. - At fire time the schedule always executes as a manual cashout using the captured destination. - Schedules in a terminal state (EXECUTED, FAILED, CANCELLED) are immutable. # Create a Static QR Code Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-a-static-qr-code en/openapi/v3-current/indirect-pix.yaml POST /v1/brcode/static Use this endpoint to create a static Pix BR Code that can be reused for multiple payments. **Notes:** - Static QR Codes are reusable and can be paid multiple times. - Amount is optional - if not provided, the payer will enter the amount. - The receiver key must be owned by the account holder making the request. - The QR Code payload (EMV) is generated following the EMV QCO specification. # Create an Immediate Charge Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/create-an-immediate-charge en/openapi/v3-current/indirect-pix.yaml POST /v1/collections/immediate Use this endpoint to create an immediate Pix collection (cobrança imediata) with a dynamic QR Code that expires after a defined period. **Notes:** - Each immediate collection is unique and can only be paid once - Amount and expiration time are required - Transaction ID (txId) must be unique across all collections - Status automatically transitions to CONCLUDED when payment is received - Collection expires automatically after the specified time period # Decide on a Pix Automático Authorization Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/decide-on-a-pix-automatico-authorization-request en/openapi/v3-current/indirect-pix.yaml POST /v1/recurrences/payer/authorization-requests/{id}/decision Accept or reject a pending authorization request. The decision is confirmed with BTG synchronously before any local state changes, so the response reflects the final outcome. `X-Idempotency` is required. # Decode a Pix QR Code Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/decode-a-pix-qr-code en/openapi/v3-current/indirect-pix.yaml POST /v1/qrcodes/decode Use this endpoint to decode a Pix BR Code payload and extract the payment data it contains. This service supports all Pix QR Code types: static, immediate collections, and due date collections. # Delete an Immediate Charge Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/delete-an-immediate-charge en/openapi/v3-current/indirect-pix.yaml DELETE /v1/collections/immediate/{collection_id} Use this endpoint to delete (cancel) an immediate collection. **Notes:** - This operation cancels the collection and changes its status - Collections with status CONCLUDED cannot be deleted - The deletion reason is required # Get a Pix Automático Authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/get-a-pix-automatico-authorization en/openapi/v3-current/indirect-pix.yaml GET /v1/recurrences/payer/authorizations/{id} Retrieve a single authorization by id. Scoped to the authenticated payer. # Get a Pix Automático Authorization Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/get-a-pix-automatico-authorization-request en/openapi/v3-current/indirect-pix.yaml GET /v1/recurrences/payer/authorization-requests/{id} Retrieve a single authorization request by id. Scoped to the authenticated payer. # Get a Pix Automático Charge Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/get-a-pix-automatico-charge en/openapi/v3-current/indirect-pix.yaml GET /v1/recurrences/payer/charges/{id} Retrieve a single charge by id. Scoped to the authenticated payer — a charge owned by another payer is reported as not-found. The response carries the sanitized attempts array; the internal debit result code is never exposed. # Get a Pix Schedule Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/get-a-pix-schedule en/openapi/v3-current/indirect-pix.yaml GET /v1/schedules/{schedule_id} Retrieve a schedule by its unique identifier. Cross-account lookups return 404 — existence is never leaked across accounts. # Get a Refund Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/get-a-refund en/openapi/v3-current/indirect-pix.yaml GET /v1/refunds/{refund_id} Use this endpoint to retrieve a refund by its unique identifier. # Get an Inbound Webhook Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/get-an-inbound-webhook en/openapi/v3-current/indirect-pix.yaml GET /v1/webhooks/inbound/{id} Get a single inbound webhook by id. Intended for low-frequency operator use. # Get an Outbound Webhook Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/get-an-outbound-webhook en/openapi/v3-current/indirect-pix.yaml GET /v1/webhooks/outbound/{id} Get a single outbound webhook by id. Dead-letter entries are resolved transparently and surfaced with `status="DEAD_LETTER"` and additional dead-letter fields populated. # Indirect Pix error list Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/indirect-pix-error-list Indirect Pix returns consistent and structured error responses. This helps you quickly understand what went wrong and how to fix it. **Error format** ```jsonon JSON theme={null} { "code": "", "title": "", "message": "" } ``` **Field definitions** * **`code`** – A stable, unique identifier for the error. Useful for programmatic handling and support requests. * **`title`** – A short, human-readable summary of the issue. * **`message`** – Detailed guidance to help you resolve the error. This structure ensures you always get actionable feedback when something doesn’t go as expected. ## Indirect Pix errors *** The following errors can occur when interacting with the Indirect Pix endpoints. Each error follows our standard structure, making it easier to debug and respond to issues programmatically. ### Internal Server error | `code` | `title` | `message` | | -------- | --------------------- | ------------------------------------------------------------------- | | PIX-0000 | Internal Server Error | The server encountered an unexpected error. Please try again later. | ### Middleware errors | `code` | `title` | `message` | | -------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PIX-0001 | Missing Headers in Request | Your request is missing one or more required header params. Please refer to the documentation to ensure all necessary header params are included in your request. | | PIX-0002 | Invalid Header Value | One or more header values are invalid. Please refer to the documentation to verify the expected format and allowed values for each header. | | PIX-0003 | 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. | | PIX-0004 | Invalid Field Values in Request | Your request contains one or more fields with invalid values. Please refer to the documentation to verify that all fields have the correct values. | | PIX-0005 | Invalid Field Type in Request | Your request contains one or more fields with an invalid data type. Please refer to the documentation to verify that all fields have the correct type. | | PIX-0006 | Unexpected Fields in Request | The request body contains more fields than expected. Please send only the allowed fields as per the documentation. | | PIX-0007 | Missing Path Parameters in Request | Your request is missing one or more required path parameters. Please refer to the documentation to ensure all necessary path parameters are included in your request. | | PIX-0008 | Invalid Path Parameter in Request | One or more path parameters are invalid. Please refer to the documentation to verify the expected format and allowed values for each path parameter. | | PIX-0009 | Missing Query Params in Request | Your request is missing one or more required query params. Please refer to the documentation to ensure all necessary query params are included in your request. | | PIX-0010 | Invalid Query Param in Request | One or more query params are invalid. Please refer to the documentation to verify the expected format and allowed values for each query param. | | PIX-0011 | Invalid Timestamp Format | The timestamp format is invalid. Please refer to the documentation to verify the expected format. | | PIX-0012 | Invalid Metadata | The metadata is invalid. Please refer to the documentation to verify the expected format. | | PIX-0013 | Limit Exceeded | The request exceeded the allowed limit. | ### DICT errors | `code` | `title` | `message` | | -------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | PIX-0101 | EVP Key Cannot Be Provided | EVP keys cannot be provided manually, they are automatically generated. | | PIX-0102 | Invalid Key Format | Invalid format for the specified keyType. | | PIX-0103 | Pix Key Already Exists | The Pix key is already registered. | | PIX-0105 | CPF Mismatch | CPF does not match holder document in CRM. | | PIX-0106 | CNPJ Mismatch | CNPJ does not match holder document in CRM. | | PIX-0107 | Pix Key Limit Exceeded for Holder | The key limit has been exceeded for the holder. | | PIX-0108 | Pix Key Limit Exceeded for Type | The key type limit has been exceeded. | | PIX-0109 | Invalid Opening Date | The opening date in CRM is invalid. Please provide a valid date in the 'yyyy-mm-dd' format. | | PIX-0110 | Pix Key Belongs to Another Holder | The Pix key is already registered to another holder. Please initiate an ownership claim to acquire this key. | | PIX-0111 | Pix Key Already Linked to Another Account | The Pix key is already linked to another account of the same holder. Please initiate a portability request to transfer this key. | | PIX-0112 | Entry Not Found | The entry was not found. | | PIX-0113 | Claim Not Found | The claim was not found. | | PIX-0114 | Key In Custody Account | The entry queried is in custody of the participant making the query. | | PIX-0115 | EVP Key Cannot Be Updated | EVP keys cannot be updated. | | PIX-0116 | Empty Keys Array | The keys array must not be empty. | | PIX-0117 | Keys Array Exceeded Limit | The keys array must not exceed 200 items. | | PIX-0118 | Key Claim Type Not Allowed | Only PHONE keys are allowed for ownership claims. | | PIX-0119 | Pix Key Already Claimed | You already have a claim in progress for this key. | | PIX-0120 | Key Does Not Exist | The Pix key does not exist in DICT. | | PIX-0121 | Pix Key Locked By Claim | The Pix key is currently locked by another claim process. | | PIX-0122 | EVP Key Cannot Be Claimed | EVP keys cannot be claimed. | | PIX-0123 | Key Already Belongs to Holder | The Pix key is already linked to the requesting holder. | | PIX-0124 | Cannot Claim Key | It is not possible to claim this key. | | PIX-0165 | Entry Inactive | The entry is not in active status. | ### Claim errors (confirm) | `code` | `title` | `message` | | -------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | | PIX-0125 | Waiting Resolution Status | The claim must be in WAITING\_RESOLUTION status to execute the action. | | PIX-0126 | Must Be Donor Participant | The claim must be from the donor participant to confirm. | | PIX-0127 | Portability Cannot Be Confirmed | Portability claims cannot be confirmed with DEFAULT\_OPERATION reason. | | PIX-0128 | Ownership Cannot Be Confirmed | Ownership claims cannot be confirmed with DEFAULT\_OPERATION reason before the resolution period has ended. | ### Claim errors (cancel) | `code` | `title` | `message` | | -------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------- | | PIX-0134 | Claim Cannot Be Cancelled | The claim cannot be cancelled in current status. | | PIX-0135 | Donor Cannot Cancel Open Claim | The donor participant cannot cancel a claim in OPEN status. | | PIX-0136 | Invalid Reason for Claimer | The claimer cannot cancel with the provided reason in this status. | | PIX-0137 | Invalid Reason for Donor | The donor cannot cancel with the provided reason in this status. | | PIX-0141 | Donor Cannot Cancel Confirmed Portability | The donor cannot cancel a confirmed portability claim. | | PIX-0142 | Resolution Period Not Ended | The resolution period has not ended yet for DEFAULT\_OPERATION cancellation. | | PIX-0143 | Ownership Cancel Period Not Ended | Ownership claims with DEFAULT\_OPERATION can only be cancelled after CompletionPeriodEnd + 16 days. | | PIX-0144 | Donor Cancel Period Expired | The donor can only cancel with FRAUD while CompletionPeriodEnd has not passed. | | PIX-0145 | Unknown Actor | The participant is neither the claimer nor the donor. | ### Claim errors (complete) | `code` | `title` | `message` | | -------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | PIX-0146 | Claim Not Confirmed | The claim must be in CONFIRMED status. | | PIX-0147 | Must Be Claimer Participant | Only the claimer participant can complete a claim. | | PIX-0148 | Ownership Completion Period Not Ended | Ownership claims can only be completed after the completion period has ended. | | PIX-0149 | Claim Not Acknowledgeable | The claim does not meet acknowledgement preconditions (must be OPEN status and caller must be the donor participant). | | PIX-0160 | Claim Already Completed | The claim has already been completed. | | PIX-0161 | Claim Is Cancelled | The claim has been cancelled and cannot be completed. | | PIX-0162 | Claim Cannot Be Completed | The claim cannot be completed in its current status. | | PIX-0163 | Completion Period Not Set | The completion period is not set for this claim. | | PIX-0164 | Completion Period Expired | The completion period has expired for this claim. | ### VSync / CID errors | `code` | `title` | `message` | | -------- | ------------------ | ------------------------------------------------------------------ | | PIX-0150 | CID File Not Found | The specified CID file was not found or is not ready for download. | | PIX-0151 | Invalid CID Format | The CID must be a 64-character lowercase hexadecimal string. | | PIX-0152 | CID Not Found | The specified CID was not found in DICT. | ### MED erros (infraction reports) | `code` | `title` | `message` | | -------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | PIX-0200 | Infraction Report Not Found | The infraction report was not found. | | PIX-0201 | Report Details Too Long | The report details exceed the maximum allowed length of 2000 characters. | | PIX-0202 | Invalid Email Format | The email format is invalid. | | PIX-0203 | Invalid Phone Format | The phone format is invalid. | | PIX-0204 | Duplicate Infraction Report | An infraction report already exists for this transaction. | | PIX-0205 | Invalid Transaction ID Format | The transaction ID format must match a E2E ID format. | | PIX-0206 | Infraction Report Cannot Be Cancelled | The infraction report cannot be cancelled in current status. | | PIX-0207 | Infraction Report Cannot Be Closed | The infraction report cannot be closed in current status. | | PIX-0208 | Infraction Fraud Type Required | The fraud type is required when analysis result is AGREED. | | PIX-0209 | Infraction Analysis Details Required | The analysis details are required when fraud type is OTHER. | | PIX-0210 | Infraction Analysis Details Too Long | The analysis details exceed the maximum allowed length of 2000 characters. | | PIX-0211 | Transaction Not Found | The specified transaction was not found. | | PIX-0212 | Transaction Too Old | The transaction occurred more than 80 days ago and cannot be reported. | | PIX-0213 | Unauthorized to Open Refund Request | A refund request infraction report can only be opened by the payer's PSP. | | PIX-0214 | Unauthorized to Open Cancellation Report | A cancellation of refund infraction report can only be opened by the payee's PSP. | | PIX-0215 | Invalid Transaction Type | The transaction is not a refund transaction. Cancellation of refund infractions can only be used for refund transactions (pacs.004). | | PIX-0216 | Unauthorized to Close Refund Request | A refund request infraction report must be closed by the payee's PSP. | | PIX-0217 | Unauthorized to Close Cancellation Report | A cancellation of refund infraction report must be closed by the payer's PSP. | | PIX-0218 | Cancellation Reason Too Long | The cancellation reason exceeds the maximum allowed length of 500 characters. | | PIX-0219 | Invalid ID Format | The ID must be a valid UUID format. | | PIX-0220 | Invalid Date Range Order | The modified\_before timestamp must be after modified\_after timestamp. | | PIX-0221 | Refund Transaction Too Old | The refund transaction occurred more than 30 days ago and cannot be reported for cancellation. | | PIX-0222 | Invalid Situation Type | Refund cancellation infraction reports must use situationType OTHER. | | PIX-0223 | Infraction Report Cannot Be Acknowledged | The infraction report cannot be acknowledged in current status. | ### MED errors (refund requests) | `code` | `title` | `message` | | -------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | PIX-0230 | Invalid Refund Amount Format | The refund amount must be in decimal format with exactly 2 decimal places (e.g., 100.50). | | PIX-0231 | Refund Details Too Long | The refund details exceed the maximum allowed length of 2000 characters. | | PIX-0232 | Refund Amount Exceeds Transaction | The refund amount cannot exceed the original transaction amount. | | PIX-0233 | Missing Refund Details | The refundDetails field is required when refundReason is OPERATIONAL\_FLAW. | | PIX-0234 | Refund Request Not Found | The specified refund request was not found. | | PIX-0235 | Missing Refund Transaction ID | The refund transaction ID is required when the refund is accepted (TOTALLY\_ACCEPTED or PARTIALLY\_ACCEPTED). | | PIX-0236 | Invalid Request Status | The request cannot be closed because it is already in CLOSED or CANCELLED status. | | PIX-0237 | Missing Rejection Reason | The refundRejectionReason is required when analysisResult is REJECTED. | | PIX-0238 | Missing Analysis Details | The analysisDetails field is required when analysisResult is REJECTED and refundRejectionReason is INVALID\_REQUEST. | | PIX-0239 | Cannot Cancel Request | The request cannot be cancelled because it is already in CLOSED or CANCELLED status. | | PIX-0240 | Missing Infraction Report | A refund request with reason FRAUD requires a prior infraction report that has been closed with AGREED result. | | PIX-0241 | Infraction Report Expired | The infraction report was closed more than 72 hours ago. Refund requests must be created within 72 hours of infraction report closure. | | PIX-0242 | Duplicate Refund Request | A refund request already exists for this transaction. | | PIX-0243 | Refund Request Already Closed | The refund request is already closed. | | PIX-0244 | Refund Request Already Cancelled | The refund request is already cancelled. | | PIX-0245 | Date Range Exceeds 90 Days | The date range between modifiedAfter and modifiedBefore cannot exceed 90 days. | ### MED errors (fraud markers) | `code` | `title` | `message` | | -------- | ---------------------- | ------------------------------------------------------------------------------- | | PIX-0260 | Invalid Tax ID Format | The taxIdNumber must be a valid CPF (11 digits) or CNPJ (14 digits) format. | | PIX-0261 | Invalid Key Format | The key must be between 8 and 32 characters. | | PIX-0262 | Fraud Marker Not Found | The specified fraud marker was not found. | | PIX-0263 | Cannot Cancel Marker | The fraud marker cannot be cancelled because it is already in CANCELLED status. | ### MED errors (statistics) | `code` | `title` | `message` | | -------- | ----------------- | ------------------------------------------------------------------------ | | PIX-0280 | Invalid Key Value | The key value is invalid or exceeds the maximum length of 77 characters. | ### QR Code errors | `code` | `title` | `message` | | -------- | ------------------------------ | ---------------------------------------------------------------------------------- | | PIX-0300 | Invalid EMV Format | The EMV payload does not follow the valid EMV QCO format specification. | | PIX-0301 | Corrupted QR Code | The QR Code data is corrupted and cannot be decoded. | | PIX-0302 | Unsupported QR Code Version | The QR Code version or format is not supported. | | PIX-0303 | Invalid Payment Date | The payment date exceeds the valid payment period. | | PIX-0304 | Invalid Amount | The amount must be greater than zero and follow the 0.00 format with two decimals. | | PIX-0305 | Invalid City | The city must contain only letters and spaces. | | PIX-0306 | Invalid TxID Format | The txId format is invalid. | | PIX-0307 | Receiver Key Required | The receiverKey is required. | | PIX-0309 | Invalid Category Code Format | The categoryCode must be numeric. | | PIX-0310 | Invalid Merchant Name | The merchant name is required. | | PIX-0311 | Receiver Key Not Found | The receiverKey was not found. | | PIX-0312 | Invalid Receiver Key | The receiverKey is invalid. | | PIX-0314 | EMV Required | The EMV is required. | | PIX-0315 | Empty EMV Payload | The EMV payload cannot be empty. | | PIX-0316 | EMV Not Found | The EMV QR Code was not found. | | PIX-0317 | Invalid City Code | The city code is invalid. | | PIX-0318 | BR Code Not Found | The requested BR Code was not found. | | PIX-0319 | Receiver Key Not Associated | The receiver key is not associated with the account. | | PIX-0320 | Invalid Merchant Name | The merchant name is required. | | PIX-0321 | Invalid Merchant City | The merchant city must contain only letters and spaces. | | PIX-0322 | Invalid Merchant Category Code | The merchant category code must be numeric. | | PIX-0324 | Invalid Amount Value | The amount is not a valid decimal. | | PIX-0327 | Invalid Client Request ID | The client request ID must be a valid UUID. | | PIX-0328 | Invalid Image Type | The image type must be one of: Gif, Jpeg, Png. | | PIX-0329 | Invalid Pixels Per Module | The pixels per module value is out of bounds. | | PIX-0330 | Service Unavailable | The decode service is temporarily unavailable. | ### Transfers errors | `code` | `title` | `message` | | -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | PIX-0400 | Description Too Long | The description field exceeds the maximum allowed length of 140 characters. | | PIX-0401 | Description Contains HTML | The description contains invalid HTML characters. | | PIX-0402 | Invalid Amount Format | The amount must be in format 0.00 with exactly 2 decimal places (1-10 digits before decimal). | | PIX-0403 | Amount Must Be Positive | The amount must be greater than zero. | | PIX-0404 | Invalid UUID Format | The UUID format is invalid. | | PIX-0405 | Invalid End-to-End ID Format | The end-to-end ID format is invalid. | | PIX-0406 | Intra PSP Not Supported | INTRA PSP transfers (same participant) are not supported. The destination bank ISPB cannot be the same as the source PSP. | | PIX-0407 | Transfer Account Mismatch | The transfer does not belong to the specified account. | ### Initiation errors | `code` | `title` | `message` | | -------- | ------------------------ | ------------------------------------------------------ | | PIX-0410 | Initiation Not Found | The payment initiation was not found. | | PIX-0411 | Initiation Expired | The payment initiation has expired. | | PIX-0412 | Initiation Already Used | The payment initiation has already been processed. | | PIX-0413 | Initiation Invalid State | The payment initiation is in an invalid state. | | PIX-0414 | Destination Required | Destination is required for MANUAL initiation type. | | PIX-0415 | Invalid Initiation Type | The initiation type is invalid. | | PIX-0416 | Key Required | Key is required for KEY initiation type. | | PIX-0417 | EMV Required | EMV is required for QR\_CODE initiation type. | | PIX-0418 | Invalid Destination | The destination is invalid for MANUAL initiation type. | ### Cashout errors | `code` | `title` | `message` | | -------- | ----------------------- | -------------------------------------------------------- | | PIX-0420 | Cashout Not Found | The cashout transaction was not found. | | PIX-0421 | Insufficient Balance | Insufficient balance to complete the transaction. | | PIX-0422 | Amount Mismatch | The amount does not match the fixed amount from QR code. | | PIX-0423 | Duplicate End-to-End ID | A transaction with this end-to-end ID already exists. | | PIX-0424 | Invalid Amount Value | The amount must be greater than zero. | | PIX-0425 | Cashin Not Found | The cashin transaction was not found. | | PIX-0426 | Transfer Not Found | The transfer was not found. | | PIX-0427 | Refund Not Found | The refund was not found. | | PIX-0428 | Duplicate Refund | A refund already exists for this transaction. | ### Account errors | `code` | `title` | `message` | | -------- | ----------------- | ------------------------------------------------------- | | PIX-0430 | Account Not Found | The account was not found in ledger. | | PIX-0431 | Account Blocked | The account is blocked and cannot perform transactions. | ### Refund errors | `code` | `title` | `message` | | -------- | ------------------------------ | ------------------------------------------------------------------ | | PIX-0440 | Original Transfer Not Cashin | Only cashin transfers can be refunded with cashout refunds. | | PIX-0441 | Original Transfer Not Complete | Only completed transactions can be refunded. | | PIX-0442 | Refund Exceeds Original | The refund amount exceeds the original transaction amount. | | PIX-0443 | Invalid Refund Reason | The refund reason code is invalid. | | PIX-0444 | Refund Exceeds Net Amount | The partial refund amount exceeds net amount available after fees. | | PIX-0445 | Invalid Transaction Status | Only completed transactions can be refunded. | ### Destination validation errors | `code` | `title` | `message` | | -------- | -------------------------------- | ----------------------------------------------------------------------------------------------- | | PIX-0450 | Destination Bank Required | The destination bank is required. | | PIX-0451 | Destination Bank Invalid | The destination bank must be 8 digits. | | PIX-0452 | Destination Branch Required | The destination branch is required. | | PIX-0453 | Destination Account Required | The destination account is required. | | PIX-0454 | Destination Account Type Invalid | The destination account type is invalid. | | PIX-0455 | Destination Document Required | The destination document is required. | | PIX-0456 | Destination Document Invalid | The destination document is invalid: must be CPF (11 digits) or CNPJ (14 digits). | | PIX-0457 | Destination Name Required | The destination name is required. | | PIX-0458 | Destination Person Type Invalid | The destination person type is invalid. | | PIX-0459 | Destination Person Type Mismatch | Person type does not match document: CPF requires NATURAL\_PERSON, CNPJ requires LEGAL\_PERSON. | ### Status transition errors | `code` | `title` | `message` | | -------- | ------------------------- | ----------------------------------------------- | | PIX-0470 | Invalid Status Transition | The status transition is invalid. | | PIX-0471 | Transfer Already Terminal | The transaction is already in a terminal state. | | PIX-0472 | Cannot Cancel | Cannot cancel transaction in current status. | | PIX-0473 | Already Terminal | The operation is already in a terminal state. | ### Integration errors (CRM) | `code` | `title` | `message` | | -------- | ----------------------------- | --------------------------------------------------------------------------------------- | | PIX-0500 | CRM Connection Error | Failed to connect to CRM service. | | PIX-0501 | CRM Authentication Error | Failed to authenticate with CRM service. | | PIX-0502 | Account Not Found in CRM | The account was not found in CRM for the given criteria. | | PIX-0503 | Holder Not Found in CRM | The holder was not found in CRM. | | PIX-0504 | Invalid Branch in CRM | The branch returned by CRM is invalid. Expected exactly 4 digits. | | PIX-0505 | Invalid Account Number in CRM | The account number returned by CRM is invalid. Expected 1-20 digits. | | PIX-0506 | Invalid Document in CRM | The document (CPF/CNPJ) returned by CRM is invalid. | | PIX-0507 | Invalid Name in CRM | The name returned by CRM is invalid or exceeds 120 characters. | | PIX-0508 | Invalid Trade Name in CRM | The trade name returned by CRM is invalid or exceeds 100 characters. | | PIX-0509 | Invalid Opening Date in CRM | The opening date returned by CRM is invalid. Expected format: yyyy-mm-dd. | | PIX-0510 | Invalid Account Type in CRM | The account type returned by CRM is invalid. Expected: CACC, SLRY, SVGS, TRAN, or OTHR. | | PIX-0599 | CRM Bad Request | CRM returned a bad request error. | ### Integration errors (Midaz) | `code` | `title` | `message` | | -------- | ------------------------ | ------------------------------------------------------- | | PIX-0600 | Midaz Insufficient Funds | Insufficient funds in the source account. | | PIX-0601 | Midaz Account Blocked | The account is blocked and cannot perform transactions. | | PIX-0602 | Midaz Connection Error | Failed to connect to Midaz. | ### Integration errors (Fee Service) | `code` | `title` | `message` | | -------- | ---------------------------- | --------------------------------- | | PIX-0650 | Fee Service Connection Error | Failed to connect to Fee service. | ### Collection errors | `code` | `title` | `message` | | -------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | PIX-0700 | Collection Not Found | The collection was not found. | | PIX-0701 | Duplicate Collection TxID | A collection with this txId already exists for the receiver. | | PIX-0702 | Collection Already Deleted | The collection has already been deleted. | | PIX-0703 | Invalid Collection Type | The collection type is invalid. | | PIX-0704 | Invalid Collection Status | The collection status is invalid or does not allow this operation. | | PIX-0705 | Invalid Collection Amount | The collection amount is invalid. | | PIX-0706 | Invalid Collection Expiration / Receiver Key Not Owned | The collection expiration is invalid / The receiver key is not associated with the account. | | PIX-0707 | Invalid Due Date / Invalid Debtor | The due date is invalid / The debtor information is invalid. | | PIX-0708 | Expiration Too Long / Collection Has No Provider ID | The expiration exceeds the maximum allowed: 30 days (2,592,000 seconds) / The collection has no pactual ID. | | PIX-0709 | Invalid TxID / Invalid Delete Reason | The txID is invalid or cannot be empty / The delete reason must be DELETED\_BY\_USER or DELETED\_BY\_PSP. | | PIX-0710 | Invalid Receiver Document | The receiver document is invalid or cannot be empty. | | PIX-0711 | Invalid Receiver Name | The receiver name is invalid or cannot be empty. | | PIX-0712 | Invalid Receiver Address | The receiver address is required. | | PIX-0713 | Invalid Debtor Name | The debtor name is invalid or cannot be empty. | | PIX-0714 | Invalid CPF Format | The CPF format is invalid: must be exactly 11 digits. | | PIX-0715 | Invalid CNPJ Format | The CNPJ format is invalid: must be exactly 14 digits. | | PIX-0716 | Invalid Account ID | The account ID is invalid or cannot be empty. | | PIX-0717 | Invalid Pactual ID | The pactual ID is invalid or cannot be empty. | | PIX-0718 | Invalid Location ID | The location ID is invalid or cannot be empty. | | PIX-0719 | Invalid Fine | The fine configuration is incomplete: both type and value must be provided. | | PIX-0720 | Invalid Interest | The interest configuration is incomplete: both type and value must be provided. | | PIX-0721 | Invalid Discount | The discount configuration is incomplete: type, value and days before due must be provided. | | PIX-0722 | Invalid Abatement | The abatement configuration is incomplete: both type and value must be provided. | | PIX-0723 | Too Many Additional Info | The number of additional info items exceeds the maximum allowed: 50. | | PIX-0724 | Pix Troco Amount Mismatch | Pix Troco transaction amount must equal original amount. | | PIX-0725 | Invalid Pix Saque Configuration | Pix Saque cannot have transaction amount, only withdrawal amount. | | PIX-0726 | Collection Update Not Allowed | Updates are not allowed for this collection. | | PIX-0727 | Debtor Document Mismatch | The payment debtor document does not match the collection debtor. | | PIX-0728 | Type Mismatch | The payment type does not match the collection type. | | PIX-0729 | Collection Amount Mismatch | The payment amount does not match the collection amount. | ### Provider errors (BTG) | `code` | `title` | `message` | | -------- | ----------------------------- | -------------------------------------------------- | | PIX-1000 | Provider Connection Error | Failed to connect to provider. | | PIX-1001 | Provider Authentication Error | Failed to authenticate with provider. | | PIX-1002 | Provider Request Timeout | Provider request timed out. | | PIX-1003 | Rate Limit Exceeded | Rate limit exceeded. Please try again later. | | PIX-1004 | Provider Service Unavailable | Provider service is temporarily unavailable. | | PIX-1005 | Key Not Found | The Pix key was not found. | | PIX-1006 | Key Already Exists | The Pix key already exists. | | PIX-1007 | Provider Internal Error | Provider returned an internal error. | | PIX-1008 | Provider Invalid Response | Provider returned an invalid response. | | PIX-1009 | Provider Bad Request | Provider rejected the request due to invalid data. | | PIX-1010 | Provider Unprocessable | Provider cannot process the request. | | PIX-1099 | Provider Unmapped Error | Provider returned an unmapped error. | ### Provider errors (MED) | `code` | `title` | `message` | | -------- | --------------------------- | ---------------------------------------------------------------------------------------------- | | PIX-1065 | Duplicate Refund Request | The provider reports that a refund request already exists for this transaction. | | PIX-1068 | Invalid Request Status | The provider reports that the request is in an invalid state for this operation. | | PIX-1069 | Cannot Cancel Request | The provider reports that the request cannot be cancelled due to its current state. | | PIX-1080 | Duplicate Fraud Marker | The provider reports that a fraud marker already exists for this document and key combination. | | PIX-1081 | Invalid Fraud Type | The provider reports that the fraud type is not valid for this operation. | | PIX-1083 | Cannot Cancel Marker | The provider reports that the marker cannot be cancelled due to its current state. | | PIX-1090 | Person Statistics Not Found | The provider reports that no statistics were found for the specified tax ID. | | PIX-1091 | Key Statistics Not Found | The provider reports that no statistics were found for the specified Pix key. | # Initiate a Pix Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/initiate-a-pix-transfer en/openapi/v3-current/indirect-pix.yaml POST /v1/transfers/cashout/initiate Use this endpoint to start a Pix cash-out by creating an initiation record. This endpoint retrieves and validates destination information without moving funds. **Notes:** - This endpoint supports three initiation types: MANUAL (full account data), KEY (using a Pix key), and QR_CODE (using a QR code). - For KEY initiation, the plugin queries the DICT service to retrieve destination account information. - For QR_CODE initiation, the plugin decodes the QR code and queries the DICT service to retrieve destination account information. - The initiation does not move funds - it only validates and prepares the transfer. - After successful initiation, use the Process endpoint to complete the transfer. # List All Reconciliation Jobs Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-all-reconciliation-jobs en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/reconciliation Use this endpoint to list all reconciliation jobs with optional filters. Returns empty array [] if no jobs match the criteria. # List All VSync States Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-all-vsync-states en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/vsync Use this endpoint to list all VSync states for Pix key types with optional filters. Returns empty array [] if no VSync states match the criteria. Note: This table has at most 5 records (one per Pix key type: CPF, CNPJ, EMAIL, PHONE, EVP). # List Dynamic Charges with Due Date Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-dynamic-charges-with-due-date en/openapi/v3-current/indirect-pix.yaml GET /v1/collections/duedate Use this endpoint to list all due date collections for an account with optional filters and pagination. Returns empty array [] if no collections match the criteria. # List Funds Recoveries Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-funds-recoveries en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/funds-recoveries Use this endpoint to list funds recovery requests with optional filters and pagination. # List Infraction Reports Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-funds-recovery-infraction-reports en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/funds-recoveries/{funds_recovery_id}/infraction-reports Use this endpoint to list all infraction reports associated with a funds recovery request. # List Refunds Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-funds-recovery-refunds en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/funds-recoveries/{funds_recovery_id}/refunds Use this endpoint to list all refunds associated with a funds recovery request. # List Immediate Charges Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-immediate-charges en/openapi/v3-current/indirect-pix.yaml GET /v1/collections/immediate Use this endpoint to list all immediate collections for an account with optional filters and pagination. Returns empty array [] if no collections match the criteria. # List Inbound Webhooks Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-inbound-webhooks en/openapi/v3-current/indirect-pix.yaml GET /v1/webhooks/inbound List stored inbound webhooks (received from BTG). The `entity_type` query parameter is required. # List Outbound Webhooks Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-outbound-webhooks en/openapi/v3-current/indirect-pix.yaml GET /v1/webhooks/outbound List stored outbound webhooks (sent to clients). The `flow_type` query parameter is required. Dead-letter entries are surfaced transparently with `status="DEAD_LETTER"`. # List Pix Automático Authorization Requests Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-pix-automatico-authorization-requests en/openapi/v3-current/indirect-pix.yaml GET /v1/recurrences/payer/authorization-requests List authorization requests belonging to the authenticated payer with optional filters and page/limit pagination. # List Pix Automático Authorizations Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-pix-automatico-authorizations en/openapi/v3-current/indirect-pix.yaml GET /v1/recurrences/payer/authorizations List active authorizations belonging to the authenticated payer with optional filters and page/limit pagination. Includes computed `nextChargeAt` / `nextChargeAmount`. # List Pix Automático Charges by Authorization Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-pix-automatico-charges-by-authorization en/openapi/v3-current/indirect-pix.yaml GET /v1/recurrences/payer/authorizations/{authorizationId}/charges List the charges belonging to one authorization, scoped to the authenticated payer — charges owned by another payer are never returned. Supports page/limit pagination plus status and createdAt date-range filters. Each item carries the sanitized attempts array; the internal debit result code is never exposed. # List Pix Schedules Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-pix-schedules en/openapi/v3-current/indirect-pix.yaml GET /v1/schedules List schedules belonging to the caller's account with optional status filter and pagination. # List Pix Transfers Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-pix-transfers en/openapi/v3-current/indirect-pix.yaml GET /v1/transfers Use this endpoint to list Pix Transfers with optional filters and pagination. # List Refunds Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-refunds en/openapi/v3-current/indirect-pix.yaml GET /v1/refunds Use this endpoint to list refunds (both CASHOUT and CASHIN) with optional filters and pagination. # List Static QR Codes Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-static-qr-codes en/openapi/v3-current/indirect-pix.yaml GET /v1/brcode/static Use this endpoint to list all static Pix BR Codes for an account with optional filters. Returns empty array [] if no QR codes match the criteria. # List Transaction Reports Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/list-transaction-reports en/openapi/v3-current/indirect-pix.yaml GET /v1/transaction-reports Use this endpoint to list TRCK002 transaction reports with optional filters and pagination. # Pay and Confirm a Scanned Pix Automático Recurrence Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/pay-and-confirm-a-scanned-pix-automatico-recurrence en/openapi/v3-current/indirect-pix.yaml POST /v1/recurrences/payer/qr/pay-and-confirm BACEN journey J3: as a single atomic operation, process the pre-created immediate-charge cashout (`initiationId`) and confirm the scanned recurrence (`idRec`). Returns 200 when the cashout settles immediately (the authorization request is APPROVING); returns 202 when the cashout is still processing (the authorization request stays INITIATED until the settlement webhook drives it). The payer account is the authenticated `X-Account-Id`; the debit amount is taken from the referenced initiation and is never client-supplied. `X-Idempotency` is required. # Process a Pix Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/process-a-pix-transfer en/openapi/v3-current/indirect-pix.yaml POST /v1/transfers/cashout/process Use this endpoint to process a previously initiated Pix cash-out and move funds between accounts. # Refund a Received Pix Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/refund-a-received-pix-transfer en/openapi/v3-current/indirect-pix.yaml POST /v1/transfers/{transfer_id}/refunds Use this endpoint to initiate a refund for a previously received Pix transfer. The refund returns funds to the original sender of the transaction. **Notes:** - Only received Pix transfers (cash-in) can be refunded. - Multiple refunds can be issued for the same original transfer, but the sum of all refunds cannot exceed the original transaction amount. - Refunds are processed immediately and cannot be cancelled once submitted. - The original transfer must be identified by its end-to-end identifier (E2E ID). - The refund status will change from PENDING to PROCESSING to COMPLETED/FAILED via webhook notifications. - Midaz transaction processing for refunds is handled asynchronously when the refund status is confirmed via webhook. # Request Refund Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/request-funds-recovery-refund en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/funds-recoveries/{funds_recovery_id}/refund Use this endpoint to request a refund for a funds recovery. This is only allowed when the funds recovery is in ANALYSED status. **Notes:** - Refund request triggers the MED refund flow - The funds recovery status will transition to REFUNDING # Retrieve a Pix Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-a-pix-transfer en/openapi/v3-current/indirect-pix.yaml GET /v1/transfers/{transfer_id} Use this endpoint to retrieve a Pix transfer by its unique identifier. # Retrieve a Static QR Code Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-a-static-qr-code en/openapi/v3-current/indirect-pix.yaml GET /v1/brcode/static/{brcode_id} Use this endpoint to retrieve detailed information about a specific static Pix BR Code. # Retrieve Dynamic Charge with Due Date Details Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-dynamic-charge-with-due-date-details en/openapi/v3-current/indirect-pix.yaml GET /v1/collections/duedate/{collection_id} Use this endpoint to retrieve detailed information about a specific due date collection. # Retrieve Funds Recovery details Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-funds-recovery-details en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/funds-recoveries/{funds_recovery_id} Use this endpoint to retrieve the full details of a single funds recovery request. # Retrieve Tracking Graph Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-funds-recovery-tracking-graph en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/funds-recoveries/{funds_recovery_id}/tracking-graph Use this endpoint to retrieve the tracking graph for a funds recovery request. The tracking graph shows the flow of funds from the root transaction. **Notes:** - Optional parameters can be used to customize the graph generation - The graph includes persons, accounts, and transactions related to the fraud # Retrieve Immediate Charge Details Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-immediate-charge-details en/openapi/v3-current/indirect-pix.yaml GET /v1/collections/immediate/{collection_id} Use this endpoint to retrieve detailed information about a specific immediate collection. # Retrieve Person Statistics Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retrieve-person-statistics en/openapi/v3-current/indirect-pix.yaml GET /v1/dict/statistics/persons/{tax_id} Use this endpoint to fetch aggregated Pix statistics for a person identified by their tax ID (CPF or CNPJ). **Notes:** - Statistics include transaction settlement data, fraud markers, infraction reports, and entry information. - Data is aggregated over different time periods: d90 (last 90 days), m12 (last 12 months), m60 (last 60 months). - This endpoint queries the provider directly and does not store data locally. - Use this information for risk assessment before processing Pix transactions. # Retry an Inbound Webhook Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retry-an-inbound-webhook en/openapi/v3-current/indirect-pix.yaml POST /v1/webhooks/inbound/{id}/retry Re-arm a FAILED inbound webhook so it is retried. Returns 202 Accepted. The retry preserves the current attempt count and raises the retry ceiling. # Retry an Outbound Webhook Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/retry-an-outbound-webhook en/openapi/v3-current/indirect-pix.yaml POST /v1/webhooks/outbound/{id}/retry Re-arm a FAILED outbound webhook. A webhook that has already moved to the dead-letter queue is restored (preserving its original id) and removed from the dead-letter queue atomically. Returns 202 Accepted. # Start Full Reconciliation Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/start-full-reconciliation en/openapi/v3-current/indirect-pix.yaml POST /v1/dict/reconciliation/full Triggers full reconciliation for all key types that are currently out of sync. This endpoint detects which key types have OUT_OF_SYNC status and requests CID files from BTG for each of them. The actual reconciliation processing happens asynchronously via webhooks when the files become available. **Response codes:** - **202 Accepted**: One or more reconciliation jobs were created - **200 OK**: All key types are already synchronized (no jobs created) # Submit a Transaction Report Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/submit-a-transaction-report en/openapi/v3-current/indirect-pix.yaml POST /v1/transaction-reports/{id}/submit Use this endpoint to manually trigger the submission of a TRCK002 transaction report to BTG. This is a fallback for reports that were not submitted through the normal automated flow. **Notes:** - Submission is accepted for reports in PENDING or ERROR status; ERROR reports are previously failed submissions an operator can retry. - This endpoint is idempotent: reports that are already PROCESSING or CONFIRMED are left unchanged. - Submission is accepted for asynchronous processing; the final outcome (CONFIRMED/ERROR) is delivered later via the BTG CAMT025 status webhook. # Unblock a Pix Refund Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/unblock-a-pix-refund en/openapi/v3-current/indirect-pix.yaml POST /v1/refunds/{refund_id}/unblock Use this endpoint to unblock a refund stuck in PROCESSING status. The plugin fetches the BTG reversal status and triggers settlement if the reversal has already completed or failed on BTG. # Unblock a Pix Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/unblock-a-pix-transfer en/openapi/v3-current/indirect-pix.yaml POST /v1/transfers/{transfer_id}/unblock Use this endpoint to unblock a transfer that is stuck in PROCESSING status. The plugin fetches the current status from BTG and triggers settlement if the transaction has already completed or failed on BTG. # Update a Dynamic Charge with Due Date Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/update-a-dynamic-charge-with-due-date en/openapi/v3-current/indirect-pix.yaml PATCH /v1/collections/duedate/{collection_id} Use this endpoint to update a due date collection. Only collections in ACTIVE status can be updated. # Update a Funds Recovery Request Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/update-a-funds-recovery-request en/openapi/v3-current/indirect-pix.yaml PATCH /v1/dict/funds-recoveries/{funds_recovery_id} Use this endpoint to update an existing funds recovery request. Updates are only allowed for requests in CREATED, TRACKED, or AWAITING_ANALYSIS status. **Notes:** - Only the situationType, reportDetails, and contactInformation can be updated - Updates are not allowed for requests in terminal states (COMPLETED, CANCELLED) # Update an Immediate Charge Source: https://docs.lerian.studio/en/reference/midaz/plugins/indirect-pix/update-an-immediate-charge en/openapi/v3-current/indirect-pix.yaml PUT /v1/collections/immediate/{collection_id} Use this endpoint to update an immediate collection. Only collections in ACTIVE status can be updated. **Mutable Fields (can be updated):** - expirationSeconds: Time in seconds until collection expires - receiverKey: Pix key that will receive the payment - debtorDocument: CPF/CNPJ of the payer (must be provided with debtorName) - debtorName: Name of the payer (must be provided with debtorDocument) - description: Custom description for the collection - additionalInfo: Additional key-value information - tags: Array of custom tags **Immutable Fields (cannot be updated):** - id: Collection unique identifier - accountId: Account identifier - txId: Transaction identifier - amount: Original collection amount - status: Collection status (managed by system) - pactualId: External provider ID - emv: QR code payload - locationUrl: QR code URL - createdAt: Creation timestamp - updatedAt: Last update timestamp **State Restrictions:** - Collections can ONLY be updated when status is ACTIVE - Collections with status COMPLETED, DELETED, or EXPIRED cannot be updated # Activate ICP-Brasil certificate on the RSFN (GEN0006) Source: https://docs.lerian.studio/en/reference/rails/spb/activate-certificate en/openapi/v3-current/spb.yaml post /v1/str/certificates/activate Prerequisite: the certificate must have been rotated first (see rotateCertificate) — its persisted state must be VALID or EXPIRING and carry the recorded serial. Submits a GEN0006 certificate-activation announcement to BACEN for the current OUTBOUND_SIGNING certificate. The submission is persisted with status SUBMITTED before dispatch; BACEN's GEN0006R1 acknowledgment confirms it via NUOp correlation. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Approve (sign) a parked emission Source: https://docs.lerian.studio/en/reference/rails/spb/approve-emission en/openapi/v3-current/spb.yaml post /v1/str/emission-approvals/{id}/approve Applies one distinct-actor maker-checker signature to a PENDING_APPROVAL emission. The signer is derived from the bearer token. A signer who already signed (duplicate) or who is the maker (self-approval) is rejected; a sign past the approval deadline or on an already-resolved emission is rejected. When the appended signature reaches the required quorum the emission flips to SUBMITTED and the existing outbox dispatcher liquidates it — no dispatcher change. # Compose and send a curated STR message Source: https://docs.lerian.studio/en/reference/rails/spb/compose-str-message en/openapi/v3-current/spb.yaml post /v1/str/messages/compose Catalog-gated, maker-checker-routed, per-type curated message-compose surface. The chosen capabilityId must be in the compose allowlist (STR0004/STR0005/STR0006/STR0007/STR0008/STR0053), resolve in the catalog, and be PUBLICLY_SUBMITTABLE; otherwise the request is rejected SPB-0003 (HTTP 400). The message is built through the shared compose pipeline and inherits the alçada emission gate: an amount above its band parks in PENDING_APPROVAL (visible in GET /v1/str/emission-approvals, emissionApprovalId set) and dispatches on quorum; below band it is SUBMITTED. Idempotent — replaying the same X-Idempotency key returns the same emission. The declared amount is forwarded verbatim; no position is summed. # Set Conta-PI auto-transfer configuration Source: https://docs.lerian.studio/en/reference/rails/spb/create-auto-transfer-config en/openapi/v3-current/spb.yaml post /v1/str/operations/auto-transfer-config Prerequisite: the rail must be ready before a submit — an active certificate (see rotateCertificate, activateCertificate), a READY readiness report (see getReadiness), and a passing connectivity test (see createConnectivityTest). Declares the STR's automatic-transfer policy per account class (the platform emits wire code LPI0005): for EACH class (RB/CL and CCME) set exactly ONE of {percent, value} — a value of '0'/'0.00' disables auto-transfer for that class. 'Last message wins': a new config fully replaces the prior one. Persists the last-sent config frame and returns the derived wire code, the submit status, and the NUOp. This means dispatch was accepted, not BACEN confirmation; the STR acknowledges asynchronously (LPI0005R1) and later reports each automatic transfer it performs (LPI0006). Lerian SPB forwards the config values verbatim; it performs NO arithmetic. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Create bank transfer cancellation operation Source: https://docs.lerian.studio/en/reference/rails/spb/create-bank-transfer-cancellation-operation en/openapi/v3-current/spb.yaml post /v1/str/operations/bank-transfers/{operationId}/cancellations Prerequisite: the parent operation must exist and admit cancellation — read its state via getOperation (GET /v1/str/operations/{operationId}); the {operationId} path segment structurally ties the cancellation to its parent. The handler resolves the original by operationId, gates lifecycle (SPB-3006 if status forbids), and delegates to the STR0011 cancellation path. # Create bank transfer operation Source: https://docs.lerian.studio/en/reference/rails/spb/create-bank-transfer-operation en/openapi/v3-current/spb.yaml post /v1/str/operations/bank-transfers Prerequisite: the rail must be ready before a submit — an active certificate (see rotateCertificate, activateCertificate), a READY readiness report (see getReadiness), and a passing connectivity test (see createConnectivityTest). Accepts a bank transfer operation through the operation-centric API. The client declares the typed debtor and creditor roles (sender.party / recipient.party); the platform DERIVES the BACEN STR transfer code (STR0004 institution→institution, STR0005 non-account-holders, STR0006 client→IF, STR0007 IF→client, STR0008 client→client), persists the operation aggregate and SUBMITTED outbound relay work, and returns the canonical OperationAccepted envelope. This means dispatch was accepted, not BACEN settlement confirmation. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Create bank transfer return operation Source: https://docs.lerian.studio/en/reference/rails/spb/create-bank-transfer-return-operation en/openapi/v3-current/spb.yaml post /v1/str/operations/bank-transfers/{operationId}/returns Prerequisite: the parent operation must be SETTLED before a return — read its state via getOperation (GET /v1/str/operations/{operationId}); the {operationId} path segment structurally ties the return to its parent. The handler resolves the original operation by operationId, validates that the lifecycle state admits a return (SPB-3006 otherwise), and delegates to the STR0010 return path. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Initiate connectivity (ECO) test Source: https://docs.lerian.studio/en/reference/rails/spb/create-connectivity-test en/openapi/v3-current/spb.yaml post /v1/str/connectivity-tests Prerequisite: an active certificate (see rotateCertificate, activateCertificate) and a READY readiness report (see getReadiness) before the connectivity test; a passing connectivity test is itself the prerequisite for a submit operation (see createBankTransferOperation). Builds a GEN0001 echo request addressed to the given participant, persists it as SUBMITTED before rail dispatch, and lets the outbound relay dispatch it to the RSFN. The counterparty answers with GEN0001R1 echoing the message back; the response correlates by NUOp and confirms the test. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Create IBS repasse operation Source: https://docs.lerian.studio/en/reference/rails/spb/create-ibs-repasse-operation en/openapi/v3-current/spb.yaml post /v1/str/operations/ibs-repasses Prerequisite: the rail must be ready before a submit — an active certificate (see rotateCertificate, activateCertificate), a READY readiness report (see getReadiness), and a passing connectivity test (see createConnectivityTest). Accepts a STR0053 IBS-repasse operation (reserve repasse of IBS settlement values to the comitê gestor) through the operation-centric API. The handler translates the JSON request into the STR0053 XML request path, persists the operation aggregate and SUBMITTED outbound relay work, and returns the canonical OperationAccepted envelope. Dispatch was accepted, not BACEN settlement confirmation. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Create Conta-PI liquidity transfer Source: https://docs.lerian.studio/en/reference/rails/spb/create-liquidity-transfer-operation en/openapi/v3-current/spb.yaml post /v1/str/operations/liquidity-transfers Prerequisite: the rail must be ready before a submit — an active certificate (see rotateCertificate, activateCertificate), a READY readiness report (see getReadiness), and a passing connectivity test (see createConnectivityTest). Accepts an LPI Conta-PI liquidity transfer through the operation-centric API: direction aporte funds the FI's Conta PI from its RB/CL (the platform DERIVES wire code LPI0001), resgate recalls funds (LPI0003) — the platform never composes the wire code. Persists a PENDING liquidity-transfer projection and returns the operation id, the PENDING status, and the derived wire code. This means dispatch was accepted, not BACEN settlement confirmation; the settlement arrives asynchronously on the inbound STR R-leg and is read via getLiquidityTransfer. An out-of-window aporte is rejected (RLPI0007). Lerian SPB EMITS the movement fact and projects the Conta-PI balance verbatim; it never computes a position. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Create reconciliation action Source: https://docs.lerian.studio/en/reference/rails/spb/create-reconciliation-action en/openapi/v3-current/spb.yaml post /v1/str/reconciliations/{reconciliationId}/actions Records an operator recovery action on a reconciliation case. Valid actions are ACKNOWLEDGE, RESOLVE, DISMISS, INVESTIGATE. Terminal cases (RESOLVED / DISMISSED) reject further actions with SPB-3007 / HTTP 409. # Submit STR0013 reserve-account balance query Source: https://docs.lerian.studio/en/reference/rails/spb/create-reserve-balance-query en/openapi/v3-current/spb.yaml post /v1/str/reserve-balance-queries Submits a single-party STR0013 reserve-account balance query to BACEN and returns a correlation id plus a PENDING state. The answer (STR0013R1, carrying SldRB_CL — the conta Reservas Bancárias / Conta de Liquidação balance) arrives asynchronously and is read via GET /v1/str/reserve-balance-queries/{id}. Lerian SPB projects the balance BACEN sends; it never computes a position. # Deny a parked emission Source: https://docs.lerian.studio/en/reference/rails/spb/deny-emission en/openapi/v3-current/spb.yaml post /v1/str/emission-approvals/{id}/deny Terminalizes a PENDING_APPROVAL emission: a single distinct authorized actor denying flips it to FAILED with the reason recorded — one deny kills it (a denial is not subject to quorum). A FAILED emission is never claimed by the outbox dispatcher, so it never liquidates. # Get capability by ID Source: https://docs.lerian.studio/en/reference/rails/spb/get-capability en/openapi/v3-current/spb.yaml get /v1/str/capabilities/{capabilityId} Returns the detail of a single STR capability identified by its public identifier (e.g. "STR0004"). # Get event delivery Source: https://docs.lerian.studio/en/reference/rails/spb/get-event-delivery en/openapi/v3-current/spb.yaml get /v1/str/event-deliveries/{deliveryId} Returns a single event_delivery row plus its attempt history. Lookups for unknown ids surface as not-found (SPB-0010), byte-identical to truly-missing. # Get Conta-PI liquidity transfer status Source: https://docs.lerian.studio/en/reference/rails/spb/get-liquidity-transfer en/openapi/v3-current/spb.yaml get /v1/str/operations/liquidity-transfers/{operationId} Reads the projection of a previously submitted LPI Conta-PI liquidity transfer by its operation id. Returns PENDING until the inbound STR R-leg (LPI0001R1 / LPI0003R1) arrives, then SETTLED (or REJECTED) with the STR settlement confirmation (numCtrlSTR / sitLancSTR) BACEN sent, projected verbatim. Unknown ids surface as not-found (SPB-0010). Lerian SPB projects the settlement; it never computes a position. # Get STR message detail Source: https://docs.lerian.studio/en/reference/rails/spb/get-message-status en/openapi/v3-current/spb.yaml get /v1/str/messages/{nuOp} Returns the message-log detail for an STR message identified by its NUOp: the status and BACEN-receipt fields, the structured payload, an on-read reconstructed XML view (the persisted on-wire bytes decoded to XML, labeled reconstructed — the literal sent bytes are not separately retained), and the resolved original<->correspondent counterpart. Searches outbound messages first, then inbound. The NUOp is a 23-digit BACEN identifier: ISPB[8]+DATE[8]+SEQ[7]. # Get operation detail Source: https://docs.lerian.studio/en/reference/rails/spb/get-operation en/openapi/v3-current/spb.yaml get /v1/str/operations/{operationId} Returns the sanitized detail of a single operation. Unknown ids surface as not-found (SPB-0010); the service never leaks the existence of an operation it does not own. Raw protocol payload fields never appear in responses. # List operation timeline Source: https://docs.lerian.studio/en/reference/rails/spb/get-operation-timeline en/openapi/v3-current/spb.yaml get /v1/str/operations/{operationId}/timeline Returns chronological lifecycle entries for one operation. Unknown ids surface as not-found (SPB-0010). Sort order is occurred_at ASC. Raw protocol payload fields never appear in responses. # Get operational aggregates Source: https://docs.lerian.studio/en/reference/rails/spb/get-operational-aggregates en/openapi/v3-current/spb.yaml get /v1/str/operational-aggregates Returns pre-computed aggregate buckets matching the requested window (hour or day) + date range. Source is the operational_aggregate_bucket table populated by a background worker. Buckets are ordered by bucket_start ASC. # Get operational summary Source: https://docs.lerian.studio/en/reference/rails/spb/get-operational-summary en/openapi/v3-current/spb.yaml get /v1/str/operational-summary Returns a dashboard-ready summary composing readiness, operation counts (grouped by family + status), reconciliation backlog (open/in-progress only), failed deliveries (failed/exhausted), and certificate warnings (any non-VALID status). Computed on demand from live source tables. # Get service readiness Source: https://docs.lerian.studio/en/reference/rails/spb/get-readiness en/openapi/v3-current/spb.yaml get /v1/str/readiness Prerequisite: an active certificate (see rotateCertificate, activateCertificate) before readiness reports READY; a READY readiness report is itself the prerequisite for a connectivity test (see createConnectivityTest) and any submit operation (see createBankTransferOperation). Returns the operational readiness summary for the service, including database connectivity, certificate validity window, channel settings binding, and any degraded subsystems. Used by ops dashboards and pre-flight gates before STR submission. Independent of the global Kubernetes /readyz probe — this endpoint reports the participant operational readiness state, while /readyz reports process-level dependency health. # Get reconciliation case Source: https://docs.lerian.studio/en/reference/rails/spb/get-reconciliation-case en/openapi/v3-current/spb.yaml get /v1/str/reconciliations/{reconciliationId} Returns a single reconciliation case. Lookups for unknown ids surface as not-found (SPB-0010), byte-identical to truly-missing. # Movimento financeiro report Source: https://docs.lerian.studio/en/reference/rails/spb/get-report-movement en/openapi/v3-current/spb.yaml get /v1/str/reports/movement Returns, per (messageType, direction), the SUM of the declared amount + a COUNT over messages_outbound / messages_inbound created within the date range, plus a grand total per direction. The decimal sum is accumulated in integer centavos (never float). This sums Lerian SPB's OWN transmitted/received amounts — a derived report view, not a held position. BACEN-supplied reserve balances are NOT projected here (use GET /v1/str/reserve-statement-queries/{id}); movement never rolls movements up into an opening/closing balance. # Transações rejeitadas report Source: https://docs.lerian.studio/en/reference/rails/spb/get-report-rejected en/openapi/v3-current/spb.yaml get /v1/str/reports/rejected Returns a COUNT plus a cursor-paginated list of rejected messages (outbound status=REJECTED, inbound str_reject_reason present) within the date range, ordered by (createdAt DESC, id DESC). str_reject_reason is projected verbatim. A pure filter+count over Lerian SPB's own record. # Volumetria report Source: https://docs.lerian.studio/en/reference/rails/spb/get-report-volumetria en/openapi/v3-current/spb.yaml get /v1/str/reports/volumetria Returns COUNT(*) grouped by (messageType, status, day) over the raw message tables within the date range — the per-type/day message breakdown. Non-monetary (counts only). # Read the live alçada governance config Source: https://docs.lerian.studio/en/reference/rails/spb/get-str-governance en/openapi/v3-current/spb.yaml get /v1/str/governance Returns the live alçada value-band table and per-message-type signature requirements. Hot-reloaded at runtime; the configured default is only a seed. This is a classification rule table, never a position. # List audit records Source: https://docs.lerian.studio/en/reference/rails/spb/list-audit-records en/openapi/v3-current/spb.yaml get /v1/str/audit-records Lists audit evidence records with optional filters by actor, action type, and time window. Cursor-based pagination — pass the nextCursor from a previous response to continue. The cursor is opaque and MUST NOT be parsed by clients. # List audit records for an operation Source: https://docs.lerian.studio/en/reference/rails/spb/list-audit-records-for-operation en/openapi/v3-current/spb.yaml get /v1/str/operations/{operationId}/audit-records Lists audit evidence records associated with a specific operation, identified by its UUID. Cursor-based pagination — same semantics as the audit-records list. # List STR capabilities Source: https://docs.lerian.studio/en/reference/rails/spb/list-capabilities en/openapi/v3-current/spb.yaml get /v1/str/capabilities Returns the public catalog of STR capabilities. Every authenticated caller sees the same rows. Supports filtering by operation family, handling mode, direction, and availability. Cursor-based pagination. # List certificate state Source: https://docs.lerian.studio/en/reference/rails/spb/list-certificates en/openapi/v3-current/spb.yaml get /v1/str/certificates Returns the current state of the ICP-Brasil/RSFN certificate set, including purpose, status, validity window, and readiness impact. Private keys and key material are never exposed. # List emissions pending maker-checker approval Source: https://docs.lerian.studio/en/reference/rails/spb/list-emission-approvals en/openapi/v3-current/spb.yaml get /v1/str/emission-approvals Returns the queue of outbound emissions parked in PENDING_APPROVAL above their alçada band, awaiting the required distinct-actor signatures before they may liquidate. Each row carries the message's OWN declared amount (verbatim — never a summed position), the N-of-M quorum, the signatures collected so far, the approval deadline, and the maker. Read model only. # List event catalog Source: https://docs.lerian.studio/en/reference/rails/spb/list-event-catalog en/openapi/v3-current/spb.yaml get /v1/str/events/catalog Returns the public, shared catalog of control-plane event types and versions. Every authenticated caller sees the same rows. Subscribers consume this catalog to negotiate webhook delivery payload shapes. # List event deliveries Source: https://docs.lerian.studio/en/reference/rails/spb/list-event-deliveries en/openapi/v3-current/spb.yaml get /v1/str/event-deliveries Returns a page of event delivery records with attempt history. Filters by status, event type, webhook id, operation id, and date range. Cursor-based pagination — pass the nextCursor from a previous response to continue. # List inbound GEN notices Source: https://docs.lerian.studio/en/reference/rails/spb/list-gen-notices en/openapi/v3-current/spb.yaml get /v1/str/gen-notices Returns the durable, queryable log of inbound GEN-family notices (GEN0001 connectivity echo, GEN0004 transmission error, GEN0005 administrative notice) received from BACEN. Each notice carries the sanitized official fields verbatim — never a raw frame echo. Most recent first; optionally filter by noticeType or announcing-participant senderIspb. # List STR messages Source: https://docs.lerian.studio/en/reference/rails/spb/list-messages en/openapi/v3-current/spb.yaml get /v1/str/messages Lists outbound and inbound STR messages with optional filters and cursor-based pagination. The cursor is opaque — pass the nextCursor value from a previous response to continue. Filters can be combined freely; results are merged in the requested sort order. # List operations Source: https://docs.lerian.studio/en/reference/rails/spb/list-operations en/openapi/v3-current/spb.yaml get /v1/str/operations Lists operations with optional filters. Cursor-based pagination — pass the nextCursor from a previous response to continue. The cursor is opaque and MUST NOT be parsed by clients. Raw protocol payload fields never appear in responses. # List reconciliation cases Source: https://docs.lerian.studio/en/reference/rails/spb/list-reconciliation-cases en/openapi/v3-current/spb.yaml get /v1/str/reconciliations Returns a page of reconciliation cases with operator-actionable filters. Cases are ordered by openedAt DESC. Cursor-based pagination — pass the nextCursor from a previous response to continue. # List stuck operations (operator dashboard) Source: https://docs.lerian.studio/en/reference/rails/spb/list-stuck-operations en/openapi/v3-current/spb.yaml get /v1/str/stuck-operations Returns the cross-rail operator dashboard page of operations needing attention, conformed to the shared recovery vocabulary. Each row carries only the verbs SPB can actually perform on it (a verb SPB cannot perform is absent) and the disposition ('what happens if I do nothing'). Reconciliation cases are shown with their affordances; pre-case stuck-outbound rows the dispatch sweep is retrying are shown informationally with no affordances. No monetary or position fields are returned. # Replace the alçada governance config Source: https://docs.lerian.studio/en/reference/rails/spb/put-str-governance en/openapi/v3-current/spb.yaml put /v1/str/governance Validates and replaces the alçada band table and per-message-type signature requirements. Validation failure returns HTTP 400 (SPB-0003). On success the classifier hot-reloads with no restart, so newly parked emissions immediately reflect the new requiredSignatures. SPB-only (SPI has no alçada engine). This replaces a rule table, never a balance. # Retry event delivery Source: https://docs.lerian.studio/en/reference/rails/spb/retry-event-delivery en/openapi/v3-current/spb.yaml post /v1/str/event-deliveries/{deliveryId}/retry Requests a manual retry of a failed event delivery. Conflicts (already DELIVERED, retry already in flight) return SPB-3007 / HTTP 409. The request body is optional — a retry POST with no body is valid. # Rotate ICP-Brasil certificate Source: https://docs.lerian.studio/en/reference/rails/spb/rotate-certificate en/openapi/v3-current/spb.yaml post /v1/str/certificates/rotate Prerequisite: this is the first step of the onboarding chain (certificate → readiness → connectivity-test → submit); rotate the certificate before activating it (see activateCertificate), checking readiness (see getReadiness), or submitting any operation. Performs an evidence-emitting hot-reload of the ICP-Brasil/RSFN certificate pair. The new certificate must already exist on disk under the configured base path; this endpoint only swaps the in-memory pointer and emits an audit record. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. The response NEVER includes the private key path or key material. # Cancel an issued boleto Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/cancel-boleto en/openapi/v3-current/payments.yaml post /v1/boletos/{id}/cancel Cancels a boleto in CREATED status. Reverts pending credit in Midaz. # Cancel a payment Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/cancel-payment en/openapi/v3-current/payments.yaml post /v1/payments/{id}/cancel Cancels a payment in CREATED or SCHEDULED status before execution. # Configure outbound webhook Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/configure-webhook en/openapi/v3-current/payments.yaml post /v1/admin/webhooks Registers or updates the callback URL for receiving status change notifications. One configuration per tenant. # Connect provider via OAuth2 authorization code Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/connect-provider en/openapi/v3-current/payments.yaml post /v1/admin/providers/connect Exchanges an OAuth2 authorization code for access/refresh tokens to enable provider API access for the tenant. # Download boleto PDF Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/download-boleto-pdf en/openapi/v3-current/payments.yaml get /v1/boletos/{id}/pdf Streams the FEBRABAN-rendered PDF for a previously emitted boleto. The PDF is fetched on demand from the provider; if the provider has not finished registering the boleto, the response is 503 with a Retry-After header. # Get boleto by ID Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/get-boleto en/openapi/v3-current/payments.yaml get /v1/boletos/{id} Retrieves a single boleto by its ID with full detail. # Get boleto dashboard Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/get-boleto-dashboard en/openapi/v3-current/payments.yaml get /v1/dashboards/boletos Returns aggregated counts, amounts, and breakdowns of boletos for the specified period. # Get installment series Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/get-installment-series en/openapi/v3-current/payments.yaml get /v1/boletos/installments/{series_id} Retrieves the status and boletos of an installment series by its ID. # Get payment by ID Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/get-payment en/openapi/v3-current/payments.yaml get /v1/payments/{id} Retrieves a single payment by its ID with full detail including DARF fields if applicable. # Get payment dashboard Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/get-payment-dashboard en/openapi/v3-current/payments.yaml get /v1/dashboards/payments Returns aggregated counts, amounts, and breakdowns of payments for the specified period. # Get webhook configuration Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/get-webhook-configuration en/openapi/v3-current/payments.yaml get /v1/admin/webhooks Retrieves the current webhook configuration for the tenant. # Initiate a DARF payment Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/initiate-darf-payment en/openapi/v3-current/payments.yaml post /v1/payments/darf Initiates a DARF (federal tax) payment with specific tax fields. # Initiate a payment Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/initiate-payment en/openapi/v3-current/payments.yaml post /v1/payments Initiates a bankslip or utilities bill payment. The type is classified automatically from the digitable line. # Issue a single boleto Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/issue-boleto en/openapi/v3-current/payments.yaml post /v1/boletos Creates a new boleto (TRADITIONAL or HYBRID) via the banking provider. # Issue installment boletos Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/issue-installment-boletos en/openapi/v3-current/payments.yaml post /v1/boletos/installments Creates a series of installment boletos with automatic amount distribution. # List boletos Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/list-boletos en/openapi/v3-current/payments.yaml get /v1/boletos Lists boletos with optional filters for status, type, kind, dates, and pagination. # List payments Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/list-payments en/openapi/v3-current/payments.yaml get /v1/payments Lists payments with optional filters for status, type, dates, and pagination. # Receive provider webhook Source: https://docs.lerian.studio/en/reference/midaz/plugins/payments/receive-provider-webhook en/openapi/v3-current/payments.yaml post /v1/webhooks/btg Receives and persists a webhook event from the payment provider. Uses the provider's own Bearer token for authentication. # Cancel a Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/cancel-transfer en/openapi/v3-current/ted.yaml post /v1/transfers/{transferId}/cancel Use this endpoint to cancel a transfer that has not yet been submitted for processing. Only transfers in CREATED or PENDING status can be cancelled. Cancelling a transfer releases the held funds in Midaz and sets the status to CANCELLED. Calling cancel on an already-cancelled transfer returns success. This endpoint takes no request body — the transfer is identified by the path parameter alone. # Create a Webhook Registration Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/create-webhook en/openapi/v3-current/ted.yaml post /v1/webhooks Use this endpoint to register a tenant-scoped webhook endpoint that receives transfer event notifications. The registration is scoped to the caller's tenant, derived from the bearer JWT (`tenantId` claim), never from a request header. The server generates the `signingSecret` and returns it **exactly once** in this response. Store it securely — it is never returned again. Use it to verify the signature on every delivered event. To obtain a new secret, call the rotate endpoint. Use the `X-Idempotency` header for guaranteed deduplication. Repeated calls with the same key replay the cached response. # Delete a Webhook Registration Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/delete-webhook en/openapi/v3-current/ted.yaml delete /v1/webhooks/{webhookId} Use this endpoint to soft-delete a webhook registration. After deletion the endpoint stops receiving events. Only registrations belonging to the caller's tenant can be deleted. Use the `X-Idempotency` header for guaranteed deduplication. # Get dashboard summary Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/get-dashboard-summary en/openapi/v3-current/ted.yaml get /v1/transfers/dashboard/summary Returns aggregated transfer counters and operational health snapshot for dashboards. # Get a Webhook Registration Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/get-webhook en/openapi/v3-current/ted.yaml get /v1/webhooks/{webhookId} Use this endpoint to retrieve a single webhook registration by ID. Only returns registrations belonging to the caller's tenant. The signing secret is never included in this response. # Initiate a Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/initiate-transfer en/openapi/v3-current/ted.yaml post /v1/transfers/initiate Use this endpoint to initiate a transfer by validating sender data, checking operating hours and usage limits, detecting duplicates, and calculating the fee. No funds are held at this step. The returned initiationId is valid for 24 hours and must be confirmed via the Process Transfer endpoint. Use the `X-Idempotency` header for guaranteed deduplication. # List pending reconciliation transfers Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/list-pending-reconciliation en/openapi/v3-current/ted.yaml get /v1/transfers/reconciliation/pending Lists transfers flagged for manual reconciliation after unknown-state failures. # List unresolved TED IN reconciliation gaps Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/list-ted-in-reconciliation-gaps en/openapi/v3-current/ted.yaml get /v1/transfers/ted-in/reconciliation-gaps Use this endpoint to list durable traces of ambiguous TED IN inbound-message outcomes that an operator must verify. This is a read-only administrative endpoint; the resolve or close action is a separate concern. It is organization-independent and does not require the `X-Organization-Id` header. In multi-tenant deployments the results are scoped to the caller's resolved tenant database. # List Transfers Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/list-transfers en/openapi/v3-current/ted.yaml get /v1/transfers Use this endpoint to retrieve a paginated list of transfers. You can filter by type, status, date range, and amount range. The default limit is 50 items, with a maximum of 200. Only returns transfers belonging to the same organization. # List DLQ Messages Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/list-webhook-dlq-messages en/openapi/v3-current/ted.yaml get /v1/webhooks/dlq Use this endpoint to list messages currently in the webhook dead-letter queue, scoped to the caller's tenant. The tenant is derived from the bearer JWT (`tenantId` claim), never from a request header. This route is only mounted when authentication is enabled and a DLQ inspector is configured. Otherwise it is not exposed and any request is rejected with 503. # List Webhook Event Types Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/list-webhook-event-types en/openapi/v3-current/ted.yaml get /v1/webhooks/event-types Use this endpoint to retrieve the deterministic inventory of event types accepted in webhook registrations. Use these values to populate the `eventTypes` field when creating or patching a registration. # List Webhook Registrations Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/list-webhooks en/openapi/v3-current/ted.yaml get /v1/webhooks Use this endpoint to retrieve a paginated list of webhook registrations for the caller's tenant. Set `enabled=true` to return only enabled registrations. Signing secrets are never included in this response. # Prepare Transfer Signing Payload Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/prepare-transfer-signing en/openapi/v3-current/ted.yaml post /v1/transfers/signing/prepare Use this endpoint to freeze the canonical STR0008 payload for a previously initiated TED OUT transfer and obtain the exact bytes that must be signed before calling Process Transfer. The response includes a `signingArtifactId` and a reserved `transferId`. The reserved `transferId` is not queryable until Process Transfer succeeds. Use the `X-Idempotency` header for guaranteed deduplication. Repeated calls with the same key replay the cached response. # Process a Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/process-transfer en/openapi/v3-current/ted.yaml post /v1/transfers/process Use this endpoint to confirm and execute a previously initiated transfer. The system validates the initiationId, checks the sender's balance, holds funds in Midaz, and routes the transfer to JD SPB (for TED OUT) or creates an internal Midaz transaction (for P2P). Submitting the same initiationId returns the same transferId. # Replay TED IN Backlog Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/replay-ted-in-poll en/openapi/v3-current/ted.yaml post /v1/transfers/ted-in/replay Use this endpoint to force one immediate replay pass over persisted, unprocessed JD inbound messages for the current tenant. Unlike `/ted-in/poll`, this route does not read JD's destructive queue — it only reprocesses durable backlog rows already persisted locally. Use the `X-Idempotency` header for guaranteed deduplication. # Resume TED IN Poller Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/resume-ted-in-poller en/openapi/v3-current/ted.yaml post /v1/transfers/ted-in/resume Use this endpoint to clear the fail-closed at-most-once receive latch and re-arm a latched or dead TED IN poller without restarting the pod. This is a higher-privilege operator endpoint than `/ted-in/poll` because it re-opens JD destructive-read consumption. Reach for it only when auto-recovery cannot revive the poller — a panicked multi-tenant child or a single-tenant poller past the panic hard-cap. Resume never bypasses the money-path durable-gap gate. When the live receive-state-unknown latch is set, the current latch's reconciliation-gap row must be durable before the poller resumes; otherwise the request is refused with `409` and the poller stays dead. Verify the pending gap with [List TED IN Reconciliation Gaps](/en/reference/midaz/plugins/ted/list-ted-in-reconciliation-gaps) before calling resume. The optional body `{acknowledge, note}` additionally marks the current latch's reconciliation gap resolved and stamps an operator annotation on it. `acknowledge` defaults to `false`, leaving the gap OPEN so it remains visible for review. A healthy, not-latched poller returns `200` with `resumed: false` and `reason: "not_latched"` — the call is a safe idempotent no-op. Same-key retries replay the cached response. The route is organization-independent and does not require the `X-Organization-Id` header — the tenant is resolved from the authenticated context. In multi-tenant deployments, an inactive tenant is rejected with `503`. # Get a Transfer Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/retrieve-transfer en/openapi/v3-current/ted.yaml get /v1/transfers/{transferId} Use this endpoint to retrieve the current status and details of a transfer, including its type, status history, sender and recipient details, amounts, and timestamps. Only returns transfers belonging to the same organization. # Retry DLQ Message Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/retry-webhook-dlq-message en/openapi/v3-current/ted.yaml post /v1/webhooks/dlq/{messageId}/retry Use this endpoint to re-publish a specific DLQ message back to the main webhook processing queue. Only messages belonging to the caller's tenant are eligible. The tenant is derived from the bearer JWT (`tenantId` claim). This route is only mounted when authentication is enabled and a DLQ inspector is configured. Otherwise it is not exposed and any request is rejected with 503. # Rotate a Webhook Signing Secret Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/rotate-webhook-signing-secret en/openapi/v3-current/ted.yaml post /v1/webhooks/{webhookId}/signing-secret/rotate Use this endpoint to generate a new server-owned signing secret for a webhook registration. The new `signingSecret` is returned **only** in this response — store it securely and update your signature verification. The previous secret stops being valid. Only registrations belonging to the caller's tenant can be rotated. Use the `X-Idempotency` header for guaranteed deduplication. # Bank Transfer error list Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/ted-error-list **Bank Transfer** returns consistent and structured error responses. This helps you quickly understand what went wrong and how to fix it. **Error format** ```json JSON theme={null} { "error": { "code": "", "service": "", "category": "", "message": "", "requestId": "", "fields": {} } } ``` **Field definitions** * **`code`** – A stable, unique identifier for the error (e.g. `BTF-0010`). JD SPB rejections pass through the raw vendor code (e.g. `AAC90`); transport-level failures use the synthetic marker `TRANSPORT`. Match on this value rather than on the HTTP status. * **`service`** – The service or domain that produced the error (e.g. `plugin`, `crm`, `midaz`, `fees`, `jd_spb`). * **`category`** – Machine-readable error category for retry decisions: `deterministic`, `transient`, `rate_limit`, or `plugin`. * **`message`** – Detailed guidance to help you resolve the error. * **`requestId`** – Correlation ID for the request. Present even when empty; include it in support requests. * **`fields`** – Optional. Structured validation or retry metadata (field-level errors, limit details, and similar). This structure ensures you always get actionable feedback when something doesn't go as expected. In the tables below, the **title** column is a human-readable label for convenience — it is not a field in the response envelope. The envelope returns `code`, `service`, `category`, `message`, and `requestId`. Match on `error.code`. ## Bank Transfer errors *** The following errors can occur when interacting with the Bank Transfer endpoints. Each error follows our standard structure, making it easier to debug and respond to issues programmatically. Refer to the tables below for a list of possible error codes, what they mean, and how to resolve them. ### 400 | `code` | `title` | `message` | | -------- | ------------- | ------------------------------------------------------------------- | | BTF-0001 | Invalid Input | The request contains invalid fields. Check the field details below. | ### 401 | `code` | `title` | `message` | | -------- | ------------ | ----------------------------------------------------------------- | | BTF-0401 | Unauthorized | Authentication failed. The token is missing, invalid, or expired. | ### 403 | `code` | `title` | `message` | | -------- | --------------- | ----------------------------------------------------------------------- | | BTF-0403 | License Invalid | Organization is not licensed. Contact support to activate your license. | | BTF-0405 | Forbidden | Insufficient permissions to perform this action. | ### 404 | `code` | `title` | `message` | | -------- | -------------------- | --------------------------------------------------------- | | BTF-0200 | Transfer Not Found | Transfer not found | | BTF-0201 | Initiation Not Found | Initiation not found or belongs to different organization | | BTF-0500 | Account Not Found | Sender account does not exist in CRM | ### 409 | `code` | `title` | `message` | | -------- | ------------------ | ------------------------------------------ | | BTF-0012 | Duplicate Transfer | Duplicate transfer detected | | BTF-0203 | Already Processed | This initiation has already been processed | ### 410 | `code` | `title` | `message` | | -------- | ------------------ | ----------------------------------------------------------- | | BTF-0202 | Initiation Expired | Initiation expired after 24 hours. Create a new initiation. | ### 422 | `code` | `title` | `message` | | -------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | BTF-0010 | Operating Hours Violation | Transfers can only be initiated Monday-Friday between 06:30 and 17:00 Brasília time | | BTF-0011 | Limit Exceeded | Transfer amount exceeds daily limit | | BTF-0204 | Cannot Cancel | Transfer cannot be cancelled in its current status. Only CREATED or PENDING transfers can be cancelled. | | BTF-0501 | CRM Invalid Response | The CRM account record is missing a required field (`organizationId`). Contact your platform team to fix the account data in the CRM. | Errors returned by the JD SPB integration are not wrapped in `BTF-*` codes. The vendor code is passed through verbatim on the `error.code` field of the HTTP error envelope (for example, `ACE95` for request timeouts, `AAC90` for invalid signature rejections, `ALN01` for control-number-not-found responses). Refer to the JD SPB vendor documentation for the full list and the corresponding retry policy for each code. ### 429 | `code` | `title` | `message` | | -------- | ------------ | ---------------------------------------------------------------------------------- | | BTF-0429 | Rate Limited | Too many requests. Retry after the interval indicated by the `Retry-After` header. | Rate-limit responses use the `rate_limit` category and include a `Retry-After` header. Back off using HTTP status `429` and that header. ### 500 | `code` | `title` | `message` | | -------- | -------------- | ------------------------------------------------------------- | | BTF-9000 | Internal Error | An unexpected error occurred. Contact support if it persists. | ### 502 | `code` | `title` | `message` | | -------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | BTF-3001 | Fees Invalid Response | The fee service returned an invalid or unparseable response. Contact your platform team. | | BTF-0501 | CRM Invalid Response | The CRM returned an ambiguous or unparseable response. Contact your platform team. (BTF-0501 also appears under 422 when the CRM record is missing a required field.) | ### 503 | `code` | `title` | `message` | | -------- | ----------------------- | ------------------------------------------------------------------------------------------------- | | BTF-0502 | CRM Service Unavailable | Unable to validate account. The CRM service is temporarily unavailable. Try again later. | | BTF-2000 | Midaz Unavailable | Unable to process transfer. The Midaz ledger service is temporarily unavailable. Try again later. | | BTF-3000 | Fee Service Unavailable | Unable to calculate fee. The fee service is temporarily unavailable. Try again later. | # Trigger reconciliation sweep Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/trigger-reconciliation-sweep en/openapi/v3-current/ted.yaml post /v1/transfers/reconciliation/tick Forces one immediate reconciliation cycle for the current tenant. This operator endpoint runs the same reconciliation sweep used by the scheduled background worker. Repeated calls with the same `X-Idempotency` key replay the cached response. # Trigger TED IN Poller Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/trigger-ted-in-poll en/openapi/v3-current/ted.yaml post /v1/transfers/ted-in/poll Use this endpoint to force one immediate JD inbound poll cycle for the current tenant. This is an operational endpoint intended to accelerate receipt of JD return messages without waiting for the scheduled poll interval. It does not require the `X-Organization-Id` header — the tenant is derived from the authenticated context. Same-key retries replay the cached response instead of draining JD again. An in-flight duplicate is rejected with 409 by the route-level idempotency middleware. # Patch a Webhook Registration Source: https://docs.lerian.studio/en/reference/midaz/plugins/ted/update-webhook en/openapi/v3-current/ted.yaml patch /v1/webhooks/{webhookId} Use this endpoint to update one or more fields of a webhook registration. Only the fields present in the request body are changed. Sending `eventTypes` as `null` clears the filter so the registration receives every event type. Only registrations belonging to the caller's tenant can be patched. Use the `X-Idempotency` header for guaranteed deduplication. # Get the business-day calendar Source: https://docs.lerian.studio/en/reference/rails/siloc/get-business-day-calendar en/openapi/v3-current/siloc.yaml get /api/v1/siloc/schedule/calendar The honored national holiday calendar for a year plus the verified BCB-artifact provenance (source, retrieval instant, sha256, covered years), projected verbatim from the compiled artifact (D-4). year defaults to the current year; a year outside the artifact's coverage returns a clean 404. # Get a certificate Source: https://docs.lerian.studio/en/reference/rails/siloc/get-certificate en/openapi/v3-current/siloc.yaml get /api/v1/siloc/certificates/{certificateId} # Get an OT settlement cycle Source: https://docs.lerian.studio/en/reference/rails/siloc/get-cycle en/openapi/v3-current/siloc.yaml get /api/v1/siloc/cycles/{cycleId} The cycle detail including the FI's net position (projected from the single correlated instruction, never a sum), windows, and settlement moment. # Get a cycle's recálculo round chain Source: https://docs.lerian.studio/en/reference/rails/siloc/get-cycle-recalculations en/openapi/v3-current/siloc.yaml get /api/v1/siloc/cycles/{cycleId}/recalculations The ordered Art.18 recálculo rounds for the cycle (oldest first), each projected verbatim — the excluded participant, the recomputed side + amount, the complement window (a later round surfaces the +15min prorrogation), and the internal resolution label — plus a note that any post-exclusion residual is a bilateral participant TED (surfacing, not routing). An unknown cycle is 404; a cycle that never entered recálculo is an empty chain. # Get a cycle's reconciliation result Source: https://docs.lerian.studio/en/reference/rails/siloc/get-cycle-reconciliation en/openapi/v3-current/siloc.yaml get /api/v1/siloc/cycles/{cycleId}/reconciliation The reconciliation of the settled instruction against the active FINAL ROC revision, with reason, completeness, and any mismatch flagged. # Get notification configuration Source: https://docs.lerian.studio/en/reference/rails/siloc/get-notification-config en/openapi/v3-current/siloc.yaml get /api/v1/siloc/notifications/config The singleton notification configuration and its optimistic-concurrency version. # Get a participant Source: https://docs.lerian.studio/en/reference/rails/siloc/get-participant en/openapi/v3-current/siloc.yaml get /api/v1/siloc/participants/{participantId} # Get a participant's operational status Source: https://docs.lerian.studio/en/reference/rails/siloc/get-participant-status en/openapi/v3-current/siloc.yaml get /api/v1/siloc/participants/{participantId}/status The current PAG0101 operational status and its history. # Get the card-domain relay status Source: https://docs.lerian.studio/en/reference/rails/siloc/get-relay-status en/openapi/v3-current/siloc.yaml get /api/v1/siloc/relay/status Whether the card-domain relay to Lerian SLC is healthy. When down, card settlement is blocked — this is the single point of SILOC connectivity for the card product. # Get an OT settlement instruction Source: https://docs.lerian.studio/en/reference/rails/siloc/get-settlement-instruction en/openapi/v3-current/siloc.yaml get /api/v1/siloc/settlement-instructions/{instructionId} # Ingest a semantic ROC revision Source: https://docs.lerian.studio/en/reference/rails/siloc/ingest-roc en/openapi/v3-current/siloc.yaml post /api/v1/siloc/rocs Decodes and admits one revision of the SILOC_SIMULATOR semantic ROC (Resultado da Compensação) into its per-document chain. An exact replay (same natural key + content hash) is a committable no-op — the response is identical. D-4: the revision is projected verbatim, never netted or recomputed. # List operational alerts Source: https://docs.lerian.studio/en/reference/rails/siloc/list-alerts en/openapi/v3-current/siloc.yaml get /api/v1/siloc/alerts The operational alerts, filtered by type and (by default) active only, keyset-paged newest first. A RECALCULATION alert carries its correlated cycle's active recálculo round complement window, read verbatim from ot_recalculations — never netted, summed, or recomputed (D-4). A connection-level alert (e.g. RELAY_DOWN) carries no cycle and no window. activeOnly=false includes deactivated alerts (history). Read-only — cross-operator acknowledgement is handled by br-desk, not this rail. # List audit records Source: https://docs.lerian.studio/en/reference/rails/siloc/list-audit-records en/openapi/v3-current/siloc.yaml get /api/v1/siloc/audit-records The append-only audit trail of privileged mutations (obligations, configuration changes, relayed messages, access), keyset-paged newest occurred first. Filterable by occurred_at range (from/to, inclusive), action type, and participant ISPB. Every field is a stored column projected verbatim — no counts, no aggregates, no derived durations (D-4). Read-only: the trail is never mutated through this surface. # List settlement-coverage capabilities Source: https://docs.lerian.studio/en/reference/rails/siloc/list-capabilities en/openapi/v3-current/siloc.yaml get /api/v1/siloc/capabilities Settlement-coverage transparency — the messages/flows the rail implements versus what SILOC defines (the CATALOG.md-backed view). # List regulated certificates Source: https://docs.lerian.studio/en/reference/rails/siloc/list-certificates en/openapi/v3-current/siloc.yaml get /api/v1/siloc/certificates The certificate lifecycle view with expiry. The private key stays in the FI's custody and is never stored or accessed by the rail. # List OT settlement cycles Source: https://docs.lerian.studio/en/reference/rails/siloc/list-cycles en/openapi/v3-current/siloc.yaml get /api/v1/siloc/cycles A filterable, cursor-paginated page of the rail's own OT deferred-net cycles, newest first. # List participants Source: https://docs.lerian.studio/en/reference/rails/siloc/list-participants en/openapi/v3-current/siloc.yaml get /api/v1/siloc/participants # List parked relay failures Source: https://docs.lerian.studio/en/reference/rails/siloc/list-relay-failures en/openapi/v3-current/siloc.yaml get /api/v1/siloc/dlq/relay-failures A cursor-paginated feed of funding frames parked (RELAY_FAILED) during a sustained relay outage, awaiting administrative replay. Distinct from QM-quarantined poison frames. # List relayed card-domain messages Source: https://docs.lerian.studio/en/reference/rails/siloc/list-relayed-messages en/openapi/v3-current/siloc.yaml get /api/v1/siloc/relay/messages A cursor-paginated audit feed of funding/clearing messages relayed to Lerian SLC over the SILOC connection. # List contingency schedule changes Source: https://docs.lerian.studio/en/reference/rails/siloc/list-schedule-changes en/openapi/v3-current/siloc.yaml get /api/v1/siloc/schedule/changes The operator-recorded contingency schedule changes, keyset-paged newest announced first. Each row is an out-of-band-received Núclea announcement (OP002 §14 = telephone/e-mail) recorded verbatim by an operator (decision #1); br-siloc parses no Núclea schedule wire and computes no schedule (D-4). Read-only — the feed is projected, never mutated through this surface. # List OT cycle windows Source: https://docs.lerian.studio/en/reference/rails/siloc/list-schedule-windows en/openapi/v3-current/siloc.yaml get /api/v1/siloc/schedule/windows The compiled canonical OT deposit/settlement window grid (two windows per business day), projected verbatim from the pinned regulated policy — a pure read that needs no datastore and serves with the DB down (D-4). Operator-entered prorrogações are NOT merged here; they stay on listScheduleChanges. # List OT settlement instructions (obligations) Source: https://docs.lerian.studio/en/reference/rails/siloc/list-settlement-instructions en/openapi/v3-current/siloc.yaml get /api/v1/siloc/settlement-instructions A cursor-paginated page of settlement obligations and their funding lifecycle state. # Record a contingency schedule change Source: https://docs.lerian.studio/en/reference/rails/siloc/record-schedule-change en/openapi/v3-current/siloc.yaml post /api/v1/siloc/schedule/changes Records an out-of-band-received Núclea schedule announcement (OP002 §14 = telephone/e-mail) as an operator-entered operational fact, projected verbatim. br-siloc parses no Núclea schedule wire and computes no schedule (D-4). The recorded change appends an audit row, raises the single active SCHEDULE_CHANGE alert (deadline_at = effectiveAt), and emits a schedule.change-recorded fact — all atomically. # Register a certificate reference Source: https://docs.lerian.studio/en/reference/rails/siloc/register-certificate en/openapi/v3-current/siloc.yaml post /api/v1/siloc/certificates Registers the public certificate + custody reference for lifecycle tracking. The private key is never transmitted. # Register a participant Source: https://docs.lerian.studio/en/reference/rails/siloc/register-participant en/openapi/v3-current/siloc.yaml post /api/v1/siloc/participants Registers a direct or indirect participant in scope. Emits a participant.registered fact. # Replay a parked relay failure Source: https://docs.lerian.studio/en/reference/rails/siloc/replay-relay-failure en/openapi/v3-current/siloc.yaml post /api/v1/siloc/dlq/relay-failures/{id}/replay Re-relays the parked funding frame {id} to Lerian SLC once the relay has recovered. Idempotent via the inbound dedup ledger; an unknown or already-resolved id is a 404. # Revoke a certificate Source: https://docs.lerian.studio/en/reference/rails/siloc/revoke-certificate en/openapi/v3-current/siloc.yaml post /api/v1/siloc/certificates/{certificateId}/revoke Disables the running MQ credential before recording revocation. The request fails if runtime enforcement is unavailable. Repeated requests preserve the original revocation timestamp. # Update notification configuration Source: https://docs.lerian.studio/en/reference/rails/siloc/update-notification-config en/openapi/v3-current/siloc.yaml put /api/v1/siloc/notifications/config Replaces the notification configuration under optimistic locking; the update applies only if expectedVersion still matches the stored version (else 409). # Update a participant Source: https://docs.lerian.studio/en/reference/rails/siloc/update-participant en/openapi/v3-current/siloc.yaml put /api/v1/siloc/participants/{participantId} # Report an operation's transmission status Source: https://docs.lerian.studio/en/reference/rails/slc/connectivity-operation-transmission-status en/openapi/v3-current/slc.yaml get /v1/connectivity/operations/{id}/transmission-status Reports the transmission/transport status of an operation: its lifecycle state, the tenant's active outbound transport (REST / CD), the in-flight transmit-claim marker (transmitClaimedAt), and the derived transport diagnostics (waitingFor / stuckSince / lastError). Every field is derived from existing columns/state and the append-only state log. RBAC: connectivity:admin. The tenant is ALWAYS taken from the validated request context. # Retransmit a QUEUED operation Source: https://docs.lerian.studio/en/reference/rails/slc/connectivity-retransmit-operation en/openapi/v3-current/slc.yaml post /v1/connectivity/operations/{id}/retransmit Re-sends a QUEUED operation's ASLC envelope over its tenant-selected active transport, reusing the transmit-claim guard so a concurrent/duplicate retransmit cannot double-send (a transmit already in flight returns 409). RBAC: connectivity:admin. The tenant is ALWAYS taken from the validated request context, never the path. The REST rail is functional; a managed file-transfer or message-broker transport is not currently available and returns 503. A non-QUEUED operation returns 409 INVALID_STATE_TRANSITION with currentState + allowedStates=[QUEUED]. # Get one tenant config key Source: https://docs.lerian.studio/en/reference/rails/slc/get-tenant-config en/openapi/v3-current/slc.yaml get /admin/config/{key} Returns the request tenant's effective value for a fenced config key: the stored override when present, else the catalog default. An off-catalog key is 404. RBAC: config:read; tenant is always derived from the validated request identity, never a path or payload value. # List clearing positions Source: https://docs.lerian.studio/en/reference/rails/slc/list-clearing-positions en/openapi/v3-current/slc.yaml get /v1/clearing/positions Lists the settlement institution's (IF Liquidante) clearing positions for the request tenant: the SLC0001 (clearing net-position notice) net position per participant by stage (PREVIEW/FINAL). This is the clearing PREVIEW/POSITION notice only — it carries NO deposit window or action. Filters: referenceDate (YYYY-MM-DD), cycle, stage. Offset/limit pagination WITHOUT a total count. RBAC: clearing:read; tenant is always derived from the validated request identity. # List dead letters Source: https://docs.lerian.studio/en/reference/rails/slc/list-dead-letters en/openapi/v3-current/slc.yaml get /admin/dlq/{queue} Lists the request tenant's parked dead letters for the given queue (rsfn or sfn) NON-destructively — the messages the RSFN / SILOC ingest drivers could not process — available for administrative replay. queue is validated against the allowlist (unknown → 404). RBAC: dlq:read; tenant is always derived from the validated request identity, never a path or payload value. # List effective tenant config Source: https://docs.lerian.studio/en/reference/rails/slc/list-tenant-config en/openapi/v3-current/slc.yaml get /admin/config Lists the request tenant's EFFECTIVE per-tenant runtime config: every fenced connectivity key with its overridden value or the catalog default, ordered by key. RBAC: config:read; tenant is always derived from the validated request identity, never a path or payload value. Selection/policy only — no secret material. # Record a client settlement-deposit ack Source: https://docs.lerian.studio/en/reference/rails/slc/record-funding-ack en/openapi/v3-current/slc.yaml post /v1/funding/settlement-instructions/{instructionId}/ack Records the client's STR deposit ack for a relayed position notify and drives the settlement instruction to ACKED for a SENT deposit (from PENDING or NOTIFIED) (the client owns the STR connection; the SLC emits NO LDL0022). A FAILED result is recorded but leaves the instruction unchanged so the deposit can be re-driven. Synchronous and definitive. Idempotent via the Idempotency-Key header (a retry carrying the same key replays the stored response). result must be SENT or FAILED (coded 400 otherwise). RBAC: funding:write; tenant is always derived from the validated request identity, never the payload. # Trigger a credit-dispatch catch-up drain Source: https://docs.lerian.studio/en/reference/rails/slc/recovery-dispatch en/openapi/v3-current/slc.yaml post /v1/connectivity/dispatch Drains the QUEUED credit backlog once for the request tenant, the same work the schedule worker runs. RBAC: connectivity:admin. Window-gated internally: outside the credit window it is a no-op that still returns 202. OUTBOUND (transmits to Núclea): serialized per tenant with the schedule worker via a shared drain lock — 409 on contention, 503 when the lock backend is unavailable (never transmits unlocked). # Trigger an ASLC022 forward-to-domicile catch-up drain Source: https://docs.lerian.studio/en/reference/rails/slc/recovery-forward en/openapi/v3-current/slc.yaml post /v1/connectivity/forward Drains the ACCEPTED backlog once for the request tenant, forwarding each operation to its IF Domicílio (ASLC022), the same work the schedule worker runs. RBAC: connectivity:admin. Window-gated internally: outside the forward window it is a no-op that still returns 202. OUTBOUND (transmits to Núclea): serialized per tenant with the schedule worker via a shared drain lock — 409 on contention, 503 when the lock backend is unavailable. # Trigger a return-poll catch-up drain Source: https://docs.lerian.studio/en/reference/rails/slc/recovery-repoll en/openapi/v3-current/slc.yaml post /v1/connectivity/repoll Derives the participants awaiting a return for the request tenant and drives the ProcessReturn pipeline (poll PRO/RET → correlate → transition) once per participant, the same work the schedule worker runs. RBAC: connectivity:admin. INBOUND and NOT window-gated. It drives the idempotent ProcessReturn pipeline, so it runs lock-free and is safe to run concurrently with the worker. # Reprocess pending returns for one participant Source: https://docs.lerian.studio/en/reference/rails/slc/recovery-reprocess en/openapi/v3-current/slc.yaml post /v1/connectivity/reprocess/{participantIspb} Drives the ProcessReturn pipeline once for a single participant (poll PRO/RET → correlate → transition), the same per-participant work the return poll runs. RBAC: connectivity:admin. The participant ISPB is a path RESOURCE selector; the tenant is ALWAYS taken from the validated request context, never the path. INBOUND and NOT window-gated; it drives the idempotent ProcessReturn pipeline, so it runs lock-free. # Un-park operations held after exhausting their outbound attempt budget Source: https://docs.lerian.studio/en/reference/rails/slc/recovery-unpark en/openapi/v3-current/slc.yaml post /v1/connectivity/unpark Clears the outbound transmit claim of the named operations, returning them to the eligible backlog so the next drain tick re-drives them. RBAC: connectivity:admin. It is the ONLY recovery path for a PARKED operation. BOTH outbound stages park for the SAME reason, reported in parkReason as attempt_budget_exhausted: N consecutive counterpart refusals exhausted the operation's attempt budget (SCHEDULE_DISPATCH_MAX_TRANSMIT_ATTEMPTS, default 20). What differs is the stage and the state left behind, which the alert reports in stage: a FORWARD file (ASLC022/024/032/061/065 to the IF Domicilio) leaves the operation ACCEPTED, re-driven by the forward tick; a DISPATCH file (ASLC027/029/031/060/064 to Núclea) leaves it QUEUED, re-driven by the dispatch tick. The value permanent_rejection is RETIRED and no longer emitted - no single 4xx response carries the evidence to park a file - but historical alerts still carry it. Either way the held claim makes it invisible to every drain, and every other admin verb is a dead end: /advance rejects the no-op self-edge, /requeue is CREATED-only, /retry is REJECTED-only, and /retransmit either rejects the state (a parked forward op is ACCEPTED, not QUEUED) or loses its own claim CAS and answers 409 in-flight (a parked dispatch op IS QUEUED). Enumerate the ids with GET /v1/operations?transmitHold=parked, which is the authoritative, paginated, tenant-scoped list; the operation.forward_rejected alert (payload operationIds) and the park's ERROR log field parked_operation_ids are first-response excerpts of the same set, and the daily webhook names only the first file parked that day. There is no scope-wide form: a held transmit claim is AMBIGUOUS between a parked operation and one whose file is already at Núclea, and only the caller decides which set to release. This verb REFUSES the second kind with 409 when it is marked as such (transmitHold=in_flight) - reconcile those with Núclea and drive each one with POST /v1/operations/{operationId}/advance instead, never un-park them. Idempotent: re-running releases nothing further and reports released=0. Claims younger than the 5-minute safety floor are skipped (they may belong to a pass that is mid-submit). Un-parking also zeroes the attempt budget, so a recovered operation gets a full budget rather than re-parking on its next refusal. Runs lock-free: it transmits nothing. # Redispatch an outbox entry Source: https://docs.lerian.studio/en/reference/rails/slc/redispatch-outbox-entry en/openapi/v3-current/slc.yaml post /admin/outbox/{id}/redispatch Resets the terminally-INVALID transactional-outbox row {id} back to PENDING for the request tenant so the relay dispatcher republishes it on its next tick. The row id is PRESERVED (the event is never recreated). Idempotent: a row that is not INVALID is left untouched and returns redispatched=false with its current status — a PUBLISHED row is never re-sent. An unknown id → 404. Multi-tenant is not yet supported → 501 (single-tenant only until outbox schema alignment). RBAC: outbox:redispatch; tenant is always derived from the validated request identity, never a path or payload value. # Replay a dead letter Source: https://docs.lerian.studio/en/reference/rails/slc/replay-dead-letter en/openapi/v3-current/slc.yaml post /admin/dlq/{queue}/{id}/replay Reprocesses the dead letter {id} on the given queue (rsfn or sfn) for the request tenant by re-invoking the same in-process ingest path (NOT a work-queue re-publish). Idempotent: the processed_messages dedup trap makes a re-dispatch of an already-processed message a no-op. queue is validated against the allowlist (unknown → 404); an unknown id → 404. RBAC: dlq:replay; tenant is always derived from the validated request identity, never a path or payload value. # Reset one tenant config key Source: https://docs.lerian.studio/en/reference/rails/slc/reset-tenant-config en/openapi/v3-current/slc.yaml delete /admin/config/{key} Clears the request tenant's override for a fenced config key, returning it to the catalog default. Idempotent: resetting a key with no override is a successful no-op. An off-catalog key is 404. Returns 204 No Content. RBAC: config:write; the actor and tenant are always derived from the validated request identity, never a path or payload value. # Set one tenant config key Source: https://docs.lerian.studio/en/reference/rails/slc/set-tenant-config en/openapi/v3-current/slc.yaml put /admin/config/{key} Persists a per-tenant override for a fenced config key. Body: {"value":"..."}. The value is scope-fenced + validated against the catalog: a missing value is a schema 422, an empty value a coded 400, an off-catalog key 404, an off-enum / non-bool value 422. RBAC: config:write; the writing actor and the tenant are always derived from the validated request identity, never a path or payload value. # Register webhook endpoint Source: https://docs.lerian.studio/en/reference/rails/spb/create-event-webhook en/openapi/v3-current/spb.yaml post /v1/str/webhooks Registers a webhook endpoint that receives control-plane event deliveries. The provided secret is encrypted at rest with AES-256-GCM. The eventTypes array is required and must contain canonical control-plane event constants (str.operation.*, str.readiness.*, str.certificate.*, str.approval.*). Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Submit STR0014 reserve-account statement query (message-mode) Source: https://docs.lerian.studio/en/reference/rails/spb/create-reserve-statement-query en/openapi/v3-current/spb.yaml post /v1/str/reserve-statement-queries Submits a single-party STR0014 reserve-account statement (extrato) query to BACEN in MESSAGE-MODE (TpRet=M) and returns a correlation id plus a PENDING state. The answer (STR0014R1, carrying the opening/closing balances and up to 200 settled entries) arrives asynchronously and is read via GET /v1/str/reserve-statement-queries/{id}. If BACEN routes the statement to a file (TamArq/IdentdArq) or the window exceeds the 200-entry message-mode ceiling, the read endpoint returns an explicit error — file retrieval (TpRet=P/X) is out of scope. Lerian SPB projects the statement BACEN sends; it never computes a position. # Submit STR0001 schedule query Source: https://docs.lerian.studio/en/reference/rails/spb/create-schedule-query en/openapi/v3-current/spb.yaml post /v1/str/schedule-queries Submits a single-party STR0001 schedule query (consulta de horários do STR) to BACEN and returns a correlation id plus a PENDING state. The answer (STR0001R1, carrying the STR operating-schedule grid) arrives asynchronously and is read via GET /v1/str/schedule-queries/{id}. This means the query was accepted for dispatch, not that the schedule is available yet. Lerian SPB surfaces the schedule BACEN sends; it computes nothing. # Delete webhook endpoint Source: https://docs.lerian.studio/en/reference/rails/spb/delete-event-webhook en/openapi/v3-current/spb.yaml delete /v1/str/webhooks/{webhookId} Soft-deletes a webhook endpoint. After deletion the endpoint stops receiving deliveries; historical event_delivery / audit_record rows remain for support investigation. Deletes of unknown ids surface as not-found (SPB-0010), byte-identical to truly-missing. # Get webhook endpoint Source: https://docs.lerian.studio/en/reference/rails/spb/get-event-webhook en/openapi/v3-current/spb.yaml get /v1/str/webhooks/{webhookId} Returns a single event-webhook endpoint by id. Encrypted secrets are not exposed. Lookups for unknown ids surface as not-found (SPB-0010), byte-identical to truly-missing. # Read STR0013 reserve-balance query result Source: https://docs.lerian.studio/en/reference/rails/spb/get-reserve-balance-query en/openapi/v3-current/spb.yaml get /v1/str/reserve-balance-queries/{id} Reads the result of a previously submitted STR0013 reserve-account balance query by its correlation id. Returns PENDING until the STR0013R1 answer arrives, then the reserve-account balance BACEN sent (SldRB_CL) plus a freshness timestamp. # Read STR0014 reserve-statement query result (message-mode) Source: https://docs.lerian.studio/en/reference/rails/spb/get-reserve-statement-query en/openapi/v3-current/spb.yaml get /v1/str/reserve-statement-queries/{id} Reads the result of a previously submitted STR0014 reserve-account statement query by its correlation id. Returns PENDING until the STR0014R1 answer arrives, then the opening/closing balances and the verbatim settled-entry group BACEN sent. Returns 422 when the answer requires file-mode retrieval (BACEN returned file metadata or the window exceeded the 200-entry message-mode ceiling) — the entry list is never silently truncated. # Get operating-schedule windows Source: https://docs.lerian.studio/en/reference/rails/spb/get-schedule en/openapi/v3-current/spb.yaml get /v1/str/schedule Returns the operating-window grades announced by GEN0021 schedule-change broadcasts ("GEN avisa alteração de horários"). Each window carries the official grade code, opening/closing datetimes, schedule type and announcing provider, verbatim as announced. A re-announced grade for the same reference date overwrites the previous window. Optionally filter by reference date with dtRef. # Read STR0001 schedule-query result Source: https://docs.lerian.studio/en/reference/rails/spb/get-schedule-query en/openapi/v3-current/spb.yaml get /v1/str/schedule-queries/{id} Reads the result of a previously submitted STR0001 schedule query by its correlation id. Returns PENDING until the STR0001R1 answer arrives, then the STR operating-schedule grid BACEN sent (Grupo_STR0001R1_GrdHrio) plus a freshness timestamp. # Get channel settings Source: https://docs.lerian.studio/en/reference/rails/spb/get-settings en/openapi/v3-current/spb.yaml get /v1/str/settings Returns the STR channel settings, including the participant ISPB binding, delivery mode, and any registered webhook endpoints. When available, the response also embeds the operational readiness summary. # List webhook endpoints Source: https://docs.lerian.studio/en/reference/rails/spb/list-event-webhooks en/openapi/v3-current/spb.yaml get /v1/str/webhooks Returns all registered event-webhook endpoints. Encrypted secrets are not exposed. # Update webhook endpoint Source: https://docs.lerian.studio/en/reference/rails/spb/update-event-webhook en/openapi/v3-current/spb.yaml patch /v1/str/webhooks/{webhookId} Patches a webhook endpoint. URL, secret, eventTypes, isActive and metadata are each independently patchable. URL changes re-run the HTTPS + SSRF check; secret changes re-encrypt at rest; eventTypes changes re-validate against the canonical control-plane catalog. nil fields mean "leave unchanged"; an explicit empty eventTypes slice means "clear the subscription list". # Update participant responsibles (GEN0019) Source: https://docs.lerian.studio/en/reference/rails/spb/update-responsibles en/openapi/v3-current/spb.yaml post /v1/str/responsibles Submits a GEN0019 "Participante informa atualização de responsáveis" announcement to BACEN, declaring the participant's complete responsável roster. FULL-REPLACEMENT: the body carries the entire set (1..100 responsáveis), never a partial update. The submission is persisted with status SUBMITTED before dispatch; BACEN's GEN0019R1 acknowledgment confirms it via NUOp correlation. Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Update channel settings Source: https://docs.lerian.studio/en/reference/rails/spb/update-settings en/openapi/v3-current/spb.yaml patch /v1/str/settings Patches the STR channel settings. Only the provided fields are updated; absent fields keep their current value. At least one field must be provided. Setting the participant ISPB requires an explicit 8-digit value (placeholders such as 00000000 are rejected). Idempotent — replaying the same X-Idempotency key with the same body returns the cached response. # Get the Dataprev credential status Source: https://docs.lerian.studio/en/reference/rails/consignado/get-credential-status en/openapi/v3-current/consignado.yaml get /v1/credentials/dataprev Returns the non-secret status of the tenant's stored Dataprev credentials (cert presence + fingerprint + expiry, oauth presence). When nothing is stored it returns 200 with both presence flags false, NOT 404. NEVER returns secret material. # Upload or rotate the Dataprev client certificate Source: https://docs.lerian.studio/en/reference/rails/consignado/put-certificate en/openapi/v3-current/consignado.yaml put /v1/credentials/dataprev/certificate Stores (or rotates) the tenant's ICP-Brasil A1 client certificate (PEM cert+key) for the Dataprev rail in the tenant secret store and upserts non-secret metadata. The certificate is validated before storage. The response NEVER carries secret material. # Set the tenant's Dataprev codigoSolicitante Source: https://docs.lerian.studio/en/reference/rails/consignado/put-codigo-solicitante en/openapi/v3-current/consignado.yaml put /v1/credentials/dataprev/codigo-solicitante Stores (or corrects) the tenant's Dataprev codigoSolicitante — the institution's BACEN COMPE code, a 3-digit number (Numero 3 algarismos) in the range 001-999, required (Obrigatorio Sim) on nearly every rail operation. A COMPE code of 55 is published as 055; 000 is refused. It is PUBLIC, non-secret metadata held in Postgres beside the credential metadata;