> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Hosting indirect participants

> The three provisioning steps a deployment adds when it settles Pix on behalf of other institutions: the delivery-secret encryption key, the hosting posture, registering each indirect participant, and the systemplane keys of the indirects namespace.

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 in [Setting up the rail](/en/interfaces/pix-jd/pix-jd-setup) — the ISPB binding, the organization, the ledger, the asset, the accounts, the CRM records, the twenty accounting routes — is provisioned exactly as written there. Call that the direct-participant chain, steps 1 to 6. This page is what you add on top, and its numbering continues from there. If your deployment settles only its own customers' Pix, skip this page.

| # | Step                                                           | Where                                                |
| - | -------------------------------------------------------------- | ---------------------------------------------------- |
| 1 | the ISPB of this deployment (`tenancy/jd_integration_binding`) | [the setup page](/en/interfaces/pix-jd/pix-jd-setup) |
| 2 | organization, ledger, asset, accounts, CRM holders             | [the setup page](/en/interfaces/pix-jd/pix-jd-setup) |
| 3 | the twenty accounting route legs                               | [the setup page](/en/interfaces/pix-jd/pix-jd-setup) |
| 4 | the posting asset and the clearing account                     | [the setup page](/en/interfaces/pix-jd/pix-jd-setup) |
| 5 | the daily window                                               | [the setup page](/en/interfaces/pix-jd/pix-jd-setup) |
| 6 | the transaction limit rows, which have no creation route       | [the setup page](/en/interfaces/pix-jd/pix-jd-setup) |
| 7 | **the delivery-secret encryption key**                         | below                                                |
| 8 | **the hosting posture, `indirects/enabled = true`**            | below                                                |
| 9 | **registering each indirect participant**                      | below                                                |

<Warning>
  Step 7 comes before step 9, not after: without the encryption key, every registration in step 9 fails. Steps 7 and 8 are independent of each other — the posture does not validate the key, and the key is not read by the posture.
</Warning>

<Note>
  This page is the provisioning. What a hosted participation **is**, how an inbound credit reaches one, how its lifecycle behaves and every refusal it can answer are in [Indirect participants](/en/reference/interfaces/pix-jd/indirect-participants).
</Note>

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

```text theme={null}
409  PIX-0107  "Indirect Delivery Encryption Not Provisioned"
     "No delivery-secret encryption key is provisioned for this tenant, so the
      indirect participant was not saved and no secret was stored. ..."
```

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. Fixing the key and repeating the `POST` is the whole recovery.

<Note>
  **`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. 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.
</Note>

<Warning>
  The plugin starts without the key. There is no boot refusal: the process logs a warning at startup and comes up healthy, because nothing at startup distinguishes a deployment that will host indirect participants from one that never registers a single one — the key is read on every request, not once at boot. 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.
</Warning>

**Where it lives, and why it is not in the systemplane.** This contrast explains where each kind of value belongs on this rail.

|                     | The ISPB (`tenancy/jd_integration_binding`)                           | The encryption key                                          |
| ------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------- |
| What it is          | **identity** — the number that identifies you at BACEN                | **a credential** — cryptographic material                   |
| Is it a secret?     | no. An ISPB, an organization id, and a ledger id are not credentials  | yes                                                         |
| Where it lives      | the **systemplane**, so it can be read and written over the admin API | **outside** the systemplane                                 |
| Where it comes from | the systemplane admin API                                             | the `INDIRECTS_DELIVERY_ENCRYPTION_KEY` deployment variable |

<Warning>
  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.
</Warning>

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

```bash theme={null}
openssl rand -hex 32
```

Generate one per deployment. Do not copy another environment's key and do not reuse another service's.

<Note>
  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.
</Note>

In single-tenant, set the deployment variable:

```bash theme={null}
# in the process environment — never in a versioned file
INDIRECTS_DELIVERY_ENCRYPTION_KEY="paste-the-64-hex-characters-here"
```

That placeholder is deliberately not a valid key: pasted as-is it fails closed with the refusal above rather than encrypting your customers' secrets with a value published on a documentation page.

It is read at startup, so changing it needs a process restart. That is different from the ISPB binding, which is read on every call and heals without one.

<Warning>
  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.
</Warning>

**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 goes from `409 PIX-0107` to `201`.

<Warning>
  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. Do not spend a throwaway registration to test the key in a production environment; use an ISPB you actually intend to operate. The startup log saves you that cost.
</Warning>

### Step 8: declare that this tenant hosts indirect participants

`plugin-br-pix-jd.indirects/enabled` has to be `true`.

```bash theme={null}
curl -s -o /dev/null -w '%{http_code}\n' -X PUT \
  -H "Authorization: Bearer $PIX_JD_BEARER_TOKEN" -H 'Content-Type: application/json' \
  -d '{"value":true}' \
  "$PIX_JD_BASE_URL/system/plugin-br-pix-jd.indirects/enabled"
