Before you start
You talk to three services, and confusing them is the most common first mistake.- A bearer token for the ledger, one for the CRM, and one for the plugin. They can come from different audiences.
- The
systemplane:writepermission on the plugin token. Without it the configuration writes answer403. SYSTEMPLANE_ENABLED=trueon the plugin. With it unset, the/systemroute group is not mounted at all and every configuration write answers404.- Your institution’s ISPB: the 8-digit identifier you were accredited with at BACEN.
Provision the ledger and the holder records
These steps run against Midaz and the CRM. They create the accounts the rail posts to and the holder records it resolves counterparties from.Create the organization and the ledger
201 with no id in the body is not a success: nothing can address what was created, not you and not the cleanup afterwards. Stop there rather than carrying an empty id into the next step.Create the BRL asset, and wait for it to appear
BRL.201 and 409 are good answers. An asset is addressed by its code inside a ledger, so “already exists” is indistinguishable from success.Two things happen here, and the second one is easy to miss:- The ledger starts accepting accounts in that asset. Before this, it refuses every account with
0034 Asset Code Not Found. - Midaz creates the
@external/BRLaccount alongside it. It is the one account in a book that can be debited without having been credited first, so it is where the opening balance comes from and it is the counterparty of every settlement with the outside world.
Create one account per role
accountId of every business call you make against the plugin. The alias (@payer) is the short name the book is read and posted by, and it reappears in the next step in the least likely place.Create the holder in the CRM
typefollows the document length.NATURAL_PERSONfor a CPF (11 digits),LEGAL_PERSONfor a CNPJ (14). The plugin converts this type into the number that goes on the wire to JD, so getting it wrong files the party as the wrong kind of person.externalIdhas to be the account’s ledger alias (@payer), and the field name hides that. The CRM describes it as an external correlation identifier, which reads as optional. For this rail it is not: it is where the plugin reads which account in the book belongs to the holder. A holder with noexternalIdproduces an account that resolves, looks complete, and fails every payment.addresses.primary.cityis required for QR codes and Pix Automático. It is the receiver city printed in the code, and the plugin refuses to generate a QR without it, answering422 PIX-0033and pointing atpayee.city. No payment path reads the field, which is why its absence goes unnoticed until someone generates a QR.
Link the holder to the account
Fund the accounts
@external/BRL, created with the asset in step 2.@external/BRL can be named in the body but never in a path. The alias contains a slash and the ledger route does not decode it, so reading its balance by path answers 404 or an empty 200. Posting from it is normal; reading it that way is not.Create the twenty accounting routes
{"id": "..."}, and that id is what the routing keys below hold.Look a route up by title before creating it. Two routes with the same title make the next lookup pick either one, so if you provision the same book more than once, list first.Configure the rail
The plugin’s live configuration lives in its systemplane: values you write over the admin API, which take effect without a redeploy or a restart. Every write is aPUT to /system/<namespace>/<key> with a {"value": ...} body, and 204 No Content is the success — the plane returns no body on a write.
The tenant comes from the validated bearer, never from the URL or the body. In a multi-tenant deployment that means one bearer per tenant and one write per tenant.
GET /system/-/catalog lists every key with its type and description, and GET /system/-/catalog/<namespace>/<key> describes one. The catalog is the source of truth; this page is a copy of it, and copies age.Write the JD integration binding — your ISPB
tenancy/jd_integration_binding, in both single-tenant and multi-tenant mode, and there is no environment fallback. While the key is empty the plugin starts, answers its health probe, looks healthy — and refuses every payment on every money route.What breaks without it. 409 PIX-0092, “Tenant Pix integration not provisioned”. One end-to-end battery collected 86 refusals from this single empty value; the next most frequent code in the same run appeared 6 times. The response text asks you to contact support and does not name the key, so the code is what you search for.The trap: the value is a string that contains JSON. The body is always {"value": ...}, and here value is not an object. It is a string whose content is a JSON document. That is how it is stored, with the inner quotes escaped:jq do the escaping:organizationId or ledgerId from this key — the book it posts to keeps coming from MIDAZ_ORGANIZATION_ID and MIDAZ_LEDGER_ID. The write validator asks for all three anyway. Fill the two UUIDs with the same values those variables carry.Two sources of truth for the same fact could disagree without anyone noticing, which is why the run-time answer stays with the deployment values. In multi-tenant all three fields are read, and they are what resolves each tenant’s JD and Midaz identity.value comes back as an object rather than a quoted string, you wrote the wrong form. If it comes back "", the write did not happen — check the status code of the PUT. The real confirmation is behavioral: money routes that answered 409 PIX-0092 stop answering it.A malformed ISPB cannot be stored through this route. The write validator is the same decoder the read uses, so a 7-digit ISPB or a broken UUID is refused on the spot with 400 validation_error instead of being discovered on the first payment.409 PIX-0121 — “Tenant Pix integration ISPB invalid” — while following this page. It is the code for a binding that is provisioned and whose ispb is not 8 digits, and this route cannot create that state. It appears only when a value reached the key some other way: a direct write to the plugin’s database, or a write made before the validator existed. It is documented because if you ever do see it, its message is the one that names both the key and the field to correct.409 PIX-0092, so do not do it in an environment that is paying.Write the twenty routing keys
tenant_policy namespace, all hold a string, and all hold a route UUID that already exists in Midaz.409 PIX-0105. That is deliberate — refusing a transaction beats posting it against an undefined route. If one specific flow “does not work” and the others do, this is the first place to look.204. A profile your product never exercises can stay empty — that flow then refuses, which is what you want instead of a posting on an undefined route.Set the posting asset and the clearing account
204 can mislead you:_ID:PIX-4011 and names no account.What breaks without it. 409 PIX-0106, “Tenant ledger configuration missing”. The response names both halves and both places to set them, so it tells you which one you are missing. It is a different code from PIX-0105: a tenant can have all twenty route legs correct and still refuse every posting because the asset or the clearing account is unset.Set the daily window
400 rather than silently clamping to the bound.Declare whether this deployment hosts indirect participants
plugin-br-pix-jd.indirects/enabled declares what the tenant is. A plain direct participant sets it to false.{"value":"true"} answers 400. If this deployment settles Pix on behalf of other institutions, set it to true and follow Hosting indirect participants further down this page — there is one more value to provision before you can register anybody, and without it every registration refuses.Materialize the transaction limits with one small payment
GET /v1/limits/available answers 404 PIX-0063, “The specified transaction limit was not found in the system. Please verify the identifier and try again.”, and GET /v1/limits answers {"data":[]}. It looks like a broken account. It is not: it is the normal initial state of an account that has never transacted.PIX-0063 names whichever thing was looked up, so the error list prints its generic form — “The specified entity was not found in the system” — and this route fills in transaction limit. Same code, same 404.Why you cannot fix it by creating something. There is no creation route. PATCH /v1/limits updates a row that has to exist already. The rows are materialized by exactly one thing: the limit pre-flight of an outbound payment. The first time the account sends a payment, the plugin notices it has no rows, creates the defaults, re-reads them, and carries on.So the step is: send one small outbound payment. One cent is enough, and it is what the automated provisioning does.How to check. GET /v1/limits goes from {"data":[]} to a list, and /v1/limits/available goes from 404 to 200.This step depends on the routing keys being written first: it materializes the rows by making a real payment, and with no route the payment is refused with PIX-0105. Everything else on this page can be done in any order.Hosting indirect participants
Skip this section if your deployment settles only its own customers’ Pix. To settle a Pix at the Banco Central, an institution has to be connected to the National Financial System infrastructure. That is expensive and slow, and not every institution does it. An institution that does not uses somebody else’s connection: it becomes an indirect participant, and a direct participant settles for it and hosts it. A tenant that hosts indirect participants is a direct participant first. Everything above — the ISPB binding, the organization, the ledger, the asset, the accounts, the CRM records, the twenty accounting routes — is provisioned exactly as written. Call that the direct-participant chain, steps 1 to 6. This section is what you add on top, and its numbering continues from there.Step 7: the delivery-secret encryption key
Why this step exists. When you register an indirect participant, you supply a delivery secret. The plugin signs every notice it sends to that institution’s endpoint with it, and it is how the institution knows the notice really came from you. That secret is stored encrypted, and the key that encrypts it does not come from the database. It comes from outside. If the key is not configured, every registration that carries a secret fails. This is not an edge case — it is the whole write surface of the indirect flow.POST is the whole recovery.
409 here means “provision it”, not “try again later”. The rail looked for the key and established that it is not there, and only an operator can put it there — so repeating the call without that change fails identically. That is why the code is a 409 and says nothing about retrying.Its sibling is 503 PIX-0123, “Indirect delivery key source unavailable”, which is what you get when the key could not be read: the custody backend refused or did not answer, or a multi-tenant request carried no tenant to resolve the key for. There, the key is not known to be missing — the read itself did not complete — so the response names the faulting dependency and retrying is the right move. Both refuse the registration and store nothing; the pair exists so you can tell an incomplete setup from an outage.409 PIX-0107. There is no default and no degradation to storing the secret in the clear. That is deliberate: a silent default here would store customer secrets encrypted with a key everybody knows.409 PIX-0107 while the other tenants keep working: the resolution is per tenant and fails closed per tenant.
How to check it landed. No route reads the key back, and that is how it should be — it is a secret. There are two signals, and they are not worth the same in the two modes.
The startup log — valid in single-tenant only. With the key resolvable, the “key unavailable” warning does not appear. If it does appear, no registration carrying a secret will pass.
The behavior — the only signal that counts in both modes. Register an indirect participant: the response goes from 409 PIX-0107 to 201. In multi-tenant that proves that tenant and only that tenant, so repeat it per tenant.
Step 8: declare that this tenant hosts indirect participants
plugin-br-pix-jd.indirects/enabled has to be true.
204 when accepted. It is a JSON boolean, unquoted: {"value":"true"} answers 400.
The management API works with the posture off, so you can register participants before you enable — what the posture gates is the money paths. The full breakdown of what each half does is in Before you register anyone.
Step 9: register an indirect participant
One call runs the whole assembly: it checks the ISPB, creates the@pi_{ispb} settlement account in Midaz, and marks the participation active.
201 means the participation is ready to use: registration is atomic, status is always ACTIVE, and there is nothing to poll for.
The refusals, the lifecycle actions, and how to read the registry back are in Indirect participants.
The systemplane keys of the indirects namespace
Five keys, all in plugin-br-pix-jd.indirects, and all of them always exist. The three delivery and resolution keys only take effect once enabled is true, because they tune the money paths. validate_ispb_on_jd is the exception: it gates a step of registration, which works while enabled is still false.
204 when accepted, 400 when the validator refuses. Booleans and integers both go unquoted, and a value outside the range answers 400 rather than clamping silently.
tenancy/jd_integration_binding, and without it every payment refuses with 409 PIX-0092. The encryption key lives in a deployment variable or a secret store, and without it every registration refuses with 409 PIX-0107.What each skipped step looks like
Every refusal below is a409 except the limits one, and none of them resolves by waiting. They resolve by provisioning the missing value.
PIX-0092 stopping does not mean payments pass. It is the first barrier, not the last: with the binding in place, an empty accounting route still refuses with PIX-0105, and a missing asset or clearing account still refuses with PIX-0106. Three codes, three causes, three different fixes.Do not retry a 409. Do retry its 503 sibling
This is the distinction that tells you whether the problem is your setup or somebody’s outage, and the status carries it.
A 409 above is positive knowledge of absence: the rail read your configuration successfully and found nothing there. Only an operator can supply the value, so repeating the request cannot change the answer — a client that honours retry semantics would loop forever against a condition that never resolves on its own. Each of those responses names what to set.
Most of those conditions have a 503 sibling for the case where the rail could not read the configuration at all. Nothing was established about what you have provisioned, the response names the faulting dependency, and retrying is the right move.
PIX-0121 is a special case in a different way: you cannot provoke it through the admin API, because the write validator refuses a malformed ispb before it is stored. It appears only when a value reached the key by another path — see the binding step above.detail each code carries, is the Pix JD error list.
Prove the setup is complete
Run these in order. Each one fails for a different reason, which is what makes the sequence worth running instead of a single smoke test.- The ledger accepts an account. Create a disposable account and delete it. If the book refuses, the refusal names the missing link. Skip this and the same problem comes back later as “account not found” inside a payment flow, three layers from its cause. Never fund the probe account: Midaz refuses to delete an account with a balance.
- The binding reads back as a quoted string carrying your ISPB, as shown above.
- The alias query returns your account, filtered by document, branch, and account number.
- A money route stops answering
409 PIX-0092. This is the same call you were already making — no test harness needed. GET /v1/limitsreturns a list for an account that has sent its first payment.
Where to go next
Environment variables
Indirect participants
Direct Pix via JD
Pix JD error list
PIX-NNNN code, its status, and the detail text the response carries.
