One roof, several cabinets, several drawers
Think of the deployment as a back office with a wall of filing cabinets. Each cabinet belongs to one regulated institution. Its files never mix with the files of another institution, because a regulator audits each institution separately. Inside a cabinet, drawers separate lines of business: one for the wallet product, one for the credit product. On the wall hangs an index card that lists the cabinets and drawers. A clerk files money movements only into a drawer on the index. The clerk hands back a movement for a drawer that is not on the index, and never files it in a “close enough” drawer. The rail’s vocabulary maps one to one:
Two results come from the model:
- Every posting carries its scope. The rail writes each transaction into the organization and ledger that it resolved to. Each institution’s records are therefore complete on their own. Regulatory and accounting reports come out per institution, with no shared book to separate.
- The rail never guesses a scope. It checks a movement’s organization and ledger against the catalog. If it cannot prove them, it refuses the movement before any posting. The design exists to make one mistake impossible: money in the ledger of the wrong institution.
The toggle: an empty key means off
The catalog key starts empty, and empty means not provisioned. The tenant then posts only into the organization and ledger that its binding names, as before the key existed. A catalog write turns the feature on. The pair that the binding names stays the default: the scope of every flow that does not resolve another one.
The default organization and ledger are never in the catalog. Their accounting routes stay in the
routing.* keys, and their asset and clearing account stay in the midaz.* keys. Setting up the rail shows how to provision them. The catalog declares only what the binding does not. A copy of the default ledger in the catalog would give one book two sources of truth, so the rail refuses that catalog.Activate it, step by step
1
Complete the ordinary setup first
The catalog extends a working deployment. It does not replace the setup. Run Setting up the rail from start to end. The binding that you write there makes your institution the default participant and its ledger the default book. If your deployment is one institution with one book, stop there. While the catalog key is empty, nothing on this page changes anything.
2
Provision the additional book in Midaz and the CRM
Provision each additional ledger with the same steps as the setup page, on the new book:
- The organization and the ledger. An additional ledger of your own institution goes under your existing organization. An additional participant gets its own organization, because each Midaz organization holds exactly one regulated institution.
- The asset and the accounts on the new ledger, with its own external clearing account. An alias such as
@external/BRLis unique within a ledger, so each book needs its own. - The operation routes on the new ledger: one credit-and-debit pair for each money profile of the book. These UUIDs do not go into the
routing.*keys. They go inside the catalog document in the next step. - The holder records and aliases in the CRM, with the
X-Organization-Idof the organization that owns the book. The alias’sledgerIdtells the rail which book an account banks in. On an organization with more than one ledger, every alias must name its ledger.
3
Write the catalog
Write the catalog with one call to On a self-hosted deployment, the entry has your own ISPB and your own organization id, and the ledgers are the additional books.A successful write answers The key reloads without a restart. Write it while the app runs, and the running deployment uses it, with no restart and no redeploy.
PUT /system/tenant_policy/organizations. The wrapper is the same as for the binding. The body is {"value": ...}, and value is a string whose content is a JSON document. The document is a list with one entry per catalog organization. Each entry names its participant’s ISPB, its Midaz organization, and that organization’s ledgers with their routes.Your own organization is in the catalog only when it has additional ledgers, and never with the default ledger. An additional participant’s organization is always in it.This is the document, unwrapped, with placeholder identifiers:routes takes up to eleven profiles: in, inQrCode, out, outReversal, intraPsp, intraPspReversal, medDebit, medCredit, pixautomaticoDebit, pixautomaticoReversal, and refundSent. Each profile is a pair of Midaz operation route UUIDs.Only in and inQrCode are mandatory, because every catalog ledger exists to receive. A book that cannot serve either inbound leg shows a provisioning mistake. The other nine are optional. Without refundSent, a devolução that you send posts on out. Any other flow that needs an absent profile refuses with 409 PIX-0105, and the response names the profile, the organization, and the ledger. An unset routing.* key on the default book behaves the same way.Two fields are optional:transactionRoute, inside a profile: the UUID of the Midaz transaction route that this ledger’s postings of the profile name. Set it when the ledger validates routes. Without it, such a ledger refuses the posting. Without the field, the posting carries no transaction route.sharedIspbGate, on an entry:truedeclares that this participant shares its ISPB with another core. Its on-us transfers then ask the Courier ownership map first. The default organization declares the same fact with the keytenant_policy/onus.shared_ispb_gate_enabled. See Courier ownership map.
jq do the string escaping, as with the binding:204. Read the key back as you read the binding back. The value comes back as a quoted string with your document:4
Know what the validators refuse, and when
The rail checks the catalog two times, and the split matters. The write validator catches every error that the document alone shows. The admin surface therefore refuses a malformed catalog before any payment finds it.The write refuses these values, with
400 validation_error:- a
valueother than a string, or anything after the first JSON document. - an unknown field at any depth. A mistyped route-profile name is an unknown field too, so a typo cannot pass as an absent profile.
- an
ispbthat is not exactly 8 digits. - an
organizationId,ledgerId, route leg, ortransactionRoutethat is not a non-zero UUID. - an empty
externalAliasorasset, a route pair without one of its legs, or an organization without a ledger. - a ledger without the
inor theinQrCodeprofile. - the same
ispb,organizationId, orledgerIdtwo times anywhere in the document.
403 means a missing permission, never a refused value.The read refuses other cases, because only the running rail knows your binding and mode. The refusals come on the money path, before any posting:5
Understand how money routes once it is on
- Every transaction goes into its scope. The posting carries the organization and ledger that it resolved to. Each institution’s book therefore stays complete and auditable on its own.
- An inbound credit finds its own book. The receiver ISPB picks the organization. The account’s CRM record picks the ledger. An organization with a single ledger accepts a record that names no ledger, because it has no choice to make. An organization with several ledgers refuses that record with
500 PIX-0120and does not guess. - An outbound order names the paying participant when the tenant is several.
POST /v1/transactionstakes an optionalpayerIspb. A tenant that acts as one participant omits it. A tenant that acts as several must send it. Without it, the order answers422 PIX-0127. An ISPB that the tenant does not act as answers422 PIX-0128. An arbitrary payer could debit the customer of another institution. - Between two ledgers of the same organization, the transfer is internal. An alias is unique within a ledger, so one posting cannot hold both legs. The rail reserves the amount on the payer’s ledger against its clearing account. It then posts the credit on the payee’s ledger against that ledger’s clearing account. Last, it commits the reserve. Both legs use each ledger’s own
intraPsproute pair, and each book stays double-entry on its own. - Between two organizations of the same tenant, the payment is ordinary Pix. Two organizations mean two regulated participants. The money therefore goes over the settlement rail (SPI), as a payment to any other institution does. The same tenant is never “the same book”.
- Each additional participant authenticates as itself. Orders that leave an additional organization are sent to JD under that participant’s own credential, provisioned per ISPB. A participant whose credential is not provisioned refuses its first order with
409 PIX-0092and releases the hold, and the readiness probe reports it down naming that ISPB before traffic reaches it.
Turning it off
You can write an empty string back to
tenant_policy/organizations. The tenant then goes back to the default-only behavior, as before activation. This does not undo anything already booked. Money posted in an additional scope stays in that ledger, and this rail can no longer reach it. A flow that resolves to a scope that the catalog no longer declares refuses with 500 PIX-0120 before it touches the ledger.
So empty the key only when nothing routes to the additional books. That means no accounts whose records point at them, no payments in flight, and no hosted participant on an additional organization. To retire one book and keep the feature, remove that ledger’s entry and keep the rest of the catalog. The same read-time proof applies, scope by scope.
Where to go next
Setting up the rail
The provisioning chain that this page extends: the ledger objects, the CRM records, the routing keys, and the binding that names the default pair.
The direct and indirect model
How this axis differs from hosting indirect participants. An indirect is a position inside your book. An additional organization is a direct participant with books of its own.
Environment variables
The deploy-time configuration, and the values that live in systemplane keys instead.
Pix JD error list
Every
PIX-NNNN code that this page names, with its status and the detail text of the response.