```

`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](/en/reference/interfaces/pix-jd/indirect-participants#before-you-register-anyone).

<Note>
  The read fails closed. If the systemplane does not answer, if the key does not resolve, or if the value comes back the wrong type, the plugin reads it as **off** — never on by accident. A money path that "went back to behaving like a direct one" with nobody having touched the key is this mechanism. Look at the systemplane.
</Note>

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

```bash theme={null}
# The signing secret must never appear in a process argument list: `ps` and
# command logging expose arguments. Read it silently, export it, let jq pull it
# from the environment, and have curl read the body from standard input.
IFS= read -r -s -p 'delivery secret for this participant: ' INDIRECT_DELIVERY_SECRET; printf '\n'
export INDIRECT_DELIVERY_SECRET

jq -n '{
  name: "Indirect PSP Ltda",
  ispb: "87654321",
  messagingMode: "raw",
  delivery: { endpointUrl: "https://indirect.example.com/pix", secret: env.INDIRECT_DELIVERY_SECRET }
}' | curl -s -X POST "$PIX_JD_BASE_URL/v1/indirects" \
  -H "Authorization: Bearer $PIX_JD_BEARER_TOKEN" -H 'Content-Type: application/json' \
  --data-binary @-
unset INDIRECT_DELIVERY_SECRET
```

| Field                  | Rule                                                              | If it is wrong           |
| ---------------------- | ----------------------------------------------------------------- | ------------------------ |
| `name`                 | 1 to 120 characters                                               | `422 PIX-0098`           |
| `ispb`                 | exactly 8 digits — the **indirect institution's** ISPB, not yours | `422 PIX-0098`           |
| `delivery.endpointUrl` | a valid `https` URL, where the notices are delivered              | `422 PIX-0098`           |
| `delivery.secret`      | the symmetric signing secret, agreed with that institution        | `422 PIX-0098` if absent |
| `messagingMode`        | `raw` is the only value today                                     | `422 PIX-0098`           |

A `201` means the participation is ready to use: registration is atomic, `status` is always `ACTIVE`, and there is nothing to poll for.

<Warning>
  Do not write an indirect participant's ISPB into `tenancy/jd_integration_binding`. That key is the identity the plugin presents to JD, so a third party's ISPB there makes the plugin introduce itself as another institution — and nothing warns you, because 8 valid digits are accepted and the write answers `204`. The key is one per direct participant, not one per indirect participant: every indirect participant you host reaches the SPI through your ISPB.
</Warning>

<Warning>
  **Registration is permanent.** There is no `DELETE` on `/v1/indirects`, and `CLOSED` is terminal. An indirect participant registered by mistake in a live tenant does not come back out, and 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.
</Warning>

The refusals, the lifecycle actions, and how to read the registry back are in [Indirect participants](/en/reference/interfaces/pix-jd/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`.

| Key                        | Type    | Range   | Default | What it is                                                                                                                                        |
| -------------------------- | ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                  | boolean | —       | `false` | the hosting posture                                                                                                                               |
| `delivery_concurrency`     | integer | 1–256   | `8`     | how many delivery `POST`s run in parallel. Isolation is per participant: one stuck endpoint occupies at most one slot and never blocks the others |
| `delivery_max_attempts`    | integer | 1–64    | `8`     | the retry budget before a delivery row ends up `INVALID`. Recovery from there is manual                                                           |
| `resolution_cache_ttl_sec` | integer | 0–86400 | `30`    | how long a resolution stays cached. `0` disables the cache, which is why the minimum is 0                                                         |
| `validate_ispb_on_jd`      | boolean | —       | `false` | with `true`, registration queries JD's participant directory and **fails retryably** if JD is down                                                |

```bash theme={null}
PUT() { curl -s -o /dev/null -w "$1 -> %{http_code}\n" -X PUT \
          -H "Authorization: Bearer $PIX_JD_BEARER_TOKEN" -H 'Content-Type: application/json' \
          -d "$2" "$PIX_JD_BASE_URL/system/$1"; }

PUT plugin-br-pix-jd.indirects/enabled                  '{"value":true}'
PUT plugin-br-pix-jd.indirects/delivery_concurrency     '{"value":8}'
PUT plugin-br-pix-jd.indirects/delivery_max_attempts    '{"value":8}'
PUT plugin-br-pix-jd.indirects/resolution_cache_ttl_sec '{"value":30}'
PUT plugin-br-pix-jd.indirects/validate_ispb_on_jd      '{"value":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.

<Note>
  Neither of the two steps that break the indirect flow on their own lives in this namespace. Identity is the systemplane key `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`.
</Note>
