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.
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, 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.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:The posting asset comes from MIDAZ_ASSET_ID and the clearing account from MIDAZ_EXTERNAL_ID — both deployment variables.Both names lie about their shape. Despite the _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 — 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
A deployment that settles Pix on behalf of other institutions adds three provisioning steps on top of the chain above: the delivery-secret encryption key, the hosting posture, and the registration of each indirect participant. They are Hosting indirect participants, and their numbering continues from this page — steps 7 to 9. If your deployment settles only its own customers’ Pix, your provisioning ends here.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.
