Skip to main content
A fresh Direct Pix via JD deployment starts, responds to its health probe, and refuses every payment. Nothing is broken: the rail needs a chain of objects to exist before it can move money, and until they do it fails closed rather than guessing. This page is that chain, in order, with the failure each missing link produces. Lerian runs this provisioning with you during onboarding. Use the page to know what has to exist, what each value means, and how to prove it landed.
This provisioning chain applies to a single-tenant deployment. On the managed multi-tenant offering (SaaS), the platform provisions and resolves all of it — credentials, bindings, CRM, and the services the rail talks to — automatically per tenant; none of these steps is yours to run.
The order is a dependency chain, not a preference. Midaz refuses each link while the previous one is missing, and one systemplane key has to be written before a payment can materialize the transaction limits. Where a step can run in any order, the page says so.

Before you start

You talk to three services, and confusing them is the most common first mistake. You also need:
  • A bearer token for the ledger, one for the CRM, and one for the plugin. They can come from different audiences.
  • The systemplane:write permission on the plugin token. Without it the configuration writes answer 403.
  • SYSTEMPLANE_ENABLED=true on the plugin. With it unset, the /system route group is not mounted at all and every configuration write answers 404.
  • Your institution’s ISPB: the 8-digit identifier you were accredited with at BACEN.
The examples below use these shell variables. Where the rail already has a name for a value, the variable carries that same name, so what you read here is what you set at deploy time. Every UUID, document, and ISPB is a placeholder — use the values your own environment returns.
Midaz exposes an onboarding surface and a transaction surface, and this rail configures them separately. A deployment that serves both from one address gives the two variables the same value. The calls below are grouped by the object each one creates.
The CRM requires the X-Organization-Id header on every collection route. Without it a query is not scoped to your organization, and what comes back is another record or nothing at all.

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.
1

Create the organization and the ledger

The organization is your institution in the books. The ledger is the book it posts to. An institution can hold more than one ledger; this rail posts to exactly one.
A 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.
2

Create the BRL asset, and wait for it to appear

The asset is the currency the money is recorded in. For Pix it is BRL.
Both 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:
  1. The ledger starts accepting accounts in that asset. Before this, it refuses every account with 0034 Asset Code Not Found.
  2. Midaz creates the @external/BRL account 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.
The 201 arrives before the asset shows up in the listing. A script that creates the asset and creates an account on the next line is exactly what breaks intermittently. Poll the listing until the code appears, with a deadline:
3

Create one account per role

Create one account for each role your environment exercises — payer, payee, and whatever else your product has.
The account id is consumed twice later: in the CRM link below, and as the 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.
4

Create the holder in the CRM

The holder is the owner of the account. The plugin resolves who a counterparty is from the CRM, not from the ledger.
Four fields, three traps:
  • type follows the document length. NATURAL_PERSON for a CPF (11 digits), LEGAL_PERSON for 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.
  • externalId has 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 no externalId produces an account that resolves, looks complete, and fails every payment.
  • addresses.primary.city is 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, answering 422 PIX-0033 and pointing at payee.city. No payment path reads the field, which is why its absence goes unnoticed until someone generates a QR.
One holder supports exactly one account. externalId is a single value per holder, so giving two accounts to the same holder makes the second one move money in the first. For a second account, create a second holder.
5

Link the holder to the account

The holder and the ledger account both exist, and nothing joins them yet. This step is the join, and it is how the plugin finds where to credit an incoming Pix.
Never send bankId as an empty string. An empty string is a value: it records “this account belongs to the institution whose ISPB is empty”, which is worse than saying nothing. If you do not have the ISPB yet, omit the field.Accounts with bankId filled in pay; accounts created without it answered a server error on cash-out. That symptom is measured, but the mechanism is not confirmed. Fill it in — it is your own ISPB, it costs nothing, and the alternative is debugging an error that names nothing.
Check what was recorded, and always filter. An unfiltered query returns the first link in the organization, which is how a verification ends up approving somebody else’s account:
6

Fund the accounts

A new account holds zero, and you cannot pay from an empty account. The opening credit comes from @external/BRL, created with the asset in step 2.
Two identical bodies are one posting. Midaz collapses the repeat: the second POST answers 201 carrying the first posting’s id, and nothing moves. A top-up that “worked” and did not change the balance is this. Change the description on every credit.
@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.
7

Create the twenty accounting routes

Every money path on this rail has its own pair of Midaz operation routes — one credit leg and one debit leg. Ten profiles, two legs each, so twenty routes. The configuration step further down stores their UUIDs.
Each call returns {"id": "..."}, and that id is what the routing keys below hold.
The direction is invertible and nothing warns you. A debit leaves the payer, so it is the source leg; a credit arrives, so it is destination. Swapped, every posting still answers success — in the wrong direction. No status code reports this.
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 a PUT 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.
Ask the plugin what it expects rather than trusting a copy. 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.
1

Write the JD integration binding — your ISPB

