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

# Update an indirect participant

> Applies mutable-field changes and/or ONE lifecycle action to an indirect. Absent (null) body fields are left untouched, so a PATCH carrying only action moves the lifecycle and changes nothing else; a PATCH carrying neither a field nor an action is a no-op that returns the current row without writing anything, not an error.

The lifecycle actions are suspend, reactivate and close, and each one writes the LEDGER as well as the registry. suspend disables both sending and receiving on the indirect's `@pi` account, so the ledger refuses that participant's money even if another replica's resolution cache is still stale; reactivate re-enables both. close is IRREVERSIBLE — CLOSED is terminal, nothing leaves it, and afterwards every field mutation on the row is refused too. close never deletes: the `@pi` account is permanently blocked but kept, so a later re-registration of the same ISPB reuses it. When a lifecycle action and field changes arrive together, the action is what the audit trail records.

Refusals: 409 PIX-0094 when the action is illegal from the current status (any action on a legacy PENDING_PROVISIONING row or on a CLOSED one, reactivate on anything but SUSPENDED) — re-issuing an action the row already reached is illegal too, not an idempotent success; 409 PIX-0096 when close is attempted while the `@pi` account still holds an available or on-hold balance, refused so the funds cannot be stranded (settle it to zero first); 409 PIX-0007 when another writer changed the row concurrently — reload and retry; 404 PIX-0095 for an unknown id; 422 PIX-0098 for an unknown action, a name over 120 characters, a delivery.endpointUrl that is not a valid https URL, or a qrCertificate.publicBaseUrl that is absent while ownCertificate is true, carries a scheme, is not a bare host with an optional path, or does not name a publicly resolvable host; 422 PIX-0111 when the body carries qrCertificate and this tenant's indirect-participants feature is not enabled — the WHOLE patch is refused, including any name or delivery change riding alongside; 503 PIX-4002 when the ledger could not be reached to check the balance or move the account flags.



## OpenAPI

````yaml /en/openapi/v3-current/pix.yaml patch /v1/indirects/{indirectId}
openapi: 3.1.0
info:
  description: >-
    The API of a DIRECT Brazilian PIX participant, integrated with BACEN's DICT
    (key directory) and SPI (settlement) rails through JDPI. It covers the
    directory (key entries and claims), payments and devolutions over SPI or
    on-us, per-account limits, QR codes with self-hosted signed payloads, the
    inbound webhooks JDPI calls on this participant, MED 2.0 (BACEN's special
    return mechanism), PIX Automático (recurring payments), and the registry of
    INDIRECT participants served through this participant's ISPB.


    ## Domain map


    - **Entries** / **Claims** — the DICT directory: registering PIX keys, and
    the claim process for taking a key over from another PSP.

    - **Transactions** / **Refunds** / **Limits** / **Banks** — SPI payments:
    outbound orders (two-phase over SPI, synchronous on-us), payee-initiated
    devolutions, per-account outbound limits, and the live SPI participant
    catalog.

    - **QR Codes** — static and immediate dynamic QR codes, decode, and the
    anonymous public routes serving signed payloads to payer PSPs (documented in
    the tag).

    - **Webhooks** — the inbound mirror: cash-in, payee-bank-initiated
    devolução, and synchronous account validation, called BY JDPI on this
    participant.

    - **Indirects** — the institutions this direct participant serves: registry,
    lifecycle, settlement positions, delivery notices, reconciliation feed, and
    the optional own QR certificate.

    - **MED Infractions / Refunds / Recoveries / Fraud Markers / Inbound
    Credit** — MED 2.0: infraction reports, the refund-request money flow in
    both roles, creator-side value recoveries, DICT fraud markers, and the
    settlement credit webhook.

    - **PIX Automatico** — recurring authorizations, schedules, composite QR
    codes, the inbound registration/validation and settlement legs JDPI calls,
    and the event-stream ingress.

    - **Systemplane** — the operator-facing runtime-configuration surface;
    payment clients never need it.


    ## Authentication


    Every operation requires a bearer token minted by the platform's
    machine-to-machine (M2M) OAuth flow with the DIRECT participant's
    credentials; authorization is per resource with actions mirroring the HTTP
    verbs. INDIRECT participants never hold a credential: an indirect's id on a
    request is ROUTING DATA supplied and guaranteed by the direct participant,
    not an authentication principal. The exception to authentication is the
    public QR payload/JWKS routes described under the QR Codes tag, which are
    anonymous by construction — the payer's PSP, not a registered client, is
    their caller.


    ## Money: one API, two units


    Every money field of THIS API's contract is an **int64 count of centavos** —
    in JSON bodies, query, path and header alike (`110001` means R$ 1.100,01).
    Every money field on a body AUTHORED BY JDPI — the inbound webhook mirrors
    under Webhooks, MED Inbound Credit and the PIX Automático
    inbound/settlement/event routes — is a **JSON number in reais** (`1100.01`),
    because that is JD's contract on the payloads JD sends, converted to
    centavos at the boundary. The discriminator is WHICH SIDE AUTHORED THE BODY,
    never the field's language: Portuguese-named fields on this API's own write
    surfaces (e.g. `valorDevolucao` on MED writes) are still integer centavos.
    Each field's description states its unit.


    ## Errors


    Errors are RFC 9457 `application/problem+json`, and every one carries a
    `PIX-XXXX` taxonomy code in the `code` member — match on the code, not on
    the human-readable text. The status discipline is ATTRIBUTION: a 4xx means
    the request, or the data the request names, is what failed (the body names
    the field or entity); a 5xx names the failing dependency (e.g. `503
    PIX-1050` — the JDPI service is temporarily unavailable) rather than hiding
    behind a generic error. `500 PIX-0109` is the one deliberate exception: an
    unattributed internal failure, whose detail is scrubbed — as is every
    response body with status 500 or above.


    ## Idempotency


    Three families, each protecting a different leg:


    - **`clientRequestId`** (optional, on payment orders) is the only identifier
    that exists BEFORE the JDPI call, so it is the only retry protection a
    manually typed order has. Reusing one — or an `endToEndId` — is refused `409
    PIX-0029`.

    - **The JDPI `Chave-Idempotencia`**: where a write takes a client
    `idempotencyKey` (the MED surfaces), the value you send is a SEED — it is
    projected to a deterministic GUID (UUIDv5) before being forwarded, so one
    client key always lands in one JDPI dedup slot and a retry stays a retry.
    Correlate JDPI-side logs with the projection, never with the raw value. DICT
    writes derive their key from the full request, so a transport retry never
    double-applies.

    - **Ledger postings** are idempotent per end-to-end id, namespaced per LEG,
    so the two legs of one devolution (or a debit and its estorno) can never
    collide on one idempotency slot.


    Multi-tenant deployments resolve the tenant from the validated token, never
    from payloads or paths; the public QR routes' ISPB path segment is a routing
    selector for public key material, not an identity.
  title: plugin-br-pix-jd
  version: 1.0.0
