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

# Indirect participants

> How a smaller institution reaches Pix through your direct participation: the settlement position registering one creates, how its money arrives, how it learns about a movement, and what registering one costs permanently.

Your institution is a **direct participant**: it is present at Pix's national settlement system (SPI) in its own right, and this plugin is how it gets there. An **indirect participant** is a smaller institution that has no connection of its own and reaches SPI **through yours**. It is a client of your institution rather than a client of the rail.

Nothing about that relationship exists at BACEN. It exists in your tenant's registry, and you create it with `POST /v1/indirects`.

<Note>
  This page is what a hosted participation **is** and how it behaves. The provisioning — the delivery-secret encryption key, the hosting posture, and the registration call itself — is [Hosting indirect participants](/en/interfaces/pix-jd/hosting-indirect-participants), which continues the setup page's numbering. Of those, only the encryption key refuses every registration on its own — provision it first. The hosting posture gates the money paths, not the registry, so you can register participants before you turn it on.
</Note>

## Who is responsible for what

***

Almost every operational misunderstanding here is somebody looking for the answer in the wrong column.

|                             | Responsibility                                                                                                                        |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Banco Central / SPI         | moves money **between institutions**. It knows no end customers                                                                       |
| JD                          | the technical bridge: it delivers the notices and signs the QR codes. It makes no business decisions                                  |
| You, the direct participant | each indirect participant's **Pix account** — that it exists, is active, and holds a balance. And the settlement between institutions |
| The indirect participant    | **its own customers' accounts**                                                                                                       |

In practice: when a Pix arrives for a customer of an indirect participant, **the money stops with you**, in an account that represents that institution's position. You do not know — and cannot know — which of its customers owns it. The indirect participant credits its customer in its own core, after it receives your notice.

## What registering one creates

***

Registering an indirect participant does more than write a row. The plugin also creates that institution's **settlement position** in Midaz: a ledger account whose alias is `@pi_` followed by the institution's ISPB, the eight-digit code BACEN assigns to every participant. An indirect on ISPB `12345678` settles at `@pi_12345678`.

That position is where that institution's money sits. The response returns it as `piAccountAlias`, and the alias is derived from `ispb` — you never choose it, and a client never creates the account.

One position per institution is the whole point. A credit that arrives for one indirect lands only on that indirect's account, so one institution's money is never mixed with another's and never with your own. That segregation is the property the registry exists to provide.

<Note>
  The alias uses an **underscore**, not a slash: `@pi_12345678`. Midaz rejects a slash in a user alias, so a path-shaped name resolves to nothing.
</Note>

## Before you register anyone

***

Indirect participation is off by default. The per-tenant systemplane flag **`plugin-br-pix-jd.indirects/enabled`** must be on, and its default value is `false`.

What the flag actually gates is worth knowing, because the two halves behave differently:

* **The management API works with the flag off.** You can register participants, list them and read one before enablement. Onboarding ahead of go-live is deliberate.
* **No money moves with the flag off.** Resolution — the step that decides which indirect an incoming credit belongs to — reports the feature as disabled, so an inbound credit is never routed to a `@pi` account and no indirect can originate an outbound order.

Two operations refuse outright while the flag is off, with `422 PIX-0111`: the own-QR-certificate write on `PATCH /v1/indirects/{indirectId}`, and `GET /v1/indirects/{indirectId}/jwks`.

One thing keeps working with the flag off that you might expect to be gated: **the exception queue**, so that a credit already parked for triage is never trapped behind a switch.

<Note>
  The read fails closed. If the systemplane does not answer, if the value does not resolve, or if it comes back the wrong type, the plugin reads the flag 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>

## Registering one

***

<Steps>
  <Step title="Turn the feature on for the tenant">
    Set `plugin-br-pix-jd.indirects/enabled` to `true`. Registration works without it, but nothing settles until it is on.
  </Step>

  <Step title="POST /v1/indirects">
    Send the institution's `name`, its `ispb`, and a `delivery` block holding `endpointUrl` (an HTTPS URL) and `secret`. The answer is `201` and carries `indirectId` — the identifier every later call routes by — and `piAccountAlias`.
  </Step>

  <Step title="Nothing to poll — a 201 means ready">
    Every step runs **before the registry row is written**: the ISPB uniqueness check, the optional JD directory check, then the creation of the `@pi_{ispb}` account in Midaz. Only then is the row written, and it is written `ACTIVE`. There is no intermediate state and nothing to wait for.
  </Step>

  <Step title="On a failure, fix the cause and POST again">
    A failed step answers a **coded error** naming the step, and **no registry row is written**, so the ISPB is still free. Fix the cause and POST again — re-posting is the recovery, and there is no separate retry route because nothing was left half-written to resume.
  </Step>
</Steps>

**Registration is atomic: a `201` means ready, and a failure leaves no registry row.** `status` on a `201` is always `ACTIVE`. If you are writing a client that polls until the participant becomes routable, delete that loop — it is waiting for a transition that cannot happen.