What it is. The ISPB is the 8-digit number that identifies your institution at the Banco Central. It is the “who am I” that goes on every Pix message, and the plugin cannot sign anything as yours without it.Why you would not guess this. The ISPB is not a deployment variable. It comes from the systemplane key 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.
Restarting does not help. This is not a value read at boot — the plugin reads the key on every call, so restarting a deployment that has no ISPB returns a deployment that still has no ISPB. Writing the key does help, with the app running: the next request passes, with no redeploy.
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:
Let jq do the escaping:
All three fields are required on the write, and that is the surprising part. The deployment does not read 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.
Decoding is strict: an unknown field is refused, not ignored, and it takes the whole write down. That includes the retired routeProfiles field — the accounting routes moved to the routing.* keys below — so a document copied from an old configuration does not go in. Anything after the first JSON document is refused too.
How to check it landed. Read the key back. It holds no secret, so the value comes back in the clear:
If 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.
That is why you will not meet 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.
A 400 here is not a permission problem. That reading has already cost time: three test scenarios read this exact 400 as “my bearer has no admin grant”. Missing permission is 403. This 400 means the value you sent is wrong. The response does not say which field, so check the 8 digits first — it is the most common mistake.
The empty string is accepted, and it is the “not provisioned” sentinel. Writing it takes the deployment back to refusing with 409 PIX-0092, so do not do it in an environment that is paying.
2

Write the twenty routing keys

What it is. For each money path, the UUID pair of the Midaz operation routes created above. All twenty keys live in the tenant_policy namespace, all hold a string, and all hold a route UUID that already exists in Midaz.
What breaks without it. A missing leg does not degrade the flow, it refuses it: the matching money path fails closed with 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.
How to check. Each write answers 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.
An empty string is the “not yet provisioned” sentinel and is accepted. The all-zeros UUID is refused: it parses cleanly but is never a real Midaz identifier. A script that fills unused routes with zeros as a “safe empty” passes any naive format check and is rejected by this one. Use the empty string.
3

Set the posting asset and the clearing account

What it is. The asset the money is posted in, and the external account that stands in for the world outside your institution.Where the values go depends on the deployment mode, and this is where a 204 can mislead you:The posting asset comes from MIDAZ_ASSET_ID and the clearing account from MIDAZ_EXTERNAL_ID — both deployment variables.
The two systemplane keys tenant_policy/midaz.asset_id and tenant_policy/midaz.external_id exist, accept a write, and answer 204 — and nothing reads them: the plugin resolves the asset and the clearing account from the deployment variables. You can write both, get 204 on both, and still have the money path refusing, because the variables are still empty. A 204 here is not confirmation that the value will be used.
Both names lie about their shape. Despite the _ID:A UUID in either one answers 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.
4

Set the daily window

The two ends of the window transaction limits are accounted in. Both are integers from 0 to 23 — clock hours, not timestamps — and both are systemplane keys in both deployment modes.
The values above are an example; use your own. What does not change is the JSON type: an integer, unquoted. A value outside the range answers 400 rather than silently clamping to the bound.
5

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.
It is a JSON boolean, unquoted: {"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.
6

Materialize the transaction limits with one small payment

This is the least guessable step on the page, because the product offers no way to create what it needs.What you expect. You provision an account and read its available limit.What happens. 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.
An account with no limit rows is not an account with no limit. If the automatic creation cannot establish the limits, the payment is refused, not waved through.And do not try to force the rows by sending an amount above the ceiling. The balance check runs before the limit enforcer, so a large amount is refused for insufficient funds and the enforcer is never reached — no rows appear. It has to be an ordinary payment that fits the balance.
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 a 409 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.
The accounting routes are the exception, and it is worth knowing. PIX-0105 has no 503 sibling: a route leg that is missing, empty, malformed, the all-zeros UUID, or unreadable all answer that same 409. So unlike every other code here, a PIX-0105 on its own does not separate “this leg was never provisioned” from “the configuration plane did not answer”. Read the twenty legs back before concluding it is an outage.
Both halves of each pair refuse the operation, and neither posts anything nor stores anything. The difference is entirely what you should do next, which is why they are separate codes instead of one envelope covering both.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.
The full catalog, with the exact 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.
  1. 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.
  2. The binding reads back as a quoted string carrying your ISPB, as shown above.
  3. The alias query returns your account, filtered by document, branch, and account number.
  4. A money route stops answering 409 PIX-0092. This is the same call you were already making — no test harness needed.
  5. GET /v1/limits returns a list for an account that has sent its first payment.
Some provisioning steps are not yours. Lerian’s end-to-end battery establishes four more things before it runs: a pool of MED infractions, credentials for its JD test double, and two internal bookkeeping files. Those are test fixtures with no equivalent in a real deployment — in production the infractions arrive from JD, and JD authenticates itself. Do not try to build them.

Where to go next

Environment variables

The deploy-time configuration of this rail: JD connectivity, the ledger and CRM endpoints, QR hosting, and the notification providers.

Indirect participants

What a hosted participation is, how an inbound credit reaches one, its lifecycle, and every refusal it can answer.

Direct Pix via JD

How settled Pix movements land in Midaz, and how the two systems correlate.

Pix JD error list

Every PIX-NNNN code, its status, and the detail text the response carries.