servers: []
security:
  - BearerAuth: []
tags:
  - description: >-
      The SPI participant catalog (JDPI §8.6.1), read live from JDPI on every
      call with nothing cached — latency and availability here are JDPI's, and
      an unreachable JDPI answers 503 PIX-1050. Two projections of the same
      catalog: GET /v1/banks is the reduced (ispb, legal name) pair a payment
      form needs; GET /v1/banks/participants is the full record with
      participation type, modality and status. Keep ispb a STRING — it is 8
      characters and its leading zeros are significant.


      This is the NATIONAL directory, not this deployment's own registry: a
      participant classified as indirect here (participantType=2) has no
      relation to the indirect participants registered with POST /v1/indirects,
      and neither list implies membership in the other.
    name: Banks
  - description: >-
      DICT key claims (reivindicação): the BACEN process for taking over a PIX
      key that is currently registered at another PSP — portability of your own
      key, or an ownership claim on a key someone else holds. A claim is a
      negotiation between two PSPs mediated by the DICT, and this surface
      exposes both sides of it: open a claim (POST /v1/claims), list and inspect
      the ones this participant is involved in, confirm (the donor side
      acknowledging), conclude (the claimer side completing the transfer), and
      cancel.


      Claims move no money — they move DIRECTORY state, and several steps are
      irreversible once the DICT records them: a concluded claim has already
      re-pointed the key at the claiming account. Each operation's description
      states which side of the claim calls it and in which claim status it is
      legal; a step attempted out of order is refused with a coded 4xx rather
      than forwarded to the DICT.
    name: Claims
  - description: >-
      DICT key entries (vínculo): registering, updating, listing and removing
      the PIX keys — CPF/CNPJ, phone, email or random key (EVP) — that bind an
      addressable alias to one of this participant's accounts in the BACEN
      directory. The surface also carries the verification call (a batch
      existence check against the DICT) and the two bulk-removal flows, account
      closure (POST /v1/account-closures) and holder removal (POST
      /v1/holder-removals), which drop every key bound to a coordinate at once.


      The rule integrators trip on: the DICT locates keys by the FULL account
      coordinate — ISPB, branch, account number AND account type (tpConta) — so
      a lookup or removal that names the wrong account type finds nothing,
      silently: the answer is an empty 200, indistinguishable from an account
      that genuinely holds no keys. Registration is idempotent at JDPI through a
      deterministic idempotency projection, so a transport retry of the same
      request does not double-register a key.
    name: Entries
  - description: >-
      **What this domain is.** This participant is a DIRECT PIX participant, and
      it can serve INDIRECT participants: other institutions that reach SPI and
      the DICT through this participant's ISPB. This surface is the registry of
      those institutions — registering one, suspending, reactivating and closing
      it, reading the key set its QR codes are signed with, and reading the
      reconciliation feed of every movement that rode its settlement position.


      **The credential model, because it decides how you integrate.** An
      indirect participant NEVER holds a credential for this API. Every call is
      authenticated as the direct participant, and the indirect's id on an order
      or a registration row is ROUTING DATA: it decides which settlement
      position the money moves on, and the guarantee that a request really
      belongs to that institution is the direct participant's responsibility,
      made before the request is sent — this API does not re-validate it.


      **Where the money moves.** Registering an institution creates a dedicated
      ledger settlement position for it (its `@pi_{ispb}` account), atomically
      with the registry row. From then on: an outbound order carrying its
      indirectId is HELD on that position (the debit source always comes from
      the registry, never from the caller's body), an inbound credit whose
      receiver ISPB matches it is credited to that position, and the response to
      an indirect order is JD's own acknowledgement passed through verbatim
      (202). The feed at GET /v1/indirects/{indirectId}/transactions is the
      record of truth for all of it: windowed (the `since` bound is required),
      cursor-paged, oldest first, amounts in integer centavos.


      **Lifecycle.** ACTIVE ⇄ SUSPENDED → CLOSED. PATCH carries at most ONE
      lifecycle action per request (suspend, reactivate or close) alongside
      ordinary field updates. Suspending flips the settlement position's
      send/receive flags before the registry row changes, so the ledger
      backstops the registry rather than trailing it. CLOSED is terminal and
      requires the settlement position to hold ZERO balance (409 PIX-0096); the
      position is blocked, never deleted, and re-registering the same ISPB later
      reuses it. Repeating the current state is refused 409 PIX-0094 —
      deliberately not a no-op, so a double-fired automation is visible. One
      ISPB maps to at most one non-CLOSED registration (409 PIX-0093).


      **Delivery notices.** When an indirect's registration carries a delivery
      endpoint, the plugin can push JD's own payload BYTE-FOR-BYTE to it as an
      HTTPS POST carrying X-Lerian-Signature ('sha256=' + the hex HMAC-SHA256 of
      the exact body bytes under the registration's shared secret) and
      X-Lerian-Timestamp. Delivery is AT-LEAST-ONCE: the institution MUST dedup
      on the end-to-end id, and must treat the transactions feed — not the
      notice — as the record of truth, because a notice can fail permanently, or
      never be queued at all, while the payment stands. The shared secret is
      write-only: PATCH replaces it, and every read renders it masked.


      **The QR certificate option, which is commercial, not compliance.** By
      default a QR raised for an indirect is signed with the DIRECT
      participant's certificate and hosted on the direct's domain — the ordinary
      arrangement, and no registration step here is needed for an indirect to
      transact. An institution that opts into its OWN certificate registers it
      via PATCH `qrCertificate`: `ownCertificate: true` plus a scheme-less,
      publicly resolvable `publicBaseUrl` (with the shipped path defaults the
      base URL may spend at most 37 of BACEN's 77-character payload-URL budget —
      measure the hostname BEFORE promising it). The whole sub-object is gated
      on this tenant's indirect-participants feature (off = 422 PIX-0111), and
      it should be switched on only after the certificate is live on BACEN's
      list — before that, payments fail silently at the payer's app, which
      validates a signature nothing vouches for. The institution then hosts its
      own signed payloads AND the key set they validate against: read that key
      set with GET /v1/indirects/{indirectId}/jwks (an authenticated operator
      read; 422 PIX-0114 when the institution has no own certificate — the
      direct participant's public JWKS route serves that case) and serve the
      bytes verbatim. A SUSPENDED institution can still read its key set: QRs
      already in the wild still need validating.


      **Refusals to know.** 404 PIX-0095 unknown (or malformed) indirect id — an
      id belonging to another tenant is deliberately indistinguishable from an
      unknown one; 422 PIX-0100 the institution is not ACTIVE; 422 PIX-0102 a
      payer ISPB on an indirect order disagrees with the institution's own; 409
      PIX-0093/0094/0096 the lifecycle rules above; 422 PIX-0111/0112/0113/0114
      the QR-certificate rules above; 404 PIX-0119 an inbound credit for a
      receiver this participant does not serve.
    name: Indirects
  - description: >-
      Per-account PIX transaction limits, enforced on OUTBOUND money only. Four
      limit rows exist per account — transaction and period, each in a daily and
      a nightly band — and the day/night boundary comes from this institution's
      policy (whole hours, default 06:00–20:00 UTC as the daily window), not
      from the request.


      A fresh account legitimately answers GET /v1/limits with an empty list and
      GET /v1/limits/available with 404: the rows are seeded with defaults by
      the account's FIRST outbound transaction, not at account creation. An
      account with no rows is therefore NOT unlimited — the enforcer seeds and
      then enforces, failing closed if a limit cannot be established. PATCH
      /v1/limits is update-only: it adjusts a row that exists, and naming a
      (type, period) tuple that has no row yet is refused 422 PIX-0036. A
      payment over the applicable limit is refused 409 PIX-0032 before any money
      moves.
    name: Limits
  - description: >-
      MED 2.0 fraud markers (marcação de fraude): the DICT records this
      participant creates against a person or key involved in fraud, feeding
      BACEN's shared fraud database. Create a marker (POST
      /v1/med/fraud-markers), cancel one you created, list the local working
      set, inspect one, and query the DICT's own view (GET
      /v1/med/fraud-markers/dict).


      Markers move no money and have a two-party lifecycle: created here,
      acknowledged/consolidated at the DICT. The write operations take a client
      `idempotencyKey`; it is projected to a deterministic GUID before reaching
      JDPI (see the API description's idempotency section), so a retry with the
      same key lands in the same JDPI dedup slot.
    name: MED Fraud Markers
  - description: >-
      The MED refund CREDIT leg (JDPI §9.3.3): the webhook JDPI calls on this
      participant when a special-return devolution settles in this participant's
      favor — money coming BACK after a MED refund request this participant (or
      its customer) won. It is called by JDPI, not by your systems; the
      operation is documented so you can recognize the flow and its ledger
      effect, and so the counterparty configuration names the right path.


      This is a MONEY route: a valid credit posts to the recipient customer's
      ledger account before the acknowledgement is returned. Direction is
      guarded — the credit is accepted only when the payload's recipient ISPB is
      this participant's own — and the amount arrives as a JSON number in REAIS
      (JD's contract on bodies JD authors), converted at the boundary; this
      mirror shape is the exception to the API-wide integer-centavos rule. A
      replayed delivery of an already-settled credit is acknowledged
      idempotently rather than posted twice.
    name: MED Inbound Credit
  - description: >-
      MED 2.0 infraction reports (relato de infração): the notifications that
      open BACEN's special return mechanism on a transaction — typically fraud
      reported by the payer's PSP against a payment this participant received.
      Inbound reports addressed to this participant are collected from JDPI by a
      background poller and land in the local working set automatically; this
      surface is where you SEE them (list, inspect, read JDPI's own view) and
      ACT on them (analyze — agree or disagree — and cancel one this participant
      created).


      Infractions move no money themselves; they are the predicate for MED
      Refunds, where the money leg lives. Analysis is subject to a BACEN SLA
      measured from the report's creation, and the write operations take a
      client `idempotencyKey` that is projected to a deterministic GUID before
      reaching JDPI.
    name: MED Infractions
  - description: >-
      MED 2.0 value recoveries (recuperação de valores): the creator-side flow
      this participant runs when its own customer was defrauded and the money
      went to another PSP — open a recovery (POST /v1/med/recoveries), amend it
      while the counterparty analysis allows, and, once the counterparty
      approves, execute the devolution step (devolver) within the 72-hour window
      that approval arms. GET /v1/med/recoveries/awaiting-devolver is the
      operator worklist of recoveries whose clock is running.


      The recovery API at JDPI has no pending-list poll, so open recoveries are
      re-synced by a background poller — local state can trail the counterparty
      by one poll interval. The devolver step is the money-bearing action of the
      flow; everything else is negotiation state. Write operations take a client
      `idempotencyKey`, projected to a deterministic GUID before reaching JDPI.
    name: MED Recoveries
  - description: >-
      MED 2.0 refund requests (solicitação de devolução): the special-return
      money flow itself. This participant appears in BOTH roles — as SOLICITANTE
      it opens a refund request against another PSP (POST /v1/med/refunds); as
      CONTESTADO it receives requests (collected from JDPI by a background
      poller — GET /v1/med/refunds/pending-analysis is the analysis worklist),
      analyzes them within the BACEN SLA, and, when it accepts, EFFECTUATES the
      devolution: POST /v1/med/refunds/{idSolDevolucao}/effectuate debits the
      customer who received the original payment and submits the pacs.004 over
      SPI.


      Effectuation is the money leg, and it is two-phase: the debit is HELD as a
      pending ledger posting when the effectuate call is accepted, and a
      background settlement reconciler commits it when SPI settles (or releases
      it on a terminal SPI error) — so an accepted effectuate is not yet settled
      money, and the row's state says which side of that line it is on.
      Direction is guarded on the participant's own ISPB: only the CONTESTADO
      (the PSP whose customer RECEIVED the original payment) can effectuate,
      refused 400 PIX-0088 otherwise, and the mirror credit of a refund this
      participant WON arrives on the MED Inbound Credit webhook. GET
      /v1/med/refunds/credits/{endToEndIdDevolucao} reads that credit's landing.
      Client `idempotencyKey` values on every write are projected to a
      deterministic GUID before reaching JDPI — correlate JDPI-side logs with
      the projection, not the raw value you sent.
    name: MED Refunds
  - description: >-
      PIX Automático: BACEN's recurring-payment arrangement, where a payer
      authorizes a merchant's PSP to pull scheduled payments. This surface
      carries the whole arrangement from this participant's seat: recurring
      authorizations (recorrência — create, confirm, respond, adjust the max
      value, cancel), the payment schedules under them (agendamento — create,
      list, cancel), the composite QR codes that carry a charge and a recurrence
      in one artifact (static, dynamic, cobv and recurrence-only, with their
      updates and decode), the `/inbound` registration and validation endpoints
      JDPI calls on this participant per annexes §5.2/§5.3, the §5.4 settlement
      legs (block, debit, and the compensating estorno via DELETE), and the §6
      event-stream ingress (`/events/*`) through which JDPI pushes lifecycle
      status.


      Which party calls each route matters more here than anywhere else in the
      API: `/inbound/*`, `/events/*` and `/settlements/*` are called BY JDPI on
      this participant, while the rest are called by your systems. The §5.4
      settlement legs move money — the debit posts against the paying customer,
      the estorno compensates it — and both are idempotent per end-to-end id
      with the estorno namespaced separately, so the two legs of one scheduled
      payment can never collide. Amounts on this participant's own operations
      are integer centavos; the inbound JDPI mirrors carry reais as JSON
      numbers, per the API-wide two-unit rule.
    name: PIX Automatico
  - description: >-
      **What this domain does.** Creates, updates, decodes and lists this
      participant's PIX QR codes: STATIC (a reusable key-and-optional-amount
      code) and IMMEDIATE DYNAMIC (a single-use charge whose payload is a signed
      JWS document fetched by the payer's PSP at pay time). Due-date charges
      (cobv), withdrawal and change (saque/troco) variants are RECOGNIZED by the
      decode operations but not created here; recurring composites live under
      PIX Automatico.


      **Who signs, who hosts — the part that decides your architecture.** JDPI
      holds the certificate and signs every dynamic payload; this plugin
      composes the URLs the QR carries, and by default it also STORES and SERVES
      the signed JWS on its own public routes, so the QR that comes back is
      end-to-end resolvable with no work on your side. The exception is a charge
      raised for an indirect participant that holds its OWN certificate: the
      signed JWS comes back in the create/update response and is NOT stored —
      that response is the ONLY delivery, and the indirect hosts the document
      (and the matching key set, read via GET /v1/indirects/{id}/jwks) on its
      own domain. If that response is lost, the charge nevertheless exists at
      JDPI: recover it via GET /v1/qrcodes/list (match your
      recipientConciliationId) and PUT /v1/qrcodes/dynamic/{documentId}, which
      re-signs and returns a fresh payloadJws rather than reproducing the lost
      one.


      **The dynamic-QR lifecycle.** Create validates everything BEFORE calling
      JDPI — the recipient's name and city come from this participant's CRM
      records, never from the body; the payload URL is composed first because
      JDPI requires it in the request; the conciliation id must be new (409
      PIX-0065) — so a refused create never leaves a charge behind, and a JDPI
      failure after persistence is compensated by deleting the local row. Update
      (JDPI §8.3.5) changes the amount, conciliation, expiration, payer or
      charge status (0=Active, 1=Concluded, 2=Removed by recipient, 3=Removed by
      PSP), and REUSES the location: a printed QR keeps resolving, because the
      updated JWS atomically supersedes the old one at the same path id. The
      plugin stores no charge↔indirect association, so EACH update's indirectId
      decides who signs that revision — omitting it on an indirect-hosted charge
      moves the charge onto the direct participant's certificate and domain.


      **The 77-character budget.** BACEN caps the payload URL (urlPayloadJson)
      at 77 characters, scheme-less, and the path this deployment is configured
      with spends most of it. An over-budget URL is refused by ATTRIBUTION: 422
      PIX-0112 when an indirect's registered base URL is what does not fit
      (choose the hostname against the remaining allowance BEFORE registering
      it), 500 PIX-0053 when the deployment's own configuration leaves no room
      for any charge.


      **Decode and list.** Decode accepts the scanned EMV payload (POST
      /v1/qrcodes/decode) or the payload URL itself (POST
      /v1/qrcodes/decode/url), per §8.3.10. GET /v1/qrcodes/list (§8.3.12)
      requires a bounded registration window of at most 90 days; omitting the
      type fans the query out across every QR type.


      **Public payload & JWKS routes.** Four routes exist OUTSIDE this
      specification — they are anonymous, they are called by the PAYER'S PSP
      (never by you), and they are listed here because they are part of the
      deployment's public contract even though no operation below describes
      them. Their path prefixes (the payload path and the JWK path) and their
      host vary per deployment; the shapes are:


      - `GET /{payloadPath}/cob/{id}` (single-tenant form) and `GET
      /{payloadPath}/{ispb}/cob/{id}` (multi-tenant form, where the ISPB segment
      selects the serving participant) — the signed JWS payload of one dynamic
      charge, returned raw with Content-Type `application/jose`. The `cob`
      segment is LITERAL: only immediate charges are self-hosted, and a `cobv`
      fetch answers 404 rather than handing the payer's PSP a resource of a type
      it did not ask for.

      - `GET /{jwkPath}` (single-tenant) and `GET /{jwkPath}/{ispb}`
      (multi-tenant) — the JWK Set the payloads validate against, as
      `application/jwk-set+json`, served from a short-lived cache (minutes) over
      JDPI's key material.


      The 22-character `{id}` is a cryptographically random token minted per
      charge, and it is the route's only barrier: unguessable by construction,
      it acts as a capability — knowing the URL is what authorizes the fetch,
      which is exactly the EMV model, where the URL travels inside the QR. A
      missing, expired or unknown payload — and an unknown ISPB — answer a
      UNIFORM 404 with code PIX-0063, deliberately not distinguishing absent
      from expired: a payer PSP must not pay an expired QR, and nothing more is
      anyone else's business. Server-side faults (the participant lookup
      unavailable, JDPI unreachable with nothing cached) answer 503. Errors on
      these routes are RFC 9457 problem+json like everywhere else.
    name: QR Codes
  - description: >-
      Devolution (devolução) of a RECEIVED PIX payment — the BACEN rule is that
      refunds are PAYEE-initiated, and this surface encodes it: only the party
      that received the money can return it. A refund never mutates the original
      transaction; it is a NEW transaction row (is_refund=true) whose amount is
      capped at the original minus every prior refund of it (409 PIX-0032 over
      the cap). GET /v1/refunds/reasons is the catalog of accepted devolution
      reason codes.


      Refunding a payment this participant SENT is refused 400 PIX-0089 (ask the
      payee's PSP instead — or, for the special return mechanism, see MED
      Refunds), and a requester who is not the original payee is refused 400
      PIX-0088. An on-us original settles the return synchronously in the
      ledger; an original that arrived over SPI is returned through JDPI first,
      and the ledger posting follows JD's acceptance.
    name: Refunds
  - description: >-
      **What this domain does.** Outbound PIX payments — over SPI to any other
      participant, or settled on this participant's own book when the payee is
      on-us — plus the read surface that closes the loop on them. The Refunds
      and Limits tags are part of the same flow: a devolution is a new
      transaction row, and limits are enforced here on the way out.


      **The lifecycle, and which flow settles when.** A transaction's status is
      PENDING (0), EXECUTED (2) or ERROR (-1), and the only real edge is PENDING
      → EXECUTED or ERROR. An on-us (INTRA_PSP) order settles SYNCHRONOUSLY: one
      final ledger posting, answered 201 already EXECUTED. An external CASH_OUT
      is the API's only TWO-PHASE money flow: the amount is HELD on the paying
      position as a pending ledger debit BEFORE JDPI is called; if JDPI refuses
      the order the hold is released immediately and nothing persists; if JDPI
      accepts (§8.4.1, asynchronous by design) the order persists PENDING, and a
      background reconciler polls JDPI (§8.4.2) on a short tick — settlement (JD
      processing status 9) commits the hold and flips the row EXECUTED, a
      terminal SPI error (-1) releases the hold and flips it ERROR. EXECUTED and
      ERROR are terminal. Inbound credits (CASH_IN) appear in this feed already
      EXECUTED — they settled when the webhook posted them.


      **What that means for a client.** A 201/202 on POST /v1/transactions is an
      ACCEPTED order, not settled money: poll GET
      /v1/transactions/{transactionId} until the status is terminal. PENDING
      means "SPI has not decided", never "stuck" — the reconciler acts on its
      own tick and the status read is side-effect-free. An indirect order
      (indirectId in the body) answers 202 with JD's own acknowledgement
      verbatim instead of the 201-plus-plugin-body of a direct order; handle
      both shapes.


      **Rules you cannot guess from the schemas.** (1) Amounts are integer
      centavos everywhere on this surface. (2) Send endToEndId only when you
      already hold one — from a key lookup or a QR decode — and then payee.key
      is mandatory with it (422 PIX-0037 otherwise); omit it on a manually typed
      order and SPI mints one, returned in the response. (3) The optional
      clientRequestId is the only identifier that exists BEFORE the JD call, so
      it is the ONLY retry protection a manually typed order has; reusing one
      (or an endToEndId) is refused 409 PIX-0029. (4) Every refusal is decided
      before the hold is taken, so a refused order moves ZERO money. (5) The
      list endpoint has NO pagination — the whole matching set arrives in one
      array, so filter narrowly; and on a CASH_IN row account_id records the
      ledger alias credited rather than a CRM account id, so correlate inbound
      rows by end_to_end_id.


      **Errors that matter.** 409 PIX-0029 duplicate endToEndId/clientRequestId;
      409 PIX-0076 insufficient balance on the paying position; 409 PIX-0032
      period limit exceeded; 409 PIX-0078 transfer to the same account; 422
      PIX-0030 non-positive amount; 422 PIX-0033 the payee resolves to more than
      one account (on-us) — refused rather than guessed; 422 PIX-0108 a party
      resolves in the CRM but is bound to no ledger account; 404 PIX-0095 / 422
      PIX-0100 unknown / not-ACTIVE indirect; 503 PIX-1050 JDPI unreachable (the
      dependency is named, and the order was not placed).
    name: Transactions
  - description: >-
      **What this domain is — and is not.** The INBOUND mirror of the JD
      integration: the three endpoints JDPI calls on THIS participant. It is not
      client webhook management — there is nothing to subscribe to here. The
      callers of these routes are JDPI's systems, authenticated with the same
      bearer scheme as everything else; they are documented so an integrator can
      recognize the flows, and so the counterparty configuration names the right
      paths.


      **The three flows.** POST /v1/webhooks/cash-ins is the §9.3.2 inbound
      credit: money received by this participant or by an indirect it serves.
      POST /v1/webhooks/refunds is the §9.3.3 payee-bank-initiated devolução of
      a payment this participant SENT: JD has already executed the return when
      this arrives. POST /v1/webhooks/validates is the §9.3.1 synchronous
      account validation: a read-only preflight the payer's PSP runs before
      sending money.


      **Money arrives in REAIS here.** These bodies are authored by JD, so their
      money fields are JSON numbers in reais (converted to integer centavos at
      the boundary) — the mirror exception to the API-wide centavos rule.
      Everything this API itself answers or persists is centavos.


      **Acknowledged is not credited.** The §9.3.2/§9.3.3 success
      acknowledgement carries the triad idReqJdPi / idCreditoSgct /
      dtHrCreditoSgct, and the field that says money landed is idCreditoSgct.
      The one 200 WITHOUT it is the short-circuit for a payer settled by BACEN
      itself (ISPB 99999), which acknowledges without crediting. Refusals are
      ordinary coded HTTP errors — this surface does not hide a refusal inside a
      200.


      **How a cash-in resolves, in order.** BACEN payer → acknowledge without
      credit. Receiver on this participant's OWN ISPB → own-book credit, located
      by the ADDRESSED coordinates (branch + account number; the receiver
      document is cross-checked, not used to search — a divergence refuses
      rather than re-routes). Receiver ISPB matching an ACTIVE indirect
      participant → credit to that institution's settlement position, with the
      delivery notice recorded in the same unit of work. Anything else — an ISPB
      this participant does not serve — is REFUSED 404 PIX-0119; money is never
      parked or blindly posted. The credit is posted to the ledger BEFORE the
      acknowledgement is returned, and redelivery is deduplicated on endToEndId
      (409 PIX-0029 names a replay of a different payload; an identical replay
      of a settled credit is acknowledged idempotently). Other refusals name
      what failed to resolve: 404 PIX-0115 no account at the addressed
      coordinates, 409 PIX-0116 the coordinates resolve ambiguously, 409
      PIX-0117 the addressed account belongs to a different holder than the
      payload names, 409 PIX-0118 the account cannot be credited.


      **The devolução leg holds a PARK.** A returned payment whose original is
      already EXECUTED credits the original payer immediately. One whose
      original is still PENDING (the outbound settlement race) is acknowledged
      200 and PARKED durably — the acknowledgement's idCreditoSgct carries the
      park id — and a background sweep completes the credit once the original
      settles, or escalates it if the original ends in ERROR.


      **Validation always answers 200.** Every §9.3.1 outcome is a 200 whose
      body carries the verdict and reason: BE17 for a duplicate payment inside
      the 24h window or a payer mismatch on the referenced charge, AC03 for an
      account that does not resolve, BE01 for a document that diverges from the
      account's holder. An HTTP error from this route means infrastructure,
      never "account invalid".
    name: Webhooks
paths:
  /v1/indirects/{indirectId}:
    patch:
      tags:
        - Indirects
      summary: Update an indirect participant
      description: >-
        Applies mutable-field changes and/or ONE lifecycle action to an
        indirect. Absent (null) body fields are left untouched, so a PATCH
        carrying only action moves the lifecycle and changes nothing else; a
        PATCH carrying neither a field nor an action is a no-op that returns the
        current row without writing anything, not an error.


        The lifecycle actions are suspend, reactivate and close, and each one
        writes the LEDGER as well as the registry. suspend disables both sending
        and receiving on the indirect's `@pi` account, so the ledger refuses
        that participant's money even if another replica's resolution cache is
        still stale; reactivate re-enables both. close is IRREVERSIBLE — CLOSED
        is terminal, nothing leaves it, and afterwards every field mutation on
        the row is refused too. close never deletes: the `@pi` account is
        permanently blocked but kept, so a later re-registration of the same
        ISPB reuses it. When a lifecycle action and field changes arrive
        together, the action is what the audit trail records.


        Refusals: 409 PIX-0094 when the action is illegal from the current
        status (any action on a legacy PENDING_PROVISIONING row or on a CLOSED
        one, reactivate on anything but SUSPENDED) — re-issuing an action the
        row already reached is illegal too, not an idempotent success; 409
        PIX-0096 when close is attempted while the `@pi` account still holds an
        available or on-hold balance, refused so the funds cannot be stranded
        (settle it to zero first); 409 PIX-0007 when another writer changed the
        row concurrently — reload and retry; 404 PIX-0095 for an unknown id; 422
        PIX-0098 for an unknown action, a name over 120 characters, a
        delivery.endpointUrl that is not a valid https URL, or a
        qrCertificate.publicBaseUrl that is absent while ownCertificate is true,
        carries a scheme, is not a bare host with an optional path, or does not
        name a publicly resolvable host; 422 PIX-0111 when the body carries
        qrCertificate and this tenant's indirect-participants feature is not
        enabled — the WHOLE patch is refused, including any name or delivery
        change riding alongside; 503 PIX-4002 when the ledger could not be
        reached to check the balance or move the account flags.
      operationId: updateIndirect
      parameters:
        - description: The indirect participant id.
          in: path
          name: indirectId
          required: true
          schema:
            description: The indirect participant id.
            examples:
              - 018f2b7c-0000-7000-8000-000000000000
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateIndirectBody'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Indirect'
          description: OK
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Detail'
          description: Unprocessable Entity
        '500':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Detail'
          description: Internal Server Error
        default:
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Detail'
          description: Error
components:
  schemas:
    UpdateIndirectBody:
      additionalProperties: false
      properties:
        action:
          description: Lifecycle transition; one of suspend, reactivate, close.
          examples:
            - suspend
          type: string
        delivery:
          $ref: '#/components/schemas/UpdateIndirectDelivery'
          description: New delivery endpoint and/or secret.
        name:
          description: New display name (1..120 chars).
          examples:
            - Indirect PSP Ltda
          type: string
        qrCertificate:
          $ref: '#/components/schemas/UpdateIndirectQRCertificate'
          description: >-
            New own-QR-code certificate configuration. Absent leaves it
            unchanged. Accepted only when the indirect participants feature is
            enabled for the tenant (422 PIX-0111 otherwise).
      type: object
    Indirect:
      additionalProperties: false
      properties:
        createdAt:
          description: Creation timestamp (UTC).
          format: date-time
          type: string
        delivery:
          $ref: '#/components/schemas/IndirectDelivery'
          description: Delivery endpoint (secret redacted).
        indirectId:
          description: The indirect participant id (routing identifier).
          examples:
            - 018f2b7c-0000-7000-8000-000000000000
          type: string
        ispb:
          description: The indirect PSP's ISPB.
          examples:
            - '12345678'
          type: string
        messagingMode:
          description: Delivery mode.
          examples:
            - raw
          type: string
        name:
          description: Display name.
          examples:
            - Indirect PSP Ltda
          type: string
        piAccountAlias:
          description: The derived @pi_{ispb} Midaz account alias.
          examples:
            - '@pi_12345678'
          type: string
        provisioning:
          $ref: '#/components/schemas/IndirectProvisioning'
          description: >-
            Legacy provisioning state. Always present as an object; its
            failedStep is always null for a row registered by this service.
        qrCertificate:
          $ref: '#/components/schemas/IndirectQRCertificate'
          description: Own-QR-code certificate configuration.
        status:
          description: Lifecycle status.
          examples:
            - ACTIVE
          type: string
        updatedAt:
          description: Last-update timestamp (UTC).
          format: date-time
          type: string
      required:
        - indirectId
        - name
        - ispb
        - status
        - piAccountAlias
        - messagingMode
        - delivery
        - qrCertificate
        - provisioning
        - createdAt
        - updatedAt
      type: object
    Detail:
      additionalProperties: false
      properties:
        code:
          description: >-
            Stable, machine-readable domain error code scoped to the emitting
            service (format: <SERVICE>-NNNN).
          examples:
            - ERR-0001
          type: string
        detail:
          description: >-
            A human-readable explanation specific to this occurrence of the
            problem.
          examples:
            - Property foo is required but is missing.
          type: string
        errors:
          description: Optional list of individual error details
          items:
            $ref: '#/components/schemas/ErrorDetail'
          type:
            - array
            - 'null'
        instance:
          description: >-
            A URI reference that identifies the specific occurrence of the
            problem.
          examples:
            - https://example.com/error-log/abc123
          format: uri
          type: string
        status:
          description: HTTP status code
          examples:
            - 400
          format: int64
          type: integer
        title:
          description: >-
            A short, human-readable summary of the problem type. This value
            should not change between occurrences of the error.
          examples:
            - Bad Request
          type: string
        type:
          default: about:blank
          description: A URI reference to human-readable documentation for the error.
          examples:
            - https://example.com/errors/example
          format: uri
          type: string
        upstream:
          $ref: '#/components/schemas/Upstream'
          description: >-
            RFC 9457 extension member: the error a proxied third-party provider
            reported. Absent unless the emitting service explicitly surfaced
            one.
      type: object
    UpdateIndirectDelivery:
      additionalProperties: false
      properties:
        endpointUrl:
          description: New HTTPS delivery endpoint.
          examples:
            - https://indirect.example.com/pix
          type: string
        secret:
          description: New HMAC-SHA256 signing secret; encrypted at rest; never returned.
          examples:
            - s3cr3t
          type: string
      type: object
    UpdateIndirectQRCertificate:
      additionalProperties: false
      properties:
        ownCertificate:
          description: >-
            Whether the indirect hosts the dynamic-QR JWS/JWKS under its OWN
            certificate. False falls back to the direct participant's
            certificate and FQDN, and discards publicBaseUrl.
          examples:
            - true
          type: boolean
        publicBaseUrl:
          description: >-
            The indirect's SCHEME-LESS public base URL (host plus optional path)
            the QR payload location is built on. Required when ownCertificate is
            true; ignored otherwise.
          examples:
            - qr.indirect.example.com/pix
          type: string
      type: object
    IndirectDelivery:
      additionalProperties: false
      properties:
        endpointUrl:
          description: The HTTPS delivery endpoint.
          examples:
            - https://indirect.example.com/pix
          type: string
        secret:
          description: Always redacted.
          examples:
            - '***'
          type: string
      required:
        - endpointUrl
        - secret
      type: object
    IndirectProvisioning:
      additionalProperties: false
      properties:
        failedStep:
          description: >-
            Legacy field: the registration step a PENDING_PROVISIONING row
            stopped at. Always null for an indirect registered by this service,
            since a failed registration leaves no row.
          examples:
            - createPIAccount
          type:
            - string
            - 'null'
      required:
        - failedStep
      type: object
    IndirectQRCertificate:
      additionalProperties: false
      properties:
        ownCertificate:
          description: >-
            Whether the indirect hosts the dynamic-QR JWS/JWKS under its own
            certificate. False means it falls back to the direct participant.
          examples:
            - true
          type: boolean
        publicBaseUrl:
          description: >-
            The indirect's scheme-less public base URL for QR payload locations.
            Empty when ownCertificate is false.
          examples:
            - qr.indirect.example.com/pix
          type: string
      required:
        - ownCertificate
        - publicBaseUrl
      type: object
    ErrorDetail:
      additionalProperties: false
      properties:
        location:
          description: >-
            Where the error occurred, e.g. 'body.items[3].tags' or
            'path.thing-id'
          type: string
        message:
          description: Error message text
          type: string
        value:
          description: The value at the given location
      type: object
    Upstream:
      additionalProperties: false
      properties:
        code:
          description: The upstream provider's own error code, verbatim.
          examples:
            - E4001
          type: string
        message:
          description: >-
            The upstream provider's own error message, verbatim (bounded, never
            its raw response body).
          examples:
            - account not found at provider
          type: string
      type: object
  securitySchemes:
    BearerAuth:
      bearerFormat: JWT
      description: JWT bearer token issued by the identity provider.
      scheme: bearer
      type: http

````