The one thing a failure can leave behind is the **settlement account**, when that step succeeded and a later one did not. It is not rolled back, and it is harmless: nothing routes to a `@pi` account without an `ACTIVE` registry row pointing at it, and creating the account is idempotent by alias, so your next attempt adopts the same account instead of making a second one. What a failure never leaves is a registry row or a held ISPB.

The response still carries a `provisioning` object with a `failedStep` field. It is **legacy and always `null`** for anything this service registers, because a failed registration leaves no row to carry a marker.

The lifecycle states are `ACTIVE`, `SUSPENDED` and `CLOSED`.

<Warning>
  `PENDING_PROVISIONING` is a **retired** state with a sharp edge. Nothing is registered into it any more, and it is still accepted as a filter on `GET /v1/indirects` so that rows written before registration became atomic still read back as themselves. But that state has **no lifecycle transitions in either direction** — a legacy row cannot be suspended and cannot be closed; every action on it is refused `409 PIX-0094`.

  Such a row is readable, **not routable**, and it still holds its ISPB against the open-uniqueness index. So it blocks any fresh registration of that institution, and no API call can free it. Clearing one is an operator and data decision, deliberately not an API operation. If you inherited a tenant from before this change, run `GET /v1/indirects?status=PENDING_PROVISIONING` once to find out whether you have any.
</Warning>

## How money reaches an indirect participant

***

Follow one inbound Pix. Nothing in this path is an endpoint the indirect calls; it is your plugin doing the work.

1. **"Can this account receive?"** Before it moves the money, the Banco Central asks whether the destination account accepts the credit. The question arrives at **you**, because the indirect participant has no line to the SPI.
2. **A Pix arrives** for an account held at the indirect institution and reaches your direct participation, delivered to the plugin's cash-in webhook.
3. **Resolution finds the indirect** by the ISPB in the receiver block, looked up in the participation registry. Four outcomes, and only the first one credits an indirect:

   * it matches an `ACTIVE` participation, so the credit posts to that participation's `@pi_{ispb}` alias;
   * it is **your own** ISPB, so it is your book, and the CRM resolves the account as usual;
   * it matches nothing, or matches a suspended or closed participation, so the credit is **parked** for an operator to triage;
   * the resolution fails on infrastructure, so the plugin **neither credits nor parks**. It fails closed on purpose: nothing money-adjacent is guessed.

   A resolution miss is never cached, so an indirect that just went active is not shadowed by an earlier negative answer.
4. **The credit is booked on `@pi_{ispb}`.** The plugin records the movement against that settlement position and links its own transaction record to the indirect. **The notice is recorded in the same database transaction as the credit** — either both exist, or neither does.
5. **A signed notice is pushed** to the `delivery.endpointUrl` you registered, carrying JD's payload byte for byte. This is the plugin calling the institution, not the reverse.
6. **The indirect participant credits its own customer**, in its own core.

<Warning>
  The CRM is not consulted on the indirect path. The destination comes from the participation registry, never from data the caller sent. That is deliberate: if an indirect participant were mis-registered with your own ISPB, credits meant for your own book would land in its account. The "is this ISPB ours?" check therefore runs before the "does it match an indirect participant?" check.
</Warning>

The notice carries two headers the receiving institution verifies:

| Header               | Value                                                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `X-Lerian-Signature` | `sha256=` followed by the hex HMAC-SHA256 of the exact raw body bytes, keyed with that indirect's registered `secret`. |
| `X-Lerian-Timestamp` | The send time in Unix-epoch seconds, so the receiver can bound replay with its own freshness window.                   |

Delivery is **at-least-once**. The same notice can be re-sent after a retryable failure or a crash between the send and the bookkeeping, and the `endToEndId` is stable across attempts — so a receiver must deduplicate by `endToEndId` and treat a repeated payload as already handled.

<Note>
  The stored `secret` is write-only. Every read of an indirect returns `delivery.secret` as `***`, so a lost secret is replaced with a `PATCH`, never recovered.
</Note>

## When the notice never arrives

***

The push can fail permanently — retries exhausted, or the endpoint answers `4xx`, after which the notice is marked invalid and never tried again. **The money is booked correctly either way.** The consequence is narrower and worse than it sounds: the movement is real, and the institution does not know about it.

`GET /v1/indirects/{indirectId}/transactions` is what closes that gap. It returns the movements that actually settled on one indirect's `@pi_{ispb}` account, which makes it the authoritative record rather than the notice. Sweeping it on a schedule is how an institution stops depending on delivery having worked, and it is also the only way to re-read a window after an outage of its own.

