{
  "openapi": "3.1.0",
  "info": {
    "title": "Recivanta API",
    "version": "1.0.0",
    "summary": "The real, implemented HTTP surface of Recivanta's backend \u2014 a white-label invoice discounting (TReDS) marketplace.",
    "description": "This document describes the API exactly as the backend (`backend/internal/*/httpapi`) implements it today \u2014 every path, request field and error code here is read directly from the handler code, not from the aspirational specification in doc 06 (which also describes OAuth 2.0 client credentials, mTLS, HTTP message signatures, cursor pagination, and a broad webhook/event system \u2014 none of which exist in the code yet). Where this document and doc 06 disagree, this document is correct for integrating against the running backend today.\n\n**Authentication.** Every endpoint except `POST /v1/anchors:verify` and the pre-authentication identity endpoints requires `Authorization: Bearer <token>`, where `<token>` is a session token minted by one of the identity login endpoints (passkey login, or TOTP-only login). There is no OAuth 2.0 client-credentials flow, no mTLS, and no self-service developer application/API-key registration implemented yet \u2014 see the portal's Quickstart section for exactly how to obtain a token against a real deployment today.\n\n**Step-up.** A handful of money-moving or otherwise sensitive endpoints (accepting a bid, instructing funding, distributing a maturity waterfall, invoice acceptance) additionally require the bearer token to belong to a *step-up* (freshly re-authenticated) session \u2014 see `/v1/auth/step-up/require` and `/v1/auth/step-up/complete`. These are flagged per-operation below with a 403 `STEP_UP_REQUIRED` response.\n\n**Errors.** Every non-2xx response is `application/problem+json`, matching RFC 9457 and `internal/platform/httpx.Problem` exactly: `{type, title, status, detail, code, errors, meta}`. `code` is the stable, machine-readable value to branch on; `detail` is for logs/debugging only.\n\n**Money.** Every amount is `{amount_minor: integer, currency: string}` \u2014 integer minor units plus an ISO 4217 code, never a float.\n\n**Tenancy.** A request for another tenant's resource returns 404, never 403, to avoid revealing that the resource exists at all.",
    "contact": {
      "name": "Recivanta",
      "url": "https://developers.recivanta.com"
    }
  },
  "servers": [
    {
      "url": "https://api.{tenant}.recivanta.com",
      "description": "Production, per-tenant.",
      "variables": {
        "tenant": {
          "default": "nse",
          "description": "The tenant subdomain, e.g. `nse` for NSE Connect."
        }
      }
    },
    {
      "url": "http://localhost:8080",
      "description": "Local development (`go run ./cmd/api` from `backend/`)."
    }
  ],
  "tags": [
    {
      "name": "Identity",
      "description": "Passkey (WebAuthn) and TOTP authentication, step-up re-authentication, session logout. The pre-authentication endpoints here are the ones a real integration uses to obtain the bearer token every other endpoint requires."
    },
    {
      "name": "Participants",
      "description": "Organisation (supplier / buyer / financier / developer / settlement bank / custodian) onboarding and KYC lifecycle, including the maker-checker approval gate."
    },
    {
      "name": "Programmes",
      "description": "Anchor-buyer programme setup, limit governance (maker-checker), and supplier membership / opt-in lifecycle."
    },
    {
      "name": "Invoices",
      "description": "The Factoring Unit lifecycle: registration and its 12-state transition guard."
    },
    {
      "name": "Bidding",
      "description": "The order book, bid placement/withdrawal, and bid acceptance (trade creation)."
    },
    {
      "name": "Settlement",
      "description": "Funding and maturity-distribution instructions, and read access to settlement_instruction rows."
    },
    {
      "name": "Fees",
      "description": "Maker-checker-gated fee schedules and the fee / late-fee / simulation calculators built on them."
    },
    {
      "name": "Anchoring",
      "description": "The one genuinely public endpoint in the API: independent verification of a record's Bantu blockchain anchor. No bearer token required."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/anchors:verify": {
      "post": {
        "operationId": "verifyAnchor",
        "summary": "Verify a record's Bantu anchor",
        "description": "The one genuinely public endpoint in the entire API \u2014 deliberately mounted without auth, since the point is that any third party (regulator, auditor, counterparty) can independently verify a record's integrity against the Bantu anchor without a Recivanta account. Rate-limited by infrastructure (Caddy), not by application-level auth. A seal/record that cannot be found is a normal outcome, reported as `200 OK` with `valid: false`, never an error \u2014 anyone can submit any hash.",
        "tags": [
          "Anchoring"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnchorVerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnchorVerifyRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/dev/login": {
      "post": {
        "operationId": "devLogin",
        "summary": "Sandbox-only session bootstrap (disabled by default)",
        "description": "NOT part of the production API. This route only exists at all when the server process has `ALLOW_DEV_LOGIN=true` set (it is a true 404 otherwise, not a gated 403) \u2014 documented here because it is real, reachable code, not aspirational. Even when enabled, it issues a session with NO credential check whatsoever, and only for tenants whose `tenant.flags->>'synthetic_demo'` is `true` (every other tenant gets 403 regardless of the env var). It exists purely to unblock internal API testing against the seeded synthetic demo tenant, which otherwise has no way to ever mint a first session (every real login path needs a credential that must already be enrolled). MUST NEVER be enabled against a tenant holding real participant data.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DevLoginResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DevLoginRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/logout": {
      "post": {
        "operationId": "logout",
        "summary": "Log out (revoke the current session)",
        "description": "Revokes the caller's current session. Revoking an already-revoked (or otherwise gone) session is treated as an idempotent success, not an error.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "204": {
            "description": "Success. No response body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/auth/passkey/login/begin": {
      "post": {
        "operationId": "passkeyLoginBegin",
        "summary": "Begin WebAuthn passkey login",
        "description": "Starts a passkey login (assertion) ceremony. Every failure mode (no such user, user with no registered passkey) collapses into a single generic 401 \u2014 this never confirms or denies whether a user id exists.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasskeyLoginBeginResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Tenant-ID",
            "in": "header",
            "required": true,
            "description": "The caller's tenant. This is a deliberate, narrow stand-in: the long-term design (doc 04/06) resolves tenant from the request hostname at the gateway, but no such hostname-to-tenant registry exists yet anywhere in this codebase. Every pre-authentication identity endpoint requires this header explicitly until that lands.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasskeyLoginBeginRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/passkey/login/finish": {
      "post": {
        "operationId": "passkeyLoginFinish",
        "summary": "Finish WebAuthn passkey login",
        "description": "Completes a login ceremony started by `/begin`, submitting the browser's assertion response. On success, mints a bearer session token \u2014 this is one of the real ways to obtain the `Authorization: Bearer <token>` every other endpoint requires.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Tenant-ID",
            "in": "header",
            "required": true,
            "description": "The caller's tenant. This is a deliberate, narrow stand-in: the long-term design (doc 04/06) resolves tenant from the request hostname at the gateway, but no such hostname-to-tenant registry exists yet anywhere in this codebase. Every pre-authentication identity endpoint requires this header explicitly until that lands.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasskeyLoginFinishRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/passkey/register/begin": {
      "post": {
        "operationId": "passkeyRegisterBegin",
        "summary": "Begin WebAuthn passkey registration",
        "description": "Starts a passkey registration ceremony for an existing user (registering a passkey happens after some other login \u2014 this is not itself a login path). Returns WebAuthn creation options to pass to the browser's `navigator.credentials.create()`.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasskeyRegisterBeginResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Tenant-ID",
            "in": "header",
            "required": true,
            "description": "The caller's tenant. This is a deliberate, narrow stand-in: the long-term design (doc 04/06) resolves tenant from the request hostname at the gateway, but no such hostname-to-tenant registry exists yet anywhere in this codebase. Every pre-authentication identity endpoint requires this header explicitly until that lands.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasskeyRegisterBeginRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/passkey/register/finish": {
      "post": {
        "operationId": "passkeyRegisterFinish",
        "summary": "Finish WebAuthn passkey registration",
        "description": "Completes a registration ceremony started by `/begin`, submitting the browser's attestation response.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PasskeyRegisterFinishResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Tenant-ID",
            "in": "header",
            "required": true,
            "description": "The caller's tenant. This is a deliberate, narrow stand-in: the long-term design (doc 04/06) resolves tenant from the request hostname at the gateway, but no such hostname-to-tenant registry exists yet anywhere in this codebase. Every pre-authentication identity endpoint requires this header explicitly until that lands.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasskeyRegisterFinishRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/step-up/complete": {
      "post": {
        "operationId": "stepUpComplete",
        "summary": "Complete a step-up challenge",
        "description": "Re-verifies the caller via a TOTP code (the only re-verification method implemented today \u2014 a WebAuthn assertion ceremony could complete step-up too, but that needs its own begin/finish exchange, not yet built) and, on success, returns a new short-lived step-up session token. Use this new token's bearer value for the step-up-gated call that follows.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StepUpCompleteRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/step-up/require": {
      "post": {
        "operationId": "stepUpRequire",
        "summary": "Request a step-up challenge",
        "description": "Mints a step-up challenge for the caller's current, already-authenticated session. Call this before `/v1/auth/step-up/complete` whenever an endpoint responds 403 `STEP_UP_REQUIRED`.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StepUpRequireResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/auth/totp/confirm": {
      "post": {
        "operationId": "totpConfirm",
        "summary": "Confirm a pending TOTP factor",
        "description": "Confirms the caller's pending TOTP factor by verifying a real code generated against it, making it an active, usable factor.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TOTPConfirmResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TOTPConfirmRequest"
              }
            }
          }
        }
      }
    },
    "/v1/auth/totp/enroll": {
      "post": {
        "operationId": "totpEnroll",
        "summary": "Enroll a new TOTP factor",
        "description": "Generates a fresh TOTP secret and marks it as pending (unconfirmed) for the caller. Returns the base32 secret and an `otpauth://` URL to render as a QR code. Must be confirmed via `/v1/auth/totp/confirm` with a real generated code before it is usable for login or step-up.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TOTPEnrollResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/auth/totp/verify-login": {
      "post": {
        "operationId": "totpVerifyLogin",
        "summary": "Log in with a TOTP code",
        "description": "Issues a bearer session token given a user id and a correct, confirmed TOTP code. Honest limitation: password verification is not built anywhere in this codebase yet (no password hash column, no service method for it) \u2014 this endpoint is TOTP-ONLY login, not password-plus-second-factor. Treat it as a second factor bolted onto an identity the caller already otherwise trusts, not as a complete standalone login flow, until password verification is added. Every failure mode (wrong code, no confirmed TOTP factor, unknown user) collapses into the same generic 401, to avoid a username/enrollment-status enumeration oracle.",
        "tags": [
          "Identity"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Tenant-ID",
            "in": "header",
            "required": true,
            "description": "The caller's tenant. This is a deliberate, narrow stand-in: the long-term design (doc 04/06) resolves tenant from the request hostname at the gateway, but no such hostname-to-tenant registry exists yet anywhere in this codebase. Every pre-authentication identity endpoint requires this header explicitly until that lands.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TOTPVerifyLoginRequest"
              }
            }
          }
        }
      }
    },
    "/v1/bids": {
      "post": {
        "operationId": "placeBid",
        "summary": "Place a bid",
        "description": "CAVEAT: `financier_org_id` is taken directly from the request body \u2014 there is no membership/participant lookup wired in yet to verify the authenticated caller actually belongs to that financier org. A real deployment must add that check before trusting this value. Placing a second bid on an invoice you already have a live bid on must strictly improve the rate, or this fails with 422 `RATE_MUST_IMPROVE`.",
        "tags": [
          "Bidding"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Bid"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaceBidRequest"
              }
            }
          }
        }
      }
    },
    "/v1/bids/{bidID}/withdraw": {
      "post": {
        "operationId": "withdrawBid",
        "summary": "Withdraw a bid",
        "description": "Same caveat as place bid: the ownership check (`NOT_BID_OWNER`) is only as good as the `financier_org_id` the caller supplies, which is not independently verified against the authenticated caller's actual org membership.",
        "tags": [
          "Bidding"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WithdrawBidResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "bidID",
            "in": "path",
            "required": true,
            "description": "The bid id, e.g. `bid_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WithdrawBidRequest"
              }
            }
          }
        }
      }
    },
    "/v1/fee-schedules": {
      "post": {
        "operationId": "proposeFeeSchedule",
        "summary": "Propose a new fee schedule version (maker)",
        "description": "RUL-16 maker-checker. The new version is NOT effective until a different user calls the approve endpoint below. `day_count_basis` must be 365 or 360, and `fee_bearer_default` must be one of supplier/buyer/financier/split, or this fails with 400 `INVALID_SCHEDULE_RULES`.",
        "tags": [
          "Fees"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeeSchedule"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProposeScheduleRequest"
              }
            }
          }
        }
      }
    },
    "/v1/fee-schedules/effective": {
      "get": {
        "operationId": "getEffectiveFeeSchedule",
        "summary": "Get the fee schedule in effect",
        "description": "Per doc 08 \u00a79.3's anti-avoidance clause (\"a fee not in the schedule cannot be charged\"), there is no hardcoded fallback: a tenant with no approved schedule in effect gets a clean 404 `NO_APPROVED_SCHEDULE`, never a default rate.",
        "tags": [
          "Fees"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeeSchedule"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "RFC 3339 timestamp. Defaults to now if omitted.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ]
      }
    },
    "/v1/fee-schedules/{id}/approve": {
      "post": {
        "operationId": "approveFeeSchedule",
        "summary": "Approve a proposed fee schedule version (checker)",
        "description": "The checker half of RUL-16. Fails with 409 `SAME_MAKER_CHECKER` if the checker is the same user who proposed it, or 409 `ALREADY_APPROVED` if it was already approved.",
        "tags": [
          "Fees"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApproveScheduleResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The fee schedule id, e.g. `fee_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/fees/late-fee": {
      "post": {
        "operationId": "quoteLateFee",
        "summary": "Quote a late fee",
        "description": "RUL-12. Resolves the tenant's effective fee schedule as of `as_of` and returns zero if `days_late` is still within the schedule's grace period.",
        "tags": [
          "Fees"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LateFeeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LateFeeRequest"
              }
            }
          }
        }
      }
    },
    "/v1/fees/quote": {
      "post": {
        "operationId": "quoteFees",
        "summary": "Quote fees and net proceeds",
        "description": "Resolves the tenant's effective fee schedule as of `as_of` and computes the full discount/fee/VAT/net breakdown against it for a hypothetical face amount, rate and tenor. Fails with 404 `NO_APPROVED_SCHEDULE` if the tenant has no approved schedule in effect.",
        "tags": [
          "Fees"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Breakdown"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        }
      }
    },
    "/v1/fees/simulate": {
      "post": {
        "operationId": "simulateFeeSchedule",
        "summary": "Simulate a hypothetical fee schedule",
        "description": "OPS-04's \"simulate before publishing\" requirement: runs the same breakdown math as /v1/fees/quote against a draft, not-yet-persisted (and possibly not-yet-approved) set of rules passed inline in the request body, with no database round trip. `schedule_version` in the response is always 0. Must never be used to compute a real charge \u2014 only /v1/fees/quote (which resolves an actually approved schedule) may do that. Mounted behind the same auth middleware as every other endpoint for consistency, even though the handler itself never reads the caller's identity.",
        "tags": [
          "Fees"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Breakdown"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SimulateRequest"
              }
            }
          }
        }
      }
    },
    "/v1/invoices": {
      "post": {
        "operationId": "registerInvoice",
        "summary": "Register an invoice (Factoring Unit)",
        "description": "Computes a salted duplicate-financing hash from buyer/supplier registration numbers, invoice number, amount and dates; a match returns 409 `ALREADY_FINANCED` with deliberately no detail about which tenant or invoice holds the prior registration (doc 05: \"opaque references ... to limit what a leak of this table reveals\").",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceRegisterRequest"
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listInvoices",
        "summary": "List invoices",
        "description": "Filterable by status, supplier_org_id, buyer_org_id, programme_id; limit-only pagination (no offset, no cursor).",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "string",
              "enum": [
                "registered",
                "eligible",
                "open",
                "accepted",
                "funding",
                "funded",
                "outstanding",
                "matured",
                "settled",
                "disputed",
                "cancelled",
                "defaulted"
              ]
            }
          },
          {
            "name": "supplier_org_id",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "buyer_org_id",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "programme_id",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}": {
      "get": {
        "operationId": "getInvoice",
        "summary": "Get an invoice",
        "description": "",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/accept": {
      "post": {
        "operationId": "acceptInvoice",
        "summary": "Accept an invoice",
        "description": "Lifecycle transition: open -> accepted (internal/invoice's own lifecycle meaning of \"accept\" \u2014 distinct from bidding's accept-a-bid endpoint below, which creates a trade). Requires a step-up (re-authenticated) session \u2014 see `/v1/auth/step-up/require` and `/v1/auth/step-up/complete`.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/cancel": {
      "post": {
        "operationId": "cancelInvoice",
        "summary": "Cancel an invoice",
        "description": "Records an optional reason alongside the transition to cancelled.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReasonRequest"
              }
            }
          }
        }
      }
    },
    "/v1/invoices/{id}/default": {
      "post": {
        "operationId": "defaultInvoice",
        "summary": "Mark an invoice as defaulted",
        "description": "Records an optional reason alongside the transition to defaulted.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReasonRequest"
              }
            }
          }
        }
      }
    },
    "/v1/invoices/{id}/dispute": {
      "post": {
        "operationId": "disputeInvoice",
        "summary": "Dispute an invoice",
        "description": "Records an optional reason alongside the transition to disputed.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReasonRequest"
              }
            }
          }
        }
      }
    },
    "/v1/invoices/{id}/mark-eligible": {
      "post": {
        "operationId": "markInvoiceEligible",
        "summary": "Mark an invoice eligible",
        "description": "Lifecycle transition: registered -> eligible.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/mark-funded": {
      "post": {
        "operationId": "markInvoiceFunded",
        "summary": "Mark an invoice as funded",
        "description": "Lifecycle transition: funding -> funded.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/mark-funding": {
      "post": {
        "operationId": "markInvoiceFunding",
        "summary": "Mark an invoice as funding",
        "description": "Lifecycle transition: accepted -> funding.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/mark-matured": {
      "post": {
        "operationId": "markInvoiceMatured",
        "summary": "Mark an invoice as matured",
        "description": "Lifecycle transition: outstanding -> matured.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/mark-outstanding": {
      "post": {
        "operationId": "markInvoiceOutstanding",
        "summary": "Mark an invoice as outstanding",
        "description": "Lifecycle transition: funded -> outstanding.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/mark-settled": {
      "post": {
        "operationId": "markInvoiceSettled",
        "summary": "Mark an invoice as settled",
        "description": "Lifecycle transition: matured -> settled.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{id}/open": {
      "post": {
        "operationId": "openInvoice",
        "summary": "Open an invoice for bidding",
        "description": "Lifecycle transition: eligible -> open.",
        "tags": [
          "Invoices"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice (Factoring Unit) id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/invoices/{invoiceID}/bids/{bidID}/accept": {
      "post": {
        "operationId": "acceptBid",
        "summary": "Accept a bid (create a trade)",
        "description": "Mounted under /v1/invoices rather than as `/v1/invoices/{id}/accept` precisely to avoid colliding with invoice's own lifecycle accept endpoint above \u2014 a genuinely different operation (\"pick this bid, create a trade\" vs. invoice's \"registered -> open -> accepted\" status transition). Requires a step-up (re-authenticated) session and a mandatory `Idempotency-Key` header, since this creates a trade and must be safely retriable. CAVEAT: internal/fees is not wired into this endpoint yet, so the trade's financial figures (discount/fee/VAT/net proceeds) are taken directly from the request body rather than computed/verified server-side \u2014 see `AcceptBidRequest`. Always responds 201, including on an idempotent replay of an already-accepted bid.",
        "tags": [
          "Bidding"
        ],
        "responses": {
          "201": {
            "description": "Created (or: the idempotent-replay response for an already-accepted bid).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Trade"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "invoiceID",
            "in": "path",
            "required": true,
            "description": "The invoice id, e.g. `inv_01HZX...`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bidID",
            "in": "path",
            "required": true,
            "description": "The bid id, e.g. `bid_01HZX...`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on this endpoint because it creates a trade \u2014 a client retry (timeout, double click, two tabs) must be able to safely repeat the call. Not required on any other write endpoint in this API today, even though doc 06's general convention calls for it on every POST.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AcceptBidRequest"
              }
            }
          }
        }
      }
    },
    "/v1/order-book": {
      "get": {
        "operationId": "getOrderBook",
        "summary": "Get an invoice's live order book",
        "description": "Returns the invoice's open/outbid bids, best-rate-first. Already sorted by the engine (rate_bp ascending, then placed_at ascending) \u2014 not re-sorted by this endpoint.",
        "tags": [
          "Bidding"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderBookResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "invoice_id",
            "in": "query",
            "required": true,
            "description": "",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/participants": {
      "post": {
        "operationId": "registerParticipant",
        "summary": "Register a new participant organisation",
        "description": "Self-service onboarding (SUP-01/SUP-02/FIN-01-style). Runs before any approval, so this is recorded as a system action, not attributed to a specific authenticated actor.",
        "tags": [
          "Participants"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ParticipantRegisterRequest"
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listParticipants",
        "summary": "List participant organisations",
        "description": "Filterable by type, status, kyc_status; limit/offset paginated. `next_cursor` in the response is always null \u2014 the backing service is limit/offset-based, not keyset-based (documented honestly rather than inventing cursor semantics the service doesn't implement).",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filter by organisation type.",
            "schema": {
              "type": "string",
              "enum": [
                "supplier",
                "buyer",
                "financier",
                "developer",
                "settlement_bank",
                "custodian"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by lifecycle status.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "under_review",
                "active",
                "suspended",
                "offboarded",
                "rejected"
              ]
            }
          },
          {
            "name": "kyc_status",
            "in": "query",
            "required": false,
            "description": "Filter by KYC status.",
            "schema": {
              "type": "string",
              "enum": [
                "not_started",
                "pending",
                "approved",
                "rejected",
                "expired",
                "enhanced_review"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ]
      }
    },
    "/v1/participants/{id}": {
      "get": {
        "operationId": "getParticipant",
        "summary": "Get a participant organisation",
        "description": "Cross-tenant access to another tenant's organisation is reported as 404, never 403.",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/participants/{id}/confirm-approval": {
      "post": {
        "operationId": "participantConfirmApproval",
        "summary": "Confirm maker-checker approval (checker)",
        "description": "The checker half of RUL-16. Must be a different authenticated user than the maker who called request-approval, or this returns 403 `MAKER_CHECKER_VIOLATION` \u2014 deliberately 403, not 409: a different checker could confirm this exact same pending request right now with no state change needed, so this is an authorization fact about the caller, not a resource-state conflict.",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/participants/{id}/due-for-rescreen": {
      "get": {
        "operationId": "participantDueForRescreen",
        "summary": "Check whether a participant is due for periodic KYC re-screening",
        "description": "",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DueForRescreenResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/participants/{id}/kyc-checks": {
      "post": {
        "operationId": "participantRecordKYCChecks",
        "summary": "Record KYC check results",
        "description": "Every field is required and must be one of `pass`, `fail`, `review`.",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KYCChecksRequest"
              }
            }
          }
        }
      }
    },
    "/v1/participants/{id}/reinstate": {
      "post": {
        "operationId": "participantReinstate",
        "summary": "Reinstate a suspended participant",
        "description": "",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReasonRequest"
              }
            }
          }
        }
      }
    },
    "/v1/participants/{id}/reject": {
      "post": {
        "operationId": "participantReject",
        "summary": "Reject a participant",
        "description": "",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReasonRequest"
              }
            }
          }
        }
      }
    },
    "/v1/participants/{id}/request-approval": {
      "post": {
        "operationId": "participantRequestApproval",
        "summary": "Request maker-checker approval (maker)",
        "description": "The maker half of RUL-16's maker-checker gate. The maker is always the authenticated caller \u2014 never a client-supplied field, so the guarantee can't be forged.",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/participants/{id}/submit-for-review": {
      "post": {
        "operationId": "participantSubmitForReview",
        "summary": "Submit a participant for KYC review",
        "description": "Moves the organisation from `pending` into `under_review`. Returns the resulting organisation state directly, rather than an empty response, so the caller never needs a second round trip.",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/participants/{id}/suspend": {
      "post": {
        "operationId": "participantSuspend",
        "summary": "Suspend a participant",
        "description": "",
        "tags": [
          "Participants"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organisation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The organisation id, e.g. `org_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReasonRequest"
              }
            }
          }
        }
      }
    },
    "/v1/programmes": {
      "post": {
        "operationId": "createProgramme",
        "summary": "Create a buyer programme",
        "description": "BUY-01. Created in `draft` status. See CreateProgrammeRequest for the documented defaults applied to every optional field.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Programme"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProgrammeRequest"
              }
            }
          }
        }
      }
    },
    "/v1/programmes/memberships/{membershipId}/max-rate": {
      "patch": {
        "operationId": "updateMembershipMaxRate",
        "summary": "Update a membership's rate ceiling",
        "description": "BUY-20/SUP-20. Unlike opt-in's max_rate_bp, this field is required (plain int, not a pointer) \u2014 there is no operation exposed to clear the ceiling back to manual-only, only to set it to a new value. Returns 204.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "204": {
            "description": "Success. No response body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "membershipId",
            "in": "path",
            "required": true,
            "description": "The membership id, e.g. `mem_01HZX...`. Membership ids are globally unique, not scoped by path to their parent programme.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateMaxRateRequest"
              }
            }
          }
        }
      }
    },
    "/v1/programmes/memberships/{membershipId}/opt-in": {
      "post": {
        "operationId": "optInMembership",
        "summary": "Opt a supplier membership in",
        "description": "The request body is entirely optional \u2014 omit it (or omit max_rate_bp within it) for manual-only opt-in (no auto-allocation).",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Membership"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "membershipId",
            "in": "path",
            "required": true,
            "description": "The membership id, e.g. `mem_01HZX...`. Membership ids are globally unique, not scoped by path to their parent programme.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OptInRequest"
              }
            }
          }
        }
      }
    },
    "/v1/programmes/memberships/{membershipId}/opt-out": {
      "post": {
        "operationId": "optOutMembership",
        "summary": "Opt a supplier membership out",
        "description": "RUL-14. Returns 204. Fails with 409 `OUTSTANDING_INVOICES_EXIST` if the supplier still has outstanding invoices under this programme.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "204": {
            "description": "Success. No response body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "membershipId",
            "in": "path",
            "required": true,
            "description": "The membership id, e.g. `mem_01HZX...`. Membership ids are globally unique, not scoped by path to their parent programme.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/programmes/{id}/activate": {
      "post": {
        "operationId": "activateProgramme",
        "summary": "Activate a programme",
        "description": "Returns 204 \u2014 the backing service returns only an error, not the updated row, and there is no Get-by-id in this package to re-fetch one.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "204": {
            "description": "Success. No response body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The programme id, e.g. `prg_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/programmes/{id}/approve-limit-change": {
      "post": {
        "operationId": "approveLimitChange",
        "summary": "Approve a programme limit change (checker)",
        "description": "The checker half of RUL-16. Returns 204. Note: the programme to update is resolved from `proposal_id` in the body, not from the `{id}` path parameter \u2014 the path segment exists for REST consistency and so the URL names the programme the caller believes it is acting on, but it is not threaded into the underlying service call (tenant isolation is still enforced by row-level security regardless). Fails with 403 `SAME_MAKER_CHECKER` if the checker is the same user who proposed the change.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "204": {
            "description": "Success. No response body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The programme id, e.g. `prg_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApproveLimitChangeRequest"
              }
            }
          }
        }
      }
    },
    "/v1/programmes/{id}/close": {
      "post": {
        "operationId": "closeProgramme",
        "summary": "Close a programme",
        "description": "Returns 204, for the same reason as activate. Fails with 409 `OUTSTANDING_INVOICES_EXIST` if the programme still has outstanding invoices.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "204": {
            "description": "Success. No response body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The programme id, e.g. `prg_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/programmes/{id}/eligibility": {
      "get": {
        "operationId": "checkProgrammeEligibility",
        "summary": "Check supplier/invoice eligibility against a programme",
        "description": "SUP-05. A GET with no body \u2014 every input is a query parameter.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EligibilityResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The programme id, e.g. `prg_01HZX...`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "supplier_org_id",
            "in": "query",
            "required": true,
            "description": "",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount_minor",
            "in": "query",
            "required": true,
            "description": "",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "tenor_days",
            "in": "query",
            "required": true,
            "description": "",
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/v1/programmes/{id}/memberships": {
      "post": {
        "operationId": "inviteMembership",
        "summary": "Invite a supplier to a programme",
        "description": "",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Membership"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The programme id, e.g. `prg_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteRequest"
              }
            }
          }
        }
      }
    },
    "/v1/programmes/{id}/propose-limit-change": {
      "post": {
        "operationId": "proposeLimitChange",
        "summary": "Propose a programme limit change (maker)",
        "description": "RUL-16 maker-checker. The maker is always the authenticated caller, never a body field.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProposeLimitChangeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The programme id, e.g. `prg_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProposeLimitChangeRequest"
              }
            }
          }
        }
      }
    },
    "/v1/programmes/{id}/suspend": {
      "post": {
        "operationId": "suspendProgramme",
        "summary": "Suspend a programme",
        "description": "Returns 204, for the same reason as activate.",
        "tags": [
          "Programmes"
        ],
        "responses": {
          "204": {
            "description": "Success. No response body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The programme id, e.g. `prg_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/settlement-instructions/{id}": {
      "get": {
        "operationId": "getSettlementInstruction",
        "summary": "Get a settlement instruction",
        "description": "",
        "tags": [
          "Settlement"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementInstruction"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The settlement instruction id, e.g. `stl_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/settlement-instructions/{id}/confirm-funding": {
      "post": {
        "operationId": "confirmFunding",
        "summary": "Confirm a funding instruction (bank callback)",
        "description": "Records the settlement bank's own asynchronous confirmation of an instruction that was already sent (ISO 20022 pain.002/camt.054 flow). Not step-up gated \u2014 it acknowledges a transfer that already happened, it doesn't newly authorise one. Fails with 409 `INVALID_INSTRUCTION_STATE` if the instruction isn't in a confirmable state (e.g. it already failed to send).",
        "tags": [
          "Settlement"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementInstruction"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The settlement instruction id, e.g. `stl_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmFundingRequest"
              }
            }
          }
        }
      }
    },
    "/v1/trades/{tradeId}/funding": {
      "post": {
        "operationId": "instructFunding",
        "summary": "Instruct funding for a trade",
        "description": "Requires a step-up session \u2014 the first money movement of a trade's lifecycle, judged to deserve the same gate as accepting a bid even though doc 03/05 don't spell this particular endpoint out verbatim (see the handler's own doc comment for the reasoning). The caller must compose `bank_cutoff` from the tenant's own configuration onto today's date in the tenant's zone; this package has no tenant-config dependency of its own to do that itself. Always responds 201 once the ledger posting and 'pending' instruction row have committed, even if the subsequent outbound rail send then fails \u2014 a bank rejection is a normal outcome reflected in the returned resource's `status`/`failure_reason`, not a client error.",
        "tags": [
          "Settlement"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementInstruction"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tradeId",
            "in": "path",
            "required": true,
            "description": "The trade id, e.g. `trd_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InstructFundingRequest"
              }
            }
          }
        }
      }
    },
    "/v1/trades/{tradeId}/maturity": {
      "post": {
        "operationId": "distributeMaturity",
        "summary": "Distribute a trade's maturity waterfall",
        "description": "Requires a step-up session \u2014 moves real money through the five-step waterfall (financier principal, financier discount, exchange fee, late fee, supplier residual) to multiple counterparties in one call. `to_account_tokens` keys must be one of the five waterfall step names. Always responds 201 once the waterfall's ledger journal and every settlement_instruction row have committed, even if a per-step rail send then fails (reflected per-instruction in the response, not as an API error).",
        "tags": [
          "Settlement"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementInstructionListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tradeId",
            "in": "path",
            "required": true,
            "description": "The trade id, e.g. `trd_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DistributeMaturityRequest"
              }
            }
          }
        }
      }
    },
    "/v1/trades/{tradeId}/settlement-instructions": {
      "get": {
        "operationId": "listTradeSettlementInstructions",
        "summary": "List settlement instructions for a trade",
        "description": "",
        "tags": [
          "Settlement"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementInstructionListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tradeId",
            "in": "path",
            "required": true,
            "description": "The trade id, e.g. `trd_01HZX...`.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A session token minted by a passkey or TOTP login endpoint (or by step-up completion, for step-up-gated endpoints). Sent as `Authorization: Bearer <token>`. There is no API-key or OAuth client-credentials scheme implemented today."
      }
    },
    "schemas": {
      "FieldError": {
        "type": "object",
        "required": [
          "field",
          "detail"
        ],
        "properties": {
          "field": {
            "type": "string",
            "description": "The request field this error applies to (JSON field name, dot-path for nested keys e.g. `to_account_tokens.financier_principal`)."
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem+json error body. Every non-2xx response from every endpoint in this API uses this shape, served with `Content-Type: application/problem+json`. This mirrors `internal/platform/httpx.Problem` field-for-field.",
        "required": [
          "title",
          "status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A URI identifying the problem type. Not currently populated by any handler (always omitted on the wire today)."
          },
          "title": {
            "type": "string",
            "description": "A short, human-readable summary of the problem type, e.g. \"Not Found\", \"Conflict\"."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code, repeated in the body per RFC 9457."
          },
          "detail": {
            "type": "string",
            "description": "A human-readable explanation specific to this occurrence. Deliberately withheld (omitted) for 500s to avoid leaking internals."
          },
          "code": {
            "type": "string",
            "description": "A stable, machine-readable code, e.g. `LIMIT_EXCEEDED`, `INVOICE_NOT_FOUND`, `MAKER_CHECKER_VIOLATION`. See each operation's documented error responses for the codes it can return."
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldError"
            },
            "description": "Field-level validation errors. Present only on 422 VALIDATION_FAILED responses."
          },
          "meta": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Reserved for problem-specific structured detail. Not currently populated by any handler."
          }
        }
      },
      "Money": {
        "type": "object",
        "description": "An exact amount: integer minor units (e.g. cents) plus an ISO 4217 currency code. Never a float, anywhere in this API.",
        "required": [
          "amount_minor",
          "currency"
        ],
        "properties": {
          "amount_minor": {
            "type": "integer",
            "format": "int64",
            "example": 12500000
          },
          "currency": {
            "type": "string",
            "example": "KES",
            "description": "ISO 4217 currency code."
          }
        }
      },
      "Session": {
        "type": "object",
        "description": "A bearer session token, returned by every endpoint that mints or refreshes one (passkey login finish, TOTP login, step-up complete).",
        "required": [
          "token",
          "expires_at"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Opaque bearer token. Send as `Authorization: Bearer <token>` on every subsequent authenticated request."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PasskeyRegisterBeginRequest": {
        "type": "object",
        "required": [
          "user_id"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          }
        }
      },
      "PasskeyRegisterBeginResponse": {
        "type": "object",
        "required": [
          "session_key",
          "options"
        ],
        "properties": {
          "session_key": {
            "type": "string",
            "description": "Opaque, single-use key. Echo it back unmodified on the matching `/finish` call."
          },
          "options": {
            "type": "object",
            "description": "A WebAuthn `PublicKeyCredentialCreationOptions` object (go-webauthn's `protocol.CredentialCreation`), passed directly to the browser's `navigator.credentials.create()`."
          }
        }
      },
      "PasskeyRegisterFinishRequest": {
        "type": "object",
        "required": [
          "user_id",
          "session_key",
          "credential"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "session_key": {
            "type": "string"
          },
          "credential": {
            "type": "object",
            "description": "The raw JSON response from `navigator.credentials.create()`, passed through unmodified."
          }
        }
      },
      "PasskeyRegisterFinishResponse": {
        "type": "object",
        "required": [
          "registered"
        ],
        "properties": {
          "registered": {
            "type": "boolean"
          }
        }
      },
      "PasskeyLoginBeginRequest": {
        "type": "object",
        "required": [
          "user_id"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          }
        }
      },
      "PasskeyLoginBeginResponse": {
        "type": "object",
        "required": [
          "session_key",
          "options"
        ],
        "properties": {
          "session_key": {
            "type": "string"
          },
          "options": {
            "type": "object",
            "description": "A WebAuthn `PublicKeyCredentialRequestOptions` object (go-webauthn's `protocol.CredentialAssertion`), passed directly to the browser's `navigator.credentials.get()`."
          }
        }
      },
      "PasskeyLoginFinishRequest": {
        "type": "object",
        "required": [
          "user_id",
          "session_key",
          "credential"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "session_key": {
            "type": "string"
          },
          "credential": {
            "type": "object",
            "description": "The raw JSON response from `navigator.credentials.get()`, passed through unmodified."
          }
        }
      },
      "TOTPVerifyLoginRequest": {
        "type": "object",
        "required": [
          "user_id",
          "code"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "6-digit TOTP code."
          }
        }
      },
      "DevLoginRequest": {
        "type": "object",
        "required": [
          "tenant_id",
          "user_id"
        ],
        "properties": {
          "tenant_id": {
            "type": "string"
          },
          "user_id": {
            "type": "string"
          }
        }
      },
      "DevLoginResponse": {
        "type": "object",
        "required": [
          "token",
          "expires_at",
          "warning"
        ],
        "properties": {
          "token": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "warning": {
            "type": "string"
          }
        }
      },
      "TOTPEnrollResponse": {
        "type": "object",
        "required": [
          "secret",
          "otpauth_url"
        ],
        "properties": {
          "secret": {
            "type": "string",
            "description": "Base32-encoded TOTP secret (no padding)."
          },
          "otpauth_url": {
            "type": "string",
            "description": "An `otpauth://totp/...` URI suitable for rendering as a QR code in an authenticator app."
          }
        }
      },
      "TOTPConfirmRequest": {
        "type": "object",
        "required": [
          "code"
        ],
        "properties": {
          "code": {
            "type": "string"
          }
        }
      },
      "TOTPConfirmResponse": {
        "type": "object",
        "required": [
          "confirmed"
        ],
        "properties": {
          "confirmed": {
            "type": "boolean"
          }
        }
      },
      "StepUpRequireResponse": {
        "type": "object",
        "required": [
          "challenge_id"
        ],
        "properties": {
          "challenge_id": {
            "type": "string"
          }
        }
      },
      "StepUpCompleteRequest": {
        "type": "object",
        "required": [
          "challenge_id",
          "code"
        ],
        "properties": {
          "challenge_id": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "A TOTP code re-verifying the caller's identity. TOTP is the only step-up re-verification method implemented today."
          }
        }
      },
      "ParticipantRegisterRequest": {
        "type": "object",
        "required": [
          "type",
          "legal_name"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "supplier",
              "buyer",
              "financier",
              "developer",
              "settlement_bank",
              "custodian"
            ]
          },
          "legal_name": {
            "type": "string"
          },
          "trading_name": {
            "type": "string"
          },
          "registration_no": {
            "type": "string"
          },
          "tax_id": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "bank_account": {
            "type": "string",
            "description": "Accepted as plaintext over TLS; sealed (encrypted at rest) by the service before it ever reaches a SQL statement."
          },
          "contact_phone": {
            "type": "string"
          },
          "contact_email": {
            "type": "string"
          },
          "director_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Organisation": {
        "type": "object",
        "description": "The non-sensitive public shape of a participant organisation. Bank account, contact and director fields are deliberately never returned here.",
        "required": [
          "id",
          "tenant_id",
          "type",
          "legal_name",
          "country",
          "status",
          "kyc_status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "org_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "tenant_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "supplier",
              "buyer",
              "financier",
              "developer",
              "settlement_bank",
              "custodian"
            ]
          },
          "legal_name": {
            "type": "string"
          },
          "trading_name": {
            "type": "string"
          },
          "registration_no": {
            "type": "string"
          },
          "tax_id": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "under_review",
              "active",
              "suspended",
              "offboarded",
              "rejected"
            ]
          },
          "kyc_status": {
            "type": "string",
            "enum": [
              "not_started",
              "pending",
              "approved",
              "rejected",
              "expired",
              "enhanced_review"
            ]
          },
          "risk_rating": {
            "type": [
              "integer",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ParticipantListResponse": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Organisation"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Always null today \u2014 the backing service is limit/offset-based, not keyset/cursor-based, so there is no cursor token to return. Documented honestly rather than inventing one."
          }
        }
      },
      "KYCChecksRequest": {
        "type": "object",
        "required": [
          "registry_match",
          "tax_pin_valid",
          "liveness_check",
          "sanctions_pep_screen",
          "bank_name_match"
        ],
        "properties": {
          "registry_match": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "review"
            ]
          },
          "tax_pin_valid": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "review"
            ]
          },
          "liveness_check": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "review"
            ]
          },
          "sanctions_pep_screen": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "review"
            ]
          },
          "bank_name_match": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "review"
            ]
          }
        }
      },
      "ReasonRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "description": "Optional free-text reason. An empty/absent reason is legal."
          }
        }
      },
      "DueForRescreenResponse": {
        "type": "object",
        "required": [
          "due"
        ],
        "properties": {
          "due": {
            "type": "boolean"
          }
        }
      },
      "CreateProgrammeRequest": {
        "type": "object",
        "required": [
          "buyer_org_id",
          "name",
          "currency",
          "limit_minor"
        ],
        "properties": {
          "buyer_org_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency code."
          },
          "limit_minor": {
            "type": "integer",
            "format": "int64",
            "description": "The programme-wide exposure limit. Must be > 0 (RUL-03)."
          },
          "supplier_limit_pct_milli": {
            "type": "integer",
            "format": "int64",
            "description": "Per-supplier concentration limit as milli-percent of the programme limit (RUL-04). Default 20000 (20%) if omitted. Must be > 0."
          },
          "min_tenor_days": {
            "type": "integer",
            "description": "Default 7 if omitted."
          },
          "max_tenor_days": {
            "type": "integer",
            "description": "Default 180 if omitted. Must be >= min_tenor_days (RUL-02)."
          },
          "min_amount_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Default 0 if omitted."
          },
          "max_amount_minor": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Unbounded if omitted. Must be >= min_amount_minor if set."
          },
          "fee_bearer": {
            "type": "string",
            "enum": [
              "supplier",
              "buyer",
              "financier",
              "split"
            ],
            "description": "Who the platform fee is charged to by default (RUL-10). Default \"supplier\" if omitted."
          },
          "financier_access": {
            "type": "string",
            "enum": [
              "open",
              "whitelist"
            ],
            "description": "Default \"open\" if omitted."
          },
          "bidding_window_min_hours": {
            "type": "integer",
            "description": "RUL-06. Default 2 if omitted. Must be > 0."
          },
          "bidding_window_max_days": {
            "type": "integer",
            "description": "RUL-06. Default 5 if omitted. Must be > 0."
          },
          "best_rate_auto_allocate": {
            "type": "boolean",
            "description": "RUL-09. Default false if omitted."
          }
        }
      },
      "Programme": {
        "type": "object",
        "required": [
          "id",
          "buyer_org_id",
          "name",
          "currency",
          "limit_minor",
          "supplier_limit_pct_milli",
          "min_tenor_days",
          "max_tenor_days",
          "min_amount_minor",
          "fee_bearer",
          "financier_access",
          "bidding_window_min_hours",
          "bidding_window_max_days",
          "best_rate_auto_allocate",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "prg_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "buyer_org_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "limit_minor": {
            "type": "integer",
            "format": "int64"
          },
          "supplier_limit_pct_milli": {
            "type": "integer",
            "format": "int64"
          },
          "min_tenor_days": {
            "type": "integer"
          },
          "max_tenor_days": {
            "type": "integer"
          },
          "min_amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "max_amount_minor": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "fee_bearer": {
            "type": "string",
            "enum": [
              "supplier",
              "buyer",
              "financier",
              "split"
            ]
          },
          "financier_access": {
            "type": "string",
            "enum": [
              "open",
              "whitelist"
            ]
          },
          "bidding_window_min_hours": {
            "type": "integer"
          },
          "bidding_window_max_days": {
            "type": "integer"
          },
          "best_rate_auto_allocate": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "description": "e.g. draft, active, suspended, closed."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProposeLimitChangeRequest": {
        "type": "object",
        "required": [
          "new_limit_minor"
        ],
        "properties": {
          "new_limit_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Must be > 0 (RUL-03)."
          }
        }
      },
      "ProposeLimitChangeResponse": {
        "type": "object",
        "required": [
          "proposal_id"
        ],
        "properties": {
          "proposal_id": {
            "type": "string"
          }
        }
      },
      "ApproveLimitChangeRequest": {
        "type": "object",
        "required": [
          "proposal_id"
        ],
        "properties": {
          "proposal_id": {
            "type": "string",
            "description": "The id returned by propose-limit-change. The programme is resolved from this id, not from the `{id}` path parameter (kept for REST-path consistency only)."
          }
        }
      },
      "InviteRequest": {
        "type": "object",
        "required": [
          "supplier_org_id"
        ],
        "properties": {
          "supplier_org_id": {
            "type": "string"
          }
        }
      },
      "Membership": {
        "type": "object",
        "required": [
          "id",
          "programme_id",
          "supplier_org_id",
          "status",
          "consent_version",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "mem_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "programme_id": {
            "type": "string"
          },
          "supplier_org_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "e.g. invited, opted_in, opted_out."
          },
          "max_rate_bp": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The supplier's rate ceiling, in basis points. Null means manual-only (no auto-allocation)."
          },
          "erp_vendor_ref": {
            "type": [
              "string",
              "null"
            ]
          },
          "opted_in_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "opted_out_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "consent_version": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OptInRequest": {
        "type": "object",
        "properties": {
          "max_rate_bp": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Optional rate ceiling, in basis points. Omit entirely for manual-only opt-in (the request body itself may be omitted)."
          }
        }
      },
      "UpdateMaxRateRequest": {
        "type": "object",
        "required": [
          "max_rate_bp"
        ],
        "properties": {
          "max_rate_bp": {
            "type": "integer",
            "description": "There is no way to clear the ceiling back to manual-only via this endpoint \u2014 only to set it to a new value."
          }
        }
      },
      "EligibilityResponse": {
        "type": "object",
        "required": [
          "eligible",
          "reasons"
        ],
        "properties": {
          "eligible": {
            "type": "boolean"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Human-readable reason codes explaining an ineligible result. Always `[]`, never `null`, when eligible."
          }
        }
      },
      "InvoiceRegisterRequest": {
        "type": "object",
        "required": [
          "supplier_org_id",
          "buyer_org_id",
          "buyer_reg_no",
          "supplier_reg_no",
          "invoice_no",
          "amount_minor",
          "currency",
          "issue_date",
          "due_date",
          "mode"
        ],
        "properties": {
          "programme_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "supplier_org_id": {
            "type": "string"
          },
          "buyer_org_id": {
            "type": "string"
          },
          "external_ref": {
            "type": "string"
          },
          "amount_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Must be positive."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency code."
          },
          "issue_date": {
            "type": "string",
            "format": "date",
            "description": "YYYY-MM-DD."
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "description": "YYYY-MM-DD. Must not be before issue_date."
          },
          "mode": {
            "type": "string",
            "enum": [
              "factoring",
              "reverse_factoring"
            ]
          },
          "erp_signature": {
            "type": "string"
          },
          "buyer_reg_no": {
            "type": "string",
            "description": "Used, together with supplier_reg_no/invoice_no/amount/dates, to compute the duplicate-financing hash \u2014 see `ALREADY_FINANCED`."
          },
          "supplier_reg_no": {
            "type": "string"
          },
          "invoice_no": {
            "type": "string"
          }
        }
      },
      "Invoice": {
        "type": "object",
        "description": "The Factoring Unit. `accepted_at`/`external_ref`/`erp_signature`/`content_hash`/`programme_id` are omitted from the JSON body (not merely null) when not applicable.",
        "required": [
          "id",
          "tenant_id",
          "supplier_org_id",
          "buyer_org_id",
          "amount_minor",
          "currency",
          "issue_date",
          "due_date",
          "tenor_days",
          "status",
          "mode",
          "registered_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "inv_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "tenant_id": {
            "type": "string"
          },
          "programme_id": {
            "type": "string"
          },
          "supplier_org_id": {
            "type": "string"
          },
          "buyer_org_id": {
            "type": "string"
          },
          "external_ref": {
            "type": "string"
          },
          "amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "issue_date": {
            "type": "string",
            "format": "date"
          },
          "due_date": {
            "type": "string",
            "format": "date"
          },
          "tenor_days": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "registered",
              "eligible",
              "open",
              "accepted",
              "funding",
              "funded",
              "outstanding",
              "matured",
              "settled",
              "disputed",
              "cancelled",
              "defaulted"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "factoring",
              "reverse_factoring"
            ]
          },
          "erp_signature": {
            "type": "string"
          },
          "content_hash": {
            "type": "string",
            "description": "`sha3-256:<hex>`, present once the content hash has been computed."
          },
          "registered_at": {
            "type": "string",
            "format": "date-time"
          },
          "accepted_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InvoiceListResponse": {
        "type": "object",
        "required": [
          "invoices"
        ],
        "properties": {
          "invoices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            }
          }
        }
      },
      "PlaceBidRequest": {
        "type": "object",
        "required": [
          "invoice_id",
          "financier_org_id",
          "rate_bp",
          "amount_minor",
          "currency"
        ],
        "properties": {
          "invoice_id": {
            "type": "string"
          },
          "financier_org_id": {
            "type": "string",
            "description": "Caveat: taken directly from the request body \u2014 there is no membership lookup wired in yet to verify the authenticated caller actually belongs to this financier org. See the endpoint description."
          },
          "rate_bp": {
            "type": "integer",
            "description": "Must be a positive number of basis points."
          },
          "amount_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Must be positive."
          },
          "currency": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Bid": {
        "type": "object",
        "required": [
          "id",
          "invoice_id",
          "financier_org_id",
          "rate_bp",
          "amount_minor",
          "currency",
          "status",
          "placed_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "bid_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "invoice_id": {
            "type": "string"
          },
          "financier_org_id": {
            "type": "string"
          },
          "rate_bp": {
            "type": "integer"
          },
          "amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "outbid",
              "accepted",
              "rejected",
              "expired",
              "withdrawn"
            ]
          },
          "placed_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "withdrawn_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrderBookResponse": {
        "type": "object",
        "required": [
          "invoice_id",
          "bids"
        ],
        "properties": {
          "invoice_id": {
            "type": "string"
          },
          "bids": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Bid"
            },
            "description": "Live (open/outbid) bids, best-rate-first (rate_bp ascending, then placed_at ascending)."
          }
        }
      },
      "WithdrawBidRequest": {
        "type": "object",
        "required": [
          "financier_org_id"
        ],
        "properties": {
          "financier_org_id": {
            "type": "string"
          }
        }
      },
      "WithdrawBidResponse": {
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "outbid",
              "accepted",
              "rejected",
              "expired",
              "withdrawn"
            ]
          }
        }
      },
      "AcceptBidRequest": {
        "type": "object",
        "required": [
          "currency",
          "face_amount_minor",
          "net_proceeds_minor",
          "funding_date",
          "maturity_date"
        ],
        "properties": {
          "face_amount_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Must be positive."
          },
          "discount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "fee_minor": {
            "type": "integer",
            "format": "int64"
          },
          "vat_minor": {
            "type": "integer",
            "format": "int64"
          },
          "net_proceeds_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Must be positive."
          },
          "currency": {
            "type": "string"
          },
          "funding_date": {
            "type": "string",
            "format": "date-time"
          },
          "maturity_date": {
            "type": "string",
            "format": "date-time"
          }
        },
        "description": "CAVEAT: internal/fees is not wired into this endpoint yet \u2014 these financial figures are taken directly from the request body, not computed/verified server-side. A real deployment must not trust a client-supplied discount/fee/VAT/net-proceeds for an actual trade."
      },
      "Trade": {
        "type": "object",
        "required": [
          "id",
          "invoice_id",
          "bid_id",
          "accepted_at",
          "accepted_by",
          "funding_date",
          "maturity_date",
          "face_amount_minor",
          "discount_minor",
          "fee_minor",
          "vat_minor",
          "net_proceeds_minor",
          "currency",
          "rate_bp",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "trd_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "invoice_id": {
            "type": "string"
          },
          "bid_id": {
            "type": "string"
          },
          "accepted_at": {
            "type": "string",
            "format": "date-time"
          },
          "accepted_by": {
            "type": "string",
            "description": "The authenticated user id of the step-up-verified caller who accepted the bid."
          },
          "funding_date": {
            "type": "string",
            "format": "date-time"
          },
          "maturity_date": {
            "type": "string",
            "format": "date-time"
          },
          "face_amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "discount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "fee_minor": {
            "type": "integer",
            "format": "int64"
          },
          "vat_minor": {
            "type": "integer",
            "format": "int64"
          },
          "net_proceeds_minor": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "rate_bp": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SettlementInstruction": {
        "type": "object",
        "required": [
          "id",
          "tenant_id",
          "trade_id",
          "type",
          "amount_minor",
          "currency",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "stl_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "tenant_id": {
            "type": "string"
          },
          "trade_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "funding",
              "maturity",
              "fee",
              "residual"
            ]
          },
          "from_account_token": {
            "type": "string"
          },
          "to_account_token": {
            "type": "string"
          },
          "amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "sent",
              "confirmed",
              "failed"
            ]
          },
          "bank_ref": {
            "type": "string"
          },
          "iso20022_message_id": {
            "type": "string"
          },
          "sent_at": {
            "type": "string",
            "format": "date-time"
          },
          "confirmed_at": {
            "type": "string",
            "format": "date-time"
          },
          "failure_reason": {
            "type": "string",
            "description": "Set when status is `failed` \u2014 a real, expected-to-happen-occasionally bank rejection. Not an API error: the instruction resource still exists and this field explains why."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SettlementInstructionListResponse": {
        "type": "object",
        "required": [
          "settlement_instructions"
        ],
        "properties": {
          "settlement_instructions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SettlementInstruction"
            }
          }
        }
      },
      "InstructFundingRequest": {
        "type": "object",
        "required": [
          "financier_org_id",
          "supplier_org_id",
          "net_proceeds_minor",
          "currency",
          "bank_cutoff"
        ],
        "properties": {
          "financier_org_id": {
            "type": "string"
          },
          "supplier_org_id": {
            "type": "string"
          },
          "net_proceeds_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Must be a positive integer."
          },
          "currency": {
            "type": "string"
          },
          "from_account_token": {
            "type": "string"
          },
          "to_account_token": {
            "type": "string"
          },
          "bank_cutoff": {
            "type": "string",
            "format": "date-time",
            "description": "The caller must compose this from the tenant's own bank_cutoff config onto today's date in the tenant's zone \u2014 this endpoint has no tenant-config dependency of its own."
          },
          "now": {
            "type": "string",
            "format": "date-time",
            "description": "Optional; defaults to the real wall clock. Exists for callers (tests, backfill/replay tools) that need to pin \"the current time\"."
          }
        }
      },
      "ConfirmFundingRequest": {
        "type": "object",
        "required": [
          "bank_ref"
        ],
        "properties": {
          "bank_ref": {
            "type": "string",
            "description": "The bank's own reference for its asynchronous confirmation of an instruction that was already sent (ISO 20022 pain.002/camt.054 flow)."
          }
        }
      },
      "DistributeMaturityRequest": {
        "type": "object",
        "required": [
          "currency",
          "financier_org_id",
          "supplier_org_id"
        ],
        "properties": {
          "currency": {
            "type": "string"
          },
          "amount_received_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Must not be negative."
          },
          "principal_due_minor": {
            "type": "integer",
            "format": "int64"
          },
          "discount_due_minor": {
            "type": "integer",
            "format": "int64"
          },
          "platform_fee_due_minor": {
            "type": "integer",
            "format": "int64"
          },
          "residual_due_minor": {
            "type": "integer",
            "format": "int64",
            "description": "Informational only \u2014 does not cap what the residual waterfall step actually posts."
          },
          "late_fee_base_minor": {
            "type": "integer",
            "format": "int64"
          },
          "late_fee_rate_bp": {
            "type": "integer",
            "format": "int64"
          },
          "days_late": {
            "type": "integer",
            "format": "int64"
          },
          "grace_days": {
            "type": "integer",
            "format": "int64"
          },
          "day_count_basis": {
            "type": "integer",
            "format": "int64"
          },
          "financier_org_id": {
            "type": "string"
          },
          "supplier_org_id": {
            "type": "string"
          },
          "late_fee_beneficiary_org_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "from_account_token": {
            "type": "string"
          },
          "to_account_tokens": {
            "type": "object",
            "description": "Maps a waterfall step name to the destination account token for that step.",
            "additionalProperties": {
              "type": "string"
            },
            "properties": {
              "financier_principal": {
                "type": "string"
              },
              "financier_discount": {
                "type": "string"
              },
              "exchange_fee": {
                "type": "string"
              },
              "late_fee": {
                "type": "string"
              },
              "supplier_residual": {
                "type": "string"
              }
            }
          },
          "now": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ScheduleRules": {
        "type": "object",
        "required": [
          "platform_fee_pct_milli",
          "vat_pct_milli",
          "late_fee_pct_milli",
          "grace_period_days",
          "day_count_basis",
          "fee_bearer_default"
        ],
        "properties": {
          "platform_fee_pct_milli": {
            "type": "integer",
            "format": "int64",
            "description": "Platform fee as milli-percent of the invoice face amount, e.g. 500 = 0.50%."
          },
          "vat_pct_milli": {
            "type": "integer",
            "format": "int64",
            "description": "VAT as milli-percent, applied to the platform fee amount (not the face amount), e.g. 16000 = 16%."
          },
          "late_fee_pct_milli": {
            "type": "integer",
            "format": "int64",
            "description": "Flat late fee, as milli-percent of the overdue amount, once a payment is more than grace_period_days late."
          },
          "grace_period_days": {
            "type": "integer",
            "description": "Must not be negative."
          },
          "day_count_basis": {
            "type": "integer",
            "format": "int64",
            "description": "B in the discount formula D = A x r x (t/B). Must be 365 or 360."
          },
          "fee_bearer_default": {
            "type": "string",
            "enum": [
              "supplier",
              "buyer",
              "financier",
              "split"
            ],
            "description": "Who the platform fee is charged to by default (RUL-10)."
          }
        }
      },
      "ProposeScheduleRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ScheduleRules"
          },
          {
            "type": "object",
            "required": [
              "effective_from"
            ],
            "properties": {
              "effective_from": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "FeeSchedule": {
        "type": "object",
        "required": [
          "id",
          "version",
          "effective_from",
          "rules",
          "proposed_by",
          "created_at",
          "approved"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "fee_01HZX8K2Q7N4M3V9R6T1W5Y8Z0"
          },
          "version": {
            "type": "integer"
          },
          "effective_from": {
            "type": "string",
            "format": "date-time"
          },
          "rules": {
            "$ref": "#/components/schemas/ScheduleRules"
          },
          "proposed_by": {
            "type": "string",
            "description": "The maker who proposed this schedule version. Named `proposed_by` on the wire (not `approved_by`, which is the underlying \u2014 confusingly named \u2014 domain field) so API clients aren't misled into thinking this is the checker."
          },
          "checked_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "The checker who approved this version; null while the schedule is still pending approval."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "approved": {
            "type": "boolean",
            "description": "True once checked_by is set (RUL-16 maker-checker gate passed)."
          }
        }
      },
      "ApproveScheduleResponse": {
        "type": "object",
        "required": [
          "id",
          "checked_by",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "checked_by": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "example": "approved"
          }
        }
      },
      "Breakdown": {
        "type": "object",
        "required": [
          "face",
          "discount",
          "fee",
          "vat",
          "net",
          "rate_bp",
          "tenor_days",
          "schedule_version"
        ],
        "properties": {
          "face": {
            "$ref": "#/components/schemas/Money"
          },
          "discount": {
            "$ref": "#/components/schemas/Money"
          },
          "fee": {
            "$ref": "#/components/schemas/Money"
          },
          "vat": {
            "$ref": "#/components/schemas/Money"
          },
          "net": {
            "$ref": "#/components/schemas/Money"
          },
          "rate_bp": {
            "type": "integer",
            "format": "int64"
          },
          "tenor_days": {
            "type": "integer",
            "format": "int64"
          },
          "schedule_version": {
            "type": "integer",
            "description": "0 for a breakdown produced by /v1/fees/simulate (an unversioned, hypothetical schedule)."
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "face_amount_minor",
          "currency",
          "rate_bp",
          "tenor_days"
        ],
        "properties": {
          "face_amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "rate_bp": {
            "type": "integer",
            "format": "int64"
          },
          "tenor_days": {
            "type": "integer",
            "format": "int64"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Defaults to now if omitted."
          }
        }
      },
      "LateFeeRequest": {
        "type": "object",
        "required": [
          "overdue_amount_minor",
          "currency",
          "days_late"
        ],
        "properties": {
          "overdue_amount_minor": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "days_late": {
            "type": "integer",
            "format": "int64"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Defaults to now if omitted."
          }
        }
      },
      "LateFeeResponse": {
        "type": "object",
        "required": [
          "late_fee"
        ],
        "properties": {
          "late_fee": {
            "$ref": "#/components/schemas/Money"
          }
        }
      },
      "SimulateRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ScheduleRules"
          },
          {
            "type": "object",
            "required": [
              "face_amount_minor",
              "currency",
              "rate_bp",
              "tenor_days"
            ],
            "properties": {
              "face_amount_minor": {
                "type": "integer",
                "format": "int64"
              },
              "currency": {
                "type": "string"
              },
              "rate_bp": {
                "type": "integer",
                "format": "int64"
              },
              "tenor_days": {
                "type": "integer",
                "format": "int64"
              }
            }
          }
        ],
        "description": "A hypothetical schedule's rules, inline, plus the hypothetical trade to price against them. No database round trip, no tenant, no persistence."
      },
      "AnchorVerifyRequest": {
        "type": "object",
        "required": [
          "tenant_id",
          "record_hash",
          "seal_seq"
        ],
        "properties": {
          "tenant_id": {
            "type": "string"
          },
          "record_hash": {
            "type": "string",
            "description": "Hex-encoded record hash to verify."
          },
          "proof": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "hash",
                "on_right"
              ],
              "properties": {
                "hash": {
                  "type": "string",
                  "description": "Hex-encoded."
                },
                "on_right": {
                  "type": "boolean"
                }
              }
            },
            "description": "The Merkle inclusion proof path for record_hash."
          },
          "seal_seq": {
            "type": "integer",
            "format": "int64",
            "description": "The audit seal sequence number this proof is checked against."
          }
        }
      },
      "AnchorVerifyResponse": {
        "type": "object",
        "required": [
          "valid"
        ],
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "chain_ref": {
            "type": "string",
            "description": "The Bantu chain transaction reference, present when valid is true."
          },
          "detail": {
            "type": "string",
            "description": "Present (and valid: false) when the seal/record could not be found \u2014 e.g. not yet anchored. This is a normal, expected outcome of this public endpoint, not an error: anyone can submit any hash."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was malformed (e.g. unknown JSON field, wrong type, a plainly invalid value such as an unparseable date). `code` varies per endpoint (e.g. `MALFORMED_BODY`, `MISSING_FIELDS`, `INVALID_INPUT`).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "title": "Bad Request",
              "status": 400,
              "code": "MALFORMED_BODY",
              "detail": "json: unknown field \"foo\""
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, or invalid/expired bearer token. Deliberately generic \u2014 the API never reveals *why* a credential was rejected.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "title": "Unauthorized",
              "status": 401,
              "code": "UNAUTHORIZED",
              "detail": "invalid or expired session"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The caller is authenticated but is not permitted to perform this action against this resource (an ownership or maker-checker rule), or the session is not a step-up session on a step-up-gated endpoint.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "title": "Forbidden",
              "status": 403,
              "code": "STEP_UP_REQUIRED",
              "detail": "this action requires step-up re-authentication"
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist in the caller's tenant. Deliberately indistinguishable from \"exists but belongs to another tenant\" (doc 06's tenancy principle: cross-tenant access is reported as 404, never 403, to avoid existence leaks).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Conflict": {
        "description": "The request is well-formed and the resource exists, but its current state does not allow this action. `code` names the specific rule (e.g. `INVALID_TRANSITION`, `ALREADY_MEMBER`, `OUTSTANDING_INVOICES_EXIST`).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "One or more fields failed validation. `errors` carries a `field`/`detail` pair per failing field.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "title": "Validation Failed",
              "status": 422,
              "code": "VALIDATION_FAILED",
              "errors": [
                {
                  "field": "amount_minor",
                  "detail": "must be positive"
                }
              ]
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "The request is well-formed and the target resource's state is fine, but the submitted values fail a business rule evaluated against other data (e.g. an exposure limit, a rate-improvement rule). `code` names the specific rule.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "title": "Unprocessable Entity",
              "status": 422,
              "code": "LIMIT_EXCEEDED",
              "detail": "financier exposure limit exceeded"
            }
          }
        }
      },
      "Internal": {
        "description": "An unexpected server-side failure. The response body deliberately never includes the underlying error \u2014 correlate with the server logs using the `X-Request-Id` response header (set by chi's RequestID middleware).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "title": "Internal Server Error",
              "status": 500,
              "code": "INTERNAL"
            }
          }
        }
      }
    }
  }
}
