Skip to main content
To settle a Pix at the Banco Central, an institution needs a connection to the National Financial System infrastructure. That connection is expensive and slow to get, and not every institution has one. An institution without it uses the connection of another institution. It becomes an indirect participant, and a direct participant settles for it and hosts it. The direct and indirect model explains that arrangement: who holds the settlement account, and where each institution’s money lives. A tenant that hosts indirect participants is a direct participant first. Provision everything in Setting up the rail exactly as that page says. That includes the binding, the organization, the ledger, the asset, the accounts, the CRM records, and the accounting routes. Call that the direct-participant chain, steps 1 to 6. This page adds the next steps, and its numbering continues from there. If your deployment settles only the Pix of its own customers, skip this page.
Do step 7 before step 9. Without the encryption key, every registration in step 9 fails. Steps 7 and 8 do not depend on each other. The posture does not validate the key, and the posture does not read the key. Do step 10 after step 9, because step 10 funds the account that the registration creates.
This page covers provisioning. Indirect participants explains what a hosted participation is, how an inbound credit reaches one, how its lifecycle behaves, and every refusal it can answer.

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. 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.
Read the guarantee in the middle of that sentence: no secret was stored. Encryption fails before any write, so there is no half-created row with a plaintext secret to go and clean up. Fix the key and repeat the POST. That is the whole recovery.
This 409 means “provision it”, not “try again later”. The rail looked for the key and found that it does not exist. Only an operator can add it. A repeat of the call without that change fails the same way. That is why the code is a 409 and says nothing about a retry.Its sibling is 503 PIX-0123, “Indirect delivery key source unavailable”. You get it when the rail could not read the key, because the custody backend refused or did not answer. The rail then does not know that the key does not exist, because the read did not complete. The response names the dependency that failed, and a retry is the right action. Both codes refuse the registration and store nothing. The pair lets you tell an incomplete setup from an outage.
The plugin starts without the key, and it does not refuse to boot. The process logs a warning at startup and comes up healthy. At startup, nothing tells a deployment that will host indirect participants from one that never registers one. So the symptom arrives on the first registration, not on deploy. If you did not read the startup log, the refusal is your first notice.
Where it lives, and why it is not in the systemplane. This contrast explains where each kind of value belongs on this rail.
Do not put the encryption key in the systemplane. The systemplane is the live configuration plane, readable over the admin API, and that is the right home for everything that is not a secret. Credential material does not go there, and the rail separates the two on purpose.Do not commit it either: not in a versioned .env, not in a values.yaml, not in a compose file.
The format: exactly 64 hex characters. It is an AES-256 key (32 bytes), hex-encoded. That is 64 hex characters, not 63 and not 65.
Generate one per deployment. Do not copy another environment’s key and do not reuse another service’s.
An absent, blank, or malformed value all produce the same 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.
In single-tenant, set the deployment variable:
That placeholder is deliberately not a valid key. Pasted as-is, it fails closed with the refusal above. It does not encrypt your customers’ secrets with a value published on a documentation page. The plugin reads the key at startup, so a change needs a process restart. The ISPB binding is different: the plugin reads it on every call, and a change takes effect without a restart.
Do not type the value on a command line. It ends up in your shell history and in CI logs. Read it from a vault, or type it with read -rs, which does not echo.
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. The startup log. With the key resolvable, the “key unavailable” warning does not appear. If it does appear, no registration carrying a secret will pass. The behavior. Register an indirect participant. The response changes from 409 PIX-0107 to 201.
Think before you use the behavioral check. A registration is permanent. There is no delete route, and the only exit is close, which keeps the row and holds the ISPB. In a production environment, do not use a throwaway registration to test the key. Use an ISPB that you plan to operate. The startup log saves you that cost.

Step 8: declare that this tenant hosts indirect participants

Set plugin-br-pix-jd.indirects/enabled to true.
The plugin answers 204 when it accepts the value. The value is a JSON boolean, without quotes: {"value":"true"} answers 400. The management API works with the posture off, so you can register participants before you enable it. The posture gates the money paths. Before you register anyone shows what each half does.
The read fails closed. When a money path “goes back to direct behavior” and nobody changed the key, this is the cause. Look at the systemplane.

Step 9: register an indirect participant

One call does the whole assembly. It checks the ISPB, creates the @pi_{ispb} settlement account in Midaz, and marks the participation active.
A 201 means that the participation is ready to use. The status is always ACTIVE. You do not need to poll.
Do not write an indirect participant’s ISPB into tenancy/jd_integration_binding. That key is the identity that the plugin presents to JD. A third party’s ISPB there makes the plugin introduce itself as another institution. Nothing warns you: the plugin accepts 8 valid digits, and the write answers 204. The key is one per tenant, not one per indirect participant. Every indirect participant that you host reaches the SPI through your ISPB.
Registration is permanent. There is no DELETE on /v1/indirects, and CLOSED is terminal. A mistaken registration in a live tenant stays there. Its ISPB stays held against the uniqueness rule until somebody closes it. Check the name, the ISPB, and the endpoint before you call. Rehearse on a disposable environment.
Indirect participants has the refusals, the lifecycle actions, and how to read the registry back.

Step 10: fund the position

The registration creates the @pi_{ispb} account empty. Inbound Pix credits it, but an outbound payment needs balance first. Without funds, every payment that leaves through that indirect participant fails for insufficient balance. You make the initial load. It mirrors the arrangement: at JD you administer that institution’s limit, and in your ledger you load its position. The mechanics are the same as the funding step of Setting up the rail. Post a Midaz transaction from @external/BRL, with the same variables as that page. Only the destination changes, to the piAccountAlias from the registration.
If you lose the response to a top-up, send the exact same body again within 300 seconds. Midaz then posts it once. After that window, check the position balance before you send the top-up again.Change the description only for a new top-up. A new description on a real retry adds a second credit by mistake.
The position also controls the participation’s exit. The plugin refuses close with 409 PIX-0096 while the @pi account holds funds, available or on hold, and names the cause. Bring the position to zero before you close. A funded position keeps the participation open until you do. How to check. Read the account’s balance back in Midaz. The surest check is behavior: an outbound payment through the indirect participant stops its refusals for insufficient balance.

Return a Pix that an indirect participant received

A Pix for a customer of an indirect participant lands on that participant’s @pi_{ispb} position. To return it, send POST /v1/refunds with the indirectId of that participant. The devolução then debits that position. The plugin refuses the devolução with 422 PIX-0135 in three cases, and moves no money:
  • the original Pix landed on an indirect participant’s position, and the refund has no indirectId.
  • the refund names another indirect participant.
  • the original Pix landed on a direct account, and the refund has an indirectId.
The indirectId passes the same checks as on a payment order. The hosting posture must be on, and the participation must be ACTIVE.

The systemplane keys of the indirects namespace

There are five keys, all in plugin-br-pix-jd.indirects, and all of them always exist. The three delivery and resolution keys take effect only when 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.
The plugin answers 204 when it accepts a value and 400 when the validator refuses it. Booleans and integers go without quotes. A value outside the range answers 400. The plugin does not clamp it.
The two steps that can stop the indirect flow on their own are not in this namespace. Identity is the systemplane key tenancy/jd_integration_binding. Without it, every payment fails with 409 PIX-0092. The encryption key comes from a deployment variable or a secret store. Without it, every registration fails with 409 PIX-0107.