The window, paging, and unit rules that decide whether a reconciliation is correct — the required half-open `since` window, integer centavos, oldest-first cursor paging, the silently clamped `limit` — are the contract of [List an indirect participant's transactions](/en/reference/interfaces/pix-jd/list-indirect-participant-transactions). Reading the feed changes nothing and re-sends no notice.

## Hosting the QR code under the institution's own certificate

***

By default a dynamic QR code issued for an indirect is signed and published under your direct participation. An indirect can instead publish its own: set `qrCertificate.ownCertificate` with a `publicBaseUrl` on `PATCH /v1/indirects/{indirectId}`, and the signed document is served from the institution's own host.

The institution cannot produce the validation keys itself, because JDPI holds the certificate and does the signing. `GET /v1/indirects/{indirectId}/jwks` returns the JWK Set for that indirect so the institution can publish it on its own host for payer PSPs to validate against. Fetch it when you configure the certificate and again whenever it rotates.

Lifecycle status deliberately does not gate that read: a suspended or closed indirect still has live QR codes in circulation, and withholding the key would break their validation.

## Reading the registry

***

```bash theme={null}
# one participation
curl -s "$PIX_JD_BASE_URL/v1/indirects/$INDIRECT_PARTICIPANT_ID" -H "Authorization: Bearer $PIX_JD_BEARER_TOKEN" | jq

# the list — filters are exact and combine with AND
curl -s -G "$PIX_JD_BASE_URL/v1/indirects" -H "Authorization: Bearer $PIX_JD_BEARER_TOKEN" \
  --data-urlencode 'status=ACTIVE' --data-urlencode 'limit=25' | jq
```

Filters are `status` and `ispb` (8 digits, exact); the cursor-paging rules and the silently clamped `limit` are the contract of [List indirect participants](/en/reference/interfaces/pix-jd/list-indirect-participants).

A `GET` by id, and the list, are always the current truth. The money paths read a cached projection, so a suspension can take up to the resolution cache TTL — 30 seconds by default — to be seen by every replica. The ledger lock from `suspend` already applied at the moment of the call.

## Suspending, reactivating and closing

***

`PATCH /v1/indirects/{indirectId}` carries at most one lifecycle `action` — `suspend`, `reactivate` or `close` — and each one writes the ledger as well as the registry.

```bash theme={null}
curl -s -X PATCH "$PIX_JD_BASE_URL/v1/indirects/$INDIRECT_PARTICIPANT_ID" \
  -H "Authorization: Bearer $PIX_JD_BEARER_TOKEN" -H 'Content-Type: application/json' \
  -d '{"action":"suspend"}'
```

* **`suspend`** disables both sending and receiving on the indirect's `@pi` account. The ledger itself refuses that participant's money, which is why a suspension holds even on a replica whose resolution cache is still stale. **`reactivate`** re-enables both.
* **`close`** is irreversible. `CLOSED` is terminal, and afterwards every field change on the row is refused too.

A close never deletes anything. The `@pi` account is permanently blocked but kept, so a later re-registration of the same ISPB reuses the historical account — unblocked, reactivated, and with its accounting history intact.

A `PATCH` carrying only fields and no `action` changes those fields. A `PATCH` carrying nothing is a no-op that returns the current row, not an error.

<Warning>
  **There is no DELETE on `/v1/indirects`, and `CLOSED` is terminal.** The registry's only exit is `close`, which keeps the row and the account. An indirect registered by mistake against a live tenant stays in that tenant's registry for good, and its ISPB is held against the open-uniqueness constraint until it is closed. Every institution code you spend is permanent — verify `name`, `ispb` and the delivery endpoint before you call.
</Warning>

## What goes wrong

***

Every refusal this registry can answer — its status, its exact condition, and what was or was not written when it fired — is on the operation pages: [Register an indirect participant](/en/reference/interfaces/pix-jd/register-an-indirect-participant), [Update an indirect participant](/en/reference/interfaces/pix-jd/update-an-indirect-participant), [Get an indirect participant](/en/reference/interfaces/pix-jd/get-an-indirect-participant), [List indirect participants](/en/reference/interfaces/pix-jd/list-indirect-participants), [Get an indirect participant's JWK Set](/en/reference/interfaces/pix-jd/get-an-indirect-participant-jwk-set), and [List an indirect participant's transactions](/en/reference/interfaces/pix-jd/list-indirect-participant-transactions). The full catalog, with the exact `detail` each code carries, is the [Pix JD error list](/en/reference/interfaces/pix-jd/pix-jd-error-list).

<Note>
  **Whether to retry is decided by the status, not by the code family.** `PIX-0107` and `PIX-0123` are the same condition seen twice: a `409` means the rail read the configuration and established that the key is absent — only an operator changes that, so retrying loops forever. A `503` means the read itself did not complete, so the key is **not** known to be missing and retrying is right. Neither one stores anything.
</Note>

## Where to go next

***

* **[Hosting indirect participants](/en/interfaces/pix-jd/hosting-indirect-participants)** — the provisioning: the encryption key, the posture, and the registration call.
* **[Register an indirect participant](/en/reference/interfaces/pix-jd/register-an-indirect-participant)** — the full request contract and every refusal.
* **[List an indirect participant's transactions](/en/reference/interfaces/pix-jd/list-indirect-participant-transactions)** — the reconciliation feed, with the paging rules in full.
* **[Update an indirect participant](/en/reference/interfaces/pix-jd/update-an-indirect-participant)** — field changes, lifecycle actions and the own-certificate configuration.
* **[Direct Pix via JD](/en/interfaces/pix-jd/direct-pix-via-jd)** — the direct participation this all rides on, and how settled movements land in Midaz.
