openapi: 3.1.0
info:
  title: Cimbra API v1
  version: '2026-09-01'
  description: Versioned financial infrastructure API backed by a persistent double-entry ledger, scoped S2S API keys and signed durable webhooks. The product contract is production-grade. This host is sandbox, matching BIND APIBANK, sandbox.pismolabs.io and sandbox.api.pomelo.la. Production stays fail-closed until a production hostname is provisioned (Pismo hands it over in onboarding; BIND does not publish it; Pomelo uses api.pomelo.la after homologación) and at least one catalog product reaches Go Live. Every response includes X-Request-Id, Cimbra-Version and Cimbra-Environment. Responses after successful API-key authorization also expose X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Mutations replayed from an idempotency key include Idempotent-Replayed, while error responses may include Cimbra-Should-Retry so official SDKs can make safe retry decisions.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
externalDocs:
  description: Cimbra developer portal, quickstarts and operational limits
  url: https://cimbra-rose.vercel.app/developers
x-cimbra-environment: sandbox
x-cimbra-sdk:
  language: TypeScript
  version: 0.1.0
  url: https://cimbra-rose.vercel.app/sdk/cimbra-sdk-0.1.0.tgz
servers:
  - url: https://cimbra-rose.vercel.app
    description: Current environment is sandbox (BIND APIBANK, Pismo sandbox.pismolabs.io, Pomelo sandbox.api.pomelo.la). Production hostname is not provisioned.
security: []
paths:
  /api/auth/register:
    post:
      operationId: registerUser
      summary: Create a first-party account and session
      security: []
      responses:
        '201': { description: Account created }
        '409': { description: Email or username already exists }
        '429': { description: Too many attempts }
  /api/auth/login:
    post:
      operationId: loginUser
      summary: Start a session with email/username and password
      security: []
      responses:
        '200': { description: Authenticated }
        '401': { description: Invalid credentials }
        '429': { description: Too many attempts }
  /api/auth/logout:
    post:
      operationId: logoutUser
      summary: Revoke the current session
      security: [{ session: [] }]
      responses:
        '200': { description: Session revoked }
        '401': { description: Authentication required }
  /api/auth/me:
    get:
      operationId: getCurrentUser
      summary: Read the authenticated user
      security: [{ session: [] }]
      responses:
        '200': { description: Current user }
        '401': { description: Authentication required }
  /api/health:
    get:
      operationId: getHealth
      summary: Platform health
      security: []
      responses:
        '200': { description: Operational. Includes environment, liveReady and fail-closed live block reason. }
        '503': { description: Database or schema unavailable }
  /api/v1/customers:
    get:
      operationId: listCustomers
      summary: List customers with cursor pagination
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/PageLimit' }, { $ref: '#/components/parameters/PageCursor' }]
      responses:
        '200': { description: Customer page }
    post:
      operationId: createCustomer
      summary: Create a customer
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, country, taxId]
              properties:
                type: { type: string, enum: [individual, business] }
                name: { type: string, minLength: 2 }
                country: { type: string, enum: [AR, MX, CO, BR, CL, PE] }
                taxId: { type: string, minLength: 4 }
      responses:
        '201': { description: Customer created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '409': { description: Idempotency key was already used with a different payload }
        '401': { description: Authentication required }
  /api/v1/customers/{id}:
    get:
      operationId: retrieveCustomer
      summary: Retrieve a customer by ID
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Customer }
        '404': { description: Customer not found in tenant }
  /api/v1/accounts:
    get:
      operationId: listAccounts
      summary: List accounts with cursor pagination
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/PageLimit' }, { $ref: '#/components/parameters/PageCursor' }]
      responses:
        '200': { description: Account page }
    post:
      operationId: createAccount
      summary: Open an account
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customerId, currency, country]
              properties:
                customerId: { type: string, format: uuid }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                country: { type: string, enum: [AR, MX, CO, BR, CL, PE] }
      responses:
        '201': { description: Account opened }
        '200': { description: Idempotent replay }
        '409': { description: Idempotency key was already used with a different payload }
        '404': { description: Customer not found in tenant }
  /api/v1/accounts/{id}:
    get:
      operationId: retrieveAccount
      summary: Retrieve an account by ID
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Account }
        '404': { description: Account not found in tenant }
  /api/v1/accounts/{id}/statement:
    get:
      operationId: getAccountStatement
      summary: Retrieve a ledger-backed account statement
      description: Returns immutable postings for one tenant account plus opening and closing balances for an ISO-8601 period of at most 366 days. Reversals appear as separate compensating entries.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - { name: from, in: query, required: false, schema: { type: string, format: date-time }, description: Inclusive period start; defaults to 30 days before now. }
        - { name: to, in: query, required: false, schema: { type: string, format: date-time }, description: Exclusive period end; defaults to now. }
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200':
          description: Account statement page and period balances
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountStatement' }
        '400': { description: Invalid period or pagination }
        '403': { description: Missing accounts:read scope or console role }
        '404': { description: Account not found in tenant }
  /api/v1/card-programs:
    get:
      operationId: listCardPrograms
      summary: List tenant card programs
      description: Returns native Cimbra program definitions. No network, sponsor or competitor integration is implied.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Card programs
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/CardProgram' } }
    post:
      operationId: createCardProgram
      summary: Create an immutable card issuing program definition
      description: Session users must be owner or admin. API keys require cards:write. Name uniqueness is enforced per tenant under a transaction lock.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, product, formats, defaultCurrency]
              additionalProperties: false
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                product: { type: string, enum: [debit, credit, prepaid] }
                formats: { type: array, minItems: 1, maxItems: 2, uniqueItems: true, items: { type: string, enum: [virtual, physical] } }
                defaultCurrency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
      responses:
        '201': { description: Program created }
        '200': { description: Idempotent replay }
        '409': { description: Idempotency mismatch or duplicate program name }
  /api/v1/card-programs/{id}:
    get:
      operationId: retrieveCardProgram
      summary: Retrieve a tenant card program
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Card program
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CardProgram' }
        '404': { description: Program not found in tenant }
  /api/v1/cards:
    get:
      operationId: listCards
      summary: List cards with cursor pagination
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/PageLimit' }, { $ref: '#/components/parameters/PageCursor' }]
      responses:
        '200':
          description: Card page
          content:
            application/json:
              schema:
                type: object
                required: [data, hasMore, nextCursor]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Card' } }
                  hasMore: { type: boolean }
                  nextCursor: { type: [string, 'null'] }
    post:
      operationId: createCard
      summary: Issue a native sandbox card from a program
      description: Physical instruments start in created and require activation; virtual instruments start active. Only last4 is generated and returned. Cimbra never creates or exposes PAN or CVV in this sandbox.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accountId]
              additionalProperties: false
              properties:
                accountId: { type: string, format: uuid }
                programId: { type: string, format: uuid }
                product: { type: string, enum: [debit, credit, prepaid] }
                format: { type: string, enum: [virtual, physical] }
      responses:
        '201':
          description: Card issued
          content:
            application/json:
              schema:
                type: object
                required: [ok, card, replayed]
                properties:
                  ok: { type: boolean, const: true }
                  card: { $ref: '#/components/schemas/Card' }
                  replayed: { type: boolean }
        '200': { description: Idempotent replay }
        '409': { description: Idempotency key was already used with a different payload }
        '404': { description: Account not found in tenant }
  /api/v1/cards/{id}:
    get:
      operationId: retrieveCard
      summary: Retrieve a card by ID
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Card
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Card' }
        '404': { description: Card not found in tenant }
  /api/v1/cards/{id}/lifecycle:
    get:
      operationId: listCardLifecycle
      summary: List the append-only lifecycle of a card
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Lifecycle events ordered newest first
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/CardLifecycleEvent' } }
        '404': { description: Card not found in tenant }
    post:
      operationId: transitionCardStatus
      summary: Apply a validated card lifecycle transition
      description: Valid transitions depend on the current state. Terminated is irreversible. Every transition is tenant-scoped, idempotent, audited and emitted as a webhook event.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status, reason]
              additionalProperties: false
              properties:
                status: { type: string, enum: [active, frozen, terminated] }
                reason: { type: string, enum: [activation, user_request, internal_control, suspected_fraud, review_cleared, lost, stolen, damaged, customer_request, expired] }
      responses:
        '200': { description: Transition applied or replayed idempotently }
        '404': { description: Card not found in tenant }
        '409': { description: Invalid transition, reason or idempotency reuse }
  /api/v1/cards/{id}/controls:
    get:
      operationId: retrieveCardControls
      summary: Retrieve the current version of card controls
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Latest control version
          content:
            application/json:
              schema:
                type: object
                required: [controls]
                properties:
                  controls:
                    oneOf:
                      - { $ref: '#/components/schemas/CardControls' }
                      - { type: 'null' }
        '404': { description: Card not found in tenant }
    patch:
      operationId: replaceCardControls
      summary: Append a complete new version of card controls
      description: Limits use decimal major-unit strings at the boundary and BIGINT minor units in storage. Control versions are append-only; this sandbox records policy but does not claim network authorization enforcement.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currency, perTransactionLimit, dailyLimit, monthlyLimit, allowedChannels, allowedMccs, blockedMccs, status]
              additionalProperties: false
              properties:
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                perTransactionLimit: { type: [string, 'null'], pattern: '^\d+(?:\.\d+)?$' }
                dailyLimit: { type: [string, 'null'], pattern: '^\d+(?:\.\d+)?$' }
                monthlyLimit: { type: [string, 'null'], pattern: '^\d+(?:\.\d+)?$' }
                allowedChannels: { type: array, minItems: 1, maxItems: 5, uniqueItems: true, items: { type: string, enum: [ecommerce, contactless, chip, magstripe, atm] } }
                allowedMccs: { type: array, maxItems: 50, uniqueItems: true, items: { type: string, pattern: '^\d{4}$' } }
                blockedMccs: { type: array, maxItems: 50, uniqueItems: true, items: { type: string, pattern: '^\d{4}$' } }
                status: { type: string, enum: [active, inactive] }
      responses:
        '200': { description: New control version created or replayed idempotently }
        '404': { description: Card not found in tenant }
        '409': { description: Currency mismatch or idempotency conflict }
  /api/v1/transfers:
    get:
      operationId: listTransfers
      summary: List transfers with cursor pagination
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/PageLimit' }, { $ref: '#/components/parameters/PageCursor' }]
      responses:
        '200': { description: Transfer page }
    post:
      operationId: createTransfer
      summary: Create an idempotent sandbox transfer
      description: When the tenant enables transfer dual control, this returns 202 without reserving funds. A different owner/admin with MFA must approve in a human session; balance and risk are re-evaluated atomically at approval time.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [counterparty, description, amount, currency]
              properties:
                counterparty: { type: string, minLength: 2 }
                description: { type: string, minLength: 2 }
                amount:
                  description: Decimal major-unit amount. Prefer a string to avoid floating-point serialization.
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: Transfer created }
        '200': { description: Idempotent replay }
        '202': { description: Transfer dual-control policy is active and a human approval request is pending }
        '400': { description: Invalid payload or missing idempotency key }
        '401': { description: Authentication required }
        '403': { description: Role does not allow mutation }
        '409': { description: Idempotency mismatch or prior approval is no longer actionable }
        '422': { description: 'Insufficient available balance, risk decline or failed approved execution' }
  /api/v1/book-transfers:
    get:
      operationId: listBookTransfers
      summary: List native book transfers with cursor pagination
      description: Tenant-scoped account-to-account movements backed by paired ledger postings. This sandbox operation does not use an external rail or move real funds.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/PageLimit' }, { $ref: '#/components/parameters/PageCursor' }]
      responses:
        '200':
          description: Book transfer page
          content:
            application/json:
              schema:
                type: object
                required: [data, hasMore, nextCursor]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/BookTransfer' } }
                  hasMore: { type: boolean }
                  nextCursor: { type: [string, 'null'] }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: createBookTransfer
      summary: Atomically move balance between two Cimbra accounts
      description: Locks both tenant accounts, validates status, currency and available balance, evaluates risk, and either creates a source hold or posts an atomic debit/credit pair. The existing transfer.create maker/checker policy protects this operation when enabled.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [externalReference, sourceAccountId, destinationAccountId, description, amount, currency]
              properties:
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                sourceAccountId: { type: string, format: uuid }
                destinationAccountId: { type: string, format: uuid }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  description: Positive decimal major-unit amount. Prefer a string to avoid floating-point serialization.
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: Book transfer settled or placed in risk review }
        '200': { description: Idempotent replay }
        '202': { description: Maker/checker policy is active and a human approval is pending }
        '400': { description: Invalid payload or same source and destination }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Source or destination account not found in tenant }
        '409': { description: Account state, currency, external reference or idempotency conflict }
        '422': { description: Insufficient available balance or risk decline }
  /api/v1/book-transfers/{id}:
    get:
      operationId: retrieveBookTransfer
      summary: Retrieve a native book transfer
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Book transfer, content: { application/json: { schema: { $ref: '#/components/schemas/BookTransfer' } } } }
        '404': { description: Book transfer not found in tenant }
  /api/v1/book-transfers/{id}/reverse:
    post:
      operationId: reverseBookTransfer
      summary: Reverse a settled book transfer with compensating postings
      description: Never mutates the original postings. It creates an opposite journal, links the reversal transaction and preserves the full account statement history.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Compensating reversal created }
        '200': { description: Idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Book transfer not found in tenant }
        '409': { description: Transfer is not settled, was already reversed or idempotency conflicts }
  /api/v1/wallet-programs:
    get:
      operationId: listWalletPrograms
      summary: List native wallet programs
      description: Tenant-scoped white-label wallet programs. Branding is stored configuration, not a consumer app or custodial license.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Wallet program list
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/WalletProgram' } }
        '403': { description: Missing wallets:read scope or console role }
    post:
      operationId: createWalletProgram
      summary: Create a tenant wallet program
      description: Owner/Admin configure display name, currencies and pocket kinds. Pockets always include available and map to product ledger accounts.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name, displayName, defaultCurrency]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                displayName: { type: string, minLength: 2, maxLength: 80 }
                supportUrl: { type: [string, 'null'], format: uri, maxLength: 200 }
                termsUrl: { type: [string, 'null'], format: uri, maxLength: 200 }
                accentColor: { type: [string, 'null'], pattern: '^#[0-9A-Fa-f]{6}$' }
                defaultCurrency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                allowedCurrencies: { type: array, minItems: 1, maxItems: 7, uniqueItems: true, items: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] } }
                pocketKinds: { type: array, minItems: 1, maxItems: 3, uniqueItems: true, items: { type: string, enum: [available, pending, rewards] } }
      responses:
        '201': { description: Wallet program created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid program payload }
        '403': { description: Missing wallets:write scope or wallets.program.manage permission }
        '409': { description: Duplicate name or idempotency conflict }
  /api/v1/wallet-programs/{id}:
    get:
      operationId: retrieveWalletProgram
      summary: Retrieve a wallet program
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Wallet program, content: { application/json: { schema: { $ref: '#/components/schemas/WalletProgram' } } } }
        '404': { description: Wallet program not found in tenant }
  /api/v1/wallets:
    get:
      operationId: listWallets
      summary: List customer wallets with cursor pagination
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/PageLimit' }, { $ref: '#/components/parameters/PageCursor' }]
      responses:
        '200':
          description: Wallet page
          content:
            application/json:
              schema:
                type: object
                required: [data, hasMore, nextCursor]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Wallet' } }
                  hasMore: { type: boolean }
                  nextCursor: { type: [string, 'null'] }
        '403': { description: Missing wallets:read scope or console role }
    post:
      operationId: createWallet
      summary: Open a customer wallet with ledger-backed pockets
      description: Creates one product account per program pocket kind. Does not custody real funds or issue an end-user app.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [programId, customerId, externalReference]
              properties:
                programId: { type: string, format: uuid }
                customerId: { type: string, format: uuid }
                externalReference: { type: string, minLength: 2, maxLength: 100 }
      responses:
        '201': { description: Wallet and pockets created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid wallet payload }
        '403': { description: Missing wallets:write scope or finance.write permission }
        '404': { description: Program or customer not found in tenant }
        '409': { description: Duplicate customer/program, reference or idempotency conflict }
  /api/v1/wallets/{id}:
    get:
      operationId: retrieveWallet
      summary: Retrieve a wallet and its pockets
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Wallet with pockets }
        '404': { description: Wallet not found in tenant }
  /api/v1/wallets/{id}/lifecycle:
    get:
      operationId: listWalletLifecycle
      summary: List append-only wallet lifecycle events
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Lifecycle events }
        '404': { description: Wallet not found in tenant }
    post:
      operationId: transitionWallet
      summary: Freeze, unfreeze or close a wallet
      description: Frozen and closed wallets mark pocket accounts inactive. Closing requires zero available balance and no active holds.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [status, reason]
              properties:
                status: { type: string, enum: [active, frozen, closed] }
                reason: { type: string, enum: [user_request, internal_control, suspected_fraud, review_cleared, customer_request, compliance] }
      responses:
        '200': { description: Lifecycle event created or replayed }
        '403': { description: Missing wallets:write scope or finance.write permission }
        '404': { description: Wallet not found in tenant }
        '409': { description: Invalid transition, remaining balance, active holds or idempotency conflict }
  /api/v1/wallets/{id}/pockets:
    get:
      operationId: listWalletPockets
      summary: List wallet pockets with ledger balances
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Pockets with derived balances }
        '404': { description: Wallet not found in tenant }
  /api/v1/wallets/{id}/transfers:
    post:
      operationId: createWalletPocketTransfer
      summary: Move balance between pockets of the same wallet
      description: Resolves pocket IDs to product accounts and executes a native book transfer with the same risk, holds and transfer.create maker/checker policy.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [externalReference, sourcePocketId, destinationPocketId, description, amount, currency]
              properties:
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                sourcePocketId: { type: string, format: uuid }
                destinationPocketId: { type: string, format: uuid }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  description: Positive decimal major-unit amount. Prefer a string to avoid floating-point serialization.
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: Pocket transfer settled or placed in risk review }
        '200': { description: Idempotent replay }
        '202': { description: Maker/checker policy is active and a human approval is pending }
        '400': { description: Invalid payload or same source and destination }
        '403': { description: Missing wallets:write scope or finance.write permission }
        '404': { description: Wallet or pocket not found in tenant }
        '409': { description: Wallet inactive, currency mismatch or idempotency conflict }
        '422': { description: Insufficient available balance or risk decline }
  /api/v1/rail-instruments:
    get:
      operationId: listRailInstruments
      summary: List sandbox CVU and tenant aliases
      description: Instruments issued by Cimbra as a sandbox PSP. Prefix 0009999 is not a Coelsa assignment. Aliases resolve only inside the tenant.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: Rail instrument page }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: issueRailInstruments
      summary: Issue a sandbox CVU and optional alias for an ARS Argentine account
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [accountId]
              properties:
                accountId: { type: string, format: uuid }
                alias: { type: [string, 'null'], minLength: 6, maxLength: 20 }
      responses:
        '201': { description: CVU and optional alias issued }
        '200': { description: Idempotent replay }
        '400': { description: Invalid instrument payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '409': { description: Active CVU already issued, alias conflict or idempotency mismatch }
  /api/v1/rail-instruments/{id}:
    get:
      operationId: retrieveRailInstrument
      summary: Retrieve a rail instrument owned by the tenant
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Rail instrument, content: { application/json: { schema: { $ref: '#/components/schemas/RailInstrument' } } } }
        '404': { description: Instrument not found in tenant }
    delete:
      operationId: revokeRailInstrument
      summary: Delete a sandbox CVU without touching the account or ledger
      description: Own Cimbra contract. Marks the CVU and its tenant alias as revoked so they cannot receive or send. The ARS account and its balance remain. A later issue call reactivates the same sandbox CVU for that account.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: CVU revoked or replayed }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Instrument not found in tenant }
        '409': { description: CVU already revoked or idempotency mismatch }
        '422': { description: No CVU exists for the instrument }
  /api/v1/rail-instruments/{id}/alias:
    patch:
      operationId: assignRailAlias
      summary: Assign or change the tenant alias of an existing sandbox CVU
      description: Own Cimbra contract. Assigns or replaces the alias of a CVU already issued in the tenant. Uniqueness is tenant-scoped, not a national directory. A real change is limited to once per 24 hours; issuing the CVU or the first assignment does not start that window. Repeating the same alias returns 200 without moving the clock.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [alias]
              properties:
                alias: { type: string, minLength: 6, maxLength: 20 }
      responses:
        '200': { description: Alias assigned, unchanged or replayed }
        '400': { description: Invalid alias format }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Instrument not found in tenant }
        '409': { description: Inactive instrument or idempotency mismatch }
        '422': { description: Alias taken in tenant, no active CVU, or more than one change in 24 hours }
  /api/v1/rail-directory:
    get:
      operationId: lookupRailDirectory
      summary: Preview holder data for a CBU, CVU or alias
      description: Tenant-scoped lookup. Internal hits return holder name and tax last4. External CBU/CVU return last4 only. There is no national alias directory.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 6, maxLength: 22 }
      responses:
        '200': { description: Directory preview }
        '400': { description: Malformed CBU, CVU or alias }
        '403': { description: Missing transfers:read scope or console role }
  /api/v1/instant-transfers:
    get:
      operationId: listInstantTransfers
      summary: List sandbox instant transfers
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
        - name: scheme
          in: query
          schema: { type: string, enum: [credit_push, debit_pull, qr_collect] }
      responses:
        '200': { description: Instant transfer page }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: createInstantTransfer
      summary: Create a sandbox credit push with holder confirmation
      description: Internal CVU/alias settles between product accounts. External CBU/CVU posts cash-out to settlement. Inbound credits a Cimbra CVU from an external originator. Rail is cimbra_sandbox, not Coelsa.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [externalReference, accountId, destination, description, amount, currency, confirmHolder, holderName, taxIdLast4]
              properties:
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                accountId: { type: string, format: uuid }
                destination: { type: string, minLength: 6, maxLength: 22 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS] }
                direction: { type: string, enum: [outbound, inbound] }
                confirmHolder: { type: boolean }
                holderName: { type: string, minLength: 2, maxLength: 160 }
                taxIdLast4: { type: string, pattern: '^\d{4}$' }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: Instant transfer created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Account, alias or sandbox CVU not found }
        '409': { description: Inactive account, currency mismatch or idempotency conflict }
        '422': { description: Holder mismatch, insufficient funds or risk decline }
  /api/v1/instant-transfers/{id}:
    get:
      operationId: retrieveInstantTransfer
      summary: Retrieve an instant transfer
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Instant transfer, content: { application/json: { schema: { $ref: '#/components/schemas/InstantTransfer' } } } }
        '404': { description: Transfer not found in tenant }
  /api/v1/instant-transfers/{id}/return:
    post:
      operationId: returnInstantTransfer
      summary: Return a settled instant transfer with compensating postings
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Compensating return created }
        '200': { description: Idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Transfer not found in tenant }
        '409': { description: Transfer is not settled or already returned }
  /api/v1/debit-requests:
    get:
      operationId: listDebitRequests
      summary: List internal debit-pull requests
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: Debit request page }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: createDebitRequest
      summary: Request an internal debit from another Cimbra account in the tenant
      description: External CBU/CVU debit is rejected with external_debit_not_supported. This is not DEBIN over Coelsa.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [externalReference, collectorAccountId, payerDestination, description, amount, currency]
              properties:
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                collectorAccountId: { type: string, format: uuid }
                payerDestination: { type: string, minLength: 6, maxLength: 22 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS] }
                expiresInMinutes: { type: integer, minimum: 5, maximum: 1440 }
      responses:
        '201': { description: Debit request created pending acceptance }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '422': { description: External debit is not supported }
  /api/v1/debit-requests/{id}/respond:
    post:
      operationId: respondDebitRequest
      summary: Accept or reject a pending internal debit request
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [decision]
              properties:
                decision: { type: string, enum: [accept, reject] }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: Debit accepted and settled, or rejected }
        '200': { description: Idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Debit request not found }
        '409': { description: Request is not pending or expired }
        '422': { description: Insufficient funds or risk decline }
  /api/v1/payment-qrs:
    get:
      operationId: listPaymentQrs
      summary: List Cimbra QR collection payloads
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: Payment QR page }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: createPaymentQr
      summary: Create a Cimbra-owned QR payload for an account
      description: Dynamic payloads use cimbra:qr:v1 and expire after one payment. Static payloads use cimbra:qr:static:v1, require an active sandbox CVU, stay open-amount and reusable until cancelled, and allow one active static QR per account. Neither payload is the Argentine interoperable QR.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [accountId, description]
              properties:
                accountId: { type: string, format: uuid }
                kind: { type: string, enum: [dynamic, static], default: dynamic }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS] }
                description: { type: string, minLength: 2, maxLength: 180 }
                expiresInMinutes: { type: integer, minimum: 5, maximum: 1440 }
      responses:
        '201': { description: QR created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload or static QR with amount or expiry }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '409': { description: Account already has an active static QR }
        '422': { description: Static QR requires an active sandbox CVU }
  /api/v1/payment-qrs/{id}/pay:
    post:
      operationId: payPaymentQr
      summary: Pay a Cimbra QR from another account in the tenant
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [sourceAccountId, externalReference]
              properties:
                sourceAccountId: { type: string, format: uuid }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: QR paid }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload or open-amount QR without amount }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: QR not found }
        '409': { description: QR inactive or expired }
        '422': { description: Amount mismatch, sale order mismatch, insufficient funds or risk decline }
  /api/v1/payment-qrs/{id}/cancel:
    post:
      operationId: cancelPaymentQr
      summary: Cancel an active Cimbra QR so it can no longer be paid
      description: Stops a reusable static QR or an unused dynamic QR. Does not reverse settled payments. Not a Coelsa or competitor caja command.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: QR cancelled }
        '200': { description: Idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: QR not found }
        '409': { description: QR is not active }
  /api/v1/qr-sale-orders:
    get:
      operationId: listQrSaleOrders
      summary: List Cimbra sale orders overlaid on static QRs
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: Sale order page }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: createQrSaleOrder
      summary: Overlay a closed amount on an active Cimbra static QR
      description: One pending sale order per static QR. Creating another supersedes the pending one. Not a BIND caja, EMVCo or Transferencias 3.0 order.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [paymentQrId, externalReference, description, amount]
              properties:
                paymentQrId: { type: string, format: uuid }
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS] }
                expiresInMinutes: { type: integer, minimum: 1, maximum: 1440, default: 10 }
      responses:
        '201': { description: Sale order created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: QR not found }
        '409': { description: QR inactive or external reference conflict }
        '422': { description: Sale order requires an active static QR }
  /api/v1/qr-sale-orders/{id}:
    get:
      operationId: retrieveQrSaleOrder
      summary: Retrieve a Cimbra sale order by id
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Sale order }
        '403': { description: Missing transfers:read scope or console role }
        '404': { description: Sale order not found }
    delete:
      operationId: cancelQrSaleOrder
      summary: Delete a pending Cimbra sale order and return the static QR to open amount
      description: Does not reverse settled payments. Not a Coelsa or competitor caja command.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: Sale order cancelled or idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: Sale order not found }
        '409': { description: Sale order is not pending }
  /api/v1/qr-debts:
    get:
      operationId: listQrDebts
      summary: List Cimbra QR debts with a closed amount and a single payment
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: QR debt page }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: createQrDebt
      summary: Create a Cimbra QR debt with a closed amount and one payment
      description: Resource is the debt, not an overlay on a static QR. Payload cimbra:qr:debt:v1. Requires an active sandbox CVU on the collecting account. Sandbox settles synchronously in the same transaction. Not BIND deuda, EMVCo or Coelsa PCT.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [accountId, externalReference, description, amount]
              properties:
                accountId: { type: string, format: uuid }
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS] }
                expiresInMinutes: { type: integer, minimum: 5, maximum: 1440, default: 1440 }
      responses:
        '201': { description: QR debt created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '409': { description: External reference conflict }
        '422': { description: Collecting account requires an active sandbox CVU }
  /api/v1/qr-debts/{id}:
    get:
      operationId: retrieveQrDebt
      summary: Retrieve a Cimbra QR debt by id
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: QR debt }
        '403': { description: Missing transfers:read scope or console role }
        '404': { description: QR debt not found }
    delete:
      operationId: cancelQrDebt
      summary: Delete an open Cimbra QR debt so the QR can no longer be paid
      description: Does not reverse a settled payment. A paid debt is not reopened. Not a Coelsa or competitor deuda command.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: QR debt cancelled or idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: QR debt not found }
        '409': { description: QR debt is not open }
  /api/v1/payment-links:
    get:
      operationId: listPaymentLinks
      summary: List sandbox collection payment links
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: Payment link page }
        '403': { description: Missing payments:read scope or console role }
    post:
      operationId: createPaymentLink
      summary: Create a sandbox collection link against an Argentine ARS account
      description: Payload format cimbra:link:v1 is not a card network checkout. Allowed methods are internal Cimbra accounts, sandbox inbound cash-in, Cimbra QR of an associated open debt (cimbra_qr) or the sandbox CVU of an associated collection till (cimbra_cvu). Optional items are checkout display only and do not change the amount to collect. Card, POS, hosted PCI checkout, successUrl/errorUrl and interoperable QR return 422 or 400. Only cimbra_cvu accepts partial credits, multiple credits or an amount above the remaining balance. QR and internal collection stay closed-amount.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [accountId, externalReference, description, amount]
              properties:
                accountId: { type: string, format: uuid }
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS] }
                expiresInMinutes: { type: integer, minimum: 5, maximum: 10080 }
                methods:
                  type: array
                  minItems: 1
                  items: { type: string, enum: [internal, sandbox_inbound, cimbra_qr, cimbra_cvu] }
                qrDebtId: { type: [string, 'null'], format: uuid }
                collectionTillId: { type: [string, 'null'], format: uuid }
                items:
                  type: array
                  maxItems: 20
                  items: { $ref: '#/components/schemas/PaymentLinkItemInput' }
      responses:
        '201': { description: Payment link created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing payments:write scope or finance.write permission }
        '422': { description: Card, POS, hosted checkout or interoperable QR requested }
  /api/v1/payment-links/{id}:
    get:
      operationId: retrievePaymentLink
      summary: Retrieve a collection payment link
      description: The response embeds informational checkout items, CVU credits and partiallyCollected. Items do not change the amount to collect.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Payment link
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaymentLink' }
        '403': { description: Missing payments:read scope or console role }
        '404': { description: Payment link not found }
  /api/v1/payment-links/{id}/cancel:
    post:
      operationId: cancelPaymentLink
      summary: Cancel an open collection link
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Payment link cancelled }
        '200': { description: Idempotent replay }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Payment link not found }
        '409': { description: Link is not open }
  /api/v1/payment-links/{id}/pay:
    post:
      operationId: payPaymentLink
      summary: Collect a sandbox payment link
      description: internal debits another Cimbra account in the tenant. sandbox_inbound posts a cash-in against settlement. cimbra_qr pays the associated closed QR debt. cimbra_cvu credits the associated till CVU. amount is accepted only for cimbra_cvu; omit it to credit the remaining balance. Partial credits, several credits and overpayment are allowed until collectedMinor >= amountMinor. Card acquiring is not supported.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [method]
              properties:
                method: { type: string, enum: [internal, sandbox_inbound, cimbra_qr, cimbra_cvu, card, pos, tap_to_phone, qr_interoperable] }
                payerAccountId: { type: string, format: uuid }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: Payment link collected }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Payment link not found }
        '409': { description: Link not open or already paid }
        '422': { description: Unsupported method, insufficient funds or risk decline }
  /api/v1/payment-links/{id}/refund:
    post:
      operationId: refundPaymentLink
      summary: Refund a settled collection in full or in part with compensating ledger entries
      description: Omitting amount returns the remaining collected balance. amount refunds that ARS value from CVU credits in chronological order, or from the single closed-amount collection. creditId limits the refund to one CVU credit. A CVU link can reopen while collected remains below the original amount. Internal, QR and inbound stay closed after a partial refund. Not a card chargeback or BIND contracargo.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                creditId: { type: [string, 'null'], format: uuid }
      responses:
        '201': { description: Collection refunded in full or in part }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Payment link or credit not found }
        '409': { description: Nothing to refund or amount exceeds collected }
        '422': { description: Insufficient merchant funds }
  /api/v1/collection-tills:
    get:
      operationId: listCollectionTills
      summary: List sandbox collection tills
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: Collection till page }
        '403': { description: Missing payments:read scope or console role }
    post:
      operationId: createCollectionTill
      summary: Create a collection till with its own sandbox CVU
      description: Own Cimbra contract. Issues a sandbox CVU (000+9999) attributed to this till, not a second CVU of the collector account. Optional static QR of the same account, or issueStaticQr to mint a till-owned cimbra:qr:static:v1 that does not consume the account's one static QR. closedAmountOnly requires a pending sale order to pay that QR. presence is present or not_present attribution, not POS. Optional alias. Not a BIND caja, Coelsa CVU, EMVCo QR or POS.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [accountId, externalReference, name]
              properties:
                accountId: { type: string, format: uuid }
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                name: { type: string, minLength: 2, maxLength: 80 }
                paymentQrId: { type: [string, 'null'], format: uuid }
                alias: { type: [string, 'null'], minLength: 6, maxLength: 20 }
                issueStaticQr: { type: boolean }
                closedAmountOnly: { type: boolean }
                presence: { type: string, enum: [present, not_present] }
      responses:
        '201': { description: Collection till created }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing payments:write scope or finance.write permission }
        '409': { description: Duplicate reference, alias or static QR }
        '422': { description: Static QR is not an active account-owned static QR of the same account }
  /api/v1/collection-tills/{id}:
    get:
      operationId: retrieveCollectionTill
      summary: Retrieve a collection till
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Collection till }
        '403': { description: Missing payments:read scope or console role }
        '404': { description: Collection till not found }
    delete:
      operationId: disableCollectionTill
      summary: Disable a collection till and its sandbox CVU
      description: Irreversible. The CVU and alias stop resolving in the tenant directory. A new POST creates another till. Returns 200 JSON, including idempotent replay. Not a Coelsa disable.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: Collection till disabled or idempotent replay }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Collection till not found }
  /api/v1/collection-tills/{id}/alias:
    patch:
      operationId: assignCollectionTillAlias
      summary: Assign or change the tenant alias of a collection till
      description: Uniqueness is tenant-scoped against rail instruments and other tills. A real change is limited to once per 24 hours; the first assignment does not start that window.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [alias]
              properties:
                alias: { type: string, minLength: 6, maxLength: 20 }
      responses:
        '200': { description: Alias assigned, unchanged or replayed }
        '400': { description: Invalid alias format }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Collection till not found }
        '409': { description: Till disabled or idempotency mismatch }
        '422': { description: Alias taken in tenant or more than one change in 24 hours }
  /api/v1/collection-tills/{id}/static-qr:
    post:
      operationId: issueCollectionTillStaticQr
      summary: Issue the till-owned static QR of a collection till
      description: Own Cimbra contract. Mints one cimbra:qr:static:v1 owned by this till. The payload never changes. Paying it attributes the transfer to collectionTillId. If closedAmountOnly is true, a pending sale order is required. Not interoperable, EMVCo, PCT or a BIND caja QR.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties: {}
      responses:
        '201': { description: Till static QR issued }
        '200': { description: Idempotent replay }
        '400': { description: Extra keys rejected }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Collection till not found }
        '409': { description: Till disabled or QR already issued }
  /api/v1/collection-tills/{id}/inbound:
    post:
      operationId: creditCollectionTill
      summary: Credit a collection till with a sandbox inbound cash-in
      description: Ledger cash-in to the collector account attributed to this till. Creates an inbound instant transfer with collectionTillId. Not a Coelsa credit.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [externalReference, description, amount]
              properties:
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS] }
      responses:
        '201': { description: Till credited }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Collection till not found }
        '409': { description: Till disabled or reference conflict }
        '422': { description: Risk decline }
  /api/v1/echeqs:
    get:
      operationId: listEcheqs
      summary: List sandbox electronic checks
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/PageCursor'
      responses:
        '200': { description: ECHEQ page }
        '403': { description: Missing transfers:read scope or console role }
    post:
      operationId: issueEcheq
      summary: Issue a sandbox ECHEQ against an Argentine ARS account
      description: Payload format cimbra:echeq:v1 is not a CMC7 or Coelsa identifier. Discount, custody, USD and chamber clearing return 422. Only current-account holders can issue real ECHEQ.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [drawerAccountId, externalReference, description, amount, beneficiaryName, beneficiaryTaxId]
              properties:
                drawerAccountId: { type: string, format: uuid }
                externalReference: { type: string, minLength: 2, maxLength: 100 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount:
                  oneOf:
                    - { type: string, pattern: '^\d+(?:\.\d+)?$' }
                    - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
                currency: { type: string, enum: [ARS, USD] }
                beneficiaryName: { type: string, minLength: 2, maxLength: 120 }
                beneficiaryTaxId: { type: string, minLength: 11, maxLength: 13 }
                paymentDate: { type: string, format: date }
                toOrder: { type: boolean }
                discount: { type: boolean }
                custody: { type: boolean }
                clearing: { type: string, enum: [coelsa, camara] }
      responses:
        '201': { description: ECHEQ issued }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '422': { description: Discount, custody, USD or Coelsa clearing requested }
  /api/v1/echeqs/{id}:
    get:
      operationId: retrieveEcheq
      summary: Retrieve a sandbox ECHEQ
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: ECHEQ
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Echeq' }
        '403': { description: Missing transfers:read scope or console role }
        '404': { description: ECHEQ not found }
  /api/v1/echeqs/{id}/accept:
    post:
      operationId: acceptEcheq
      summary: Accept a sandbox ECHEQ as the current beneficiary
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [accountId, taxId]
              properties:
                accountId: { type: string, format: uuid }
                taxId: { type: string, minLength: 11, maxLength: 13 }
      responses:
        '201': { description: ECHEQ accepted }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: ECHEQ not found }
        '409': { description: ECHEQ is not acceptable }
        '422': { description: Holder or tax mismatch }
  /api/v1/echeqs/{id}/endorse:
    post:
      operationId: endorseEcheq
      summary: Endorse an accepted to-order sandbox ECHEQ
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [beneficiaryName, beneficiaryTaxId]
              properties:
                beneficiaryName: { type: string, minLength: 2, maxLength: 120 }
                beneficiaryTaxId: { type: string, minLength: 11, maxLength: 13 }
                discount: { type: boolean }
      responses:
        '201': { description: ECHEQ endorsed }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: ECHEQ not found }
        '409': { description: ECHEQ is not endorsable }
        '422': { description: Not to order or discount requested }
  /api/v1/echeqs/{id}/deposit:
    post:
      operationId: depositEcheq
      summary: Deposit a sandbox ECHEQ into a Cimbra account
      description: Settles immediately on the Cimbra ledger. Deposit to CBU/CVU/Coelsa returns 422. Insufficient drawer funds mark the ECHEQ rejected.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [accountId, taxId]
              properties:
                accountId: { type: string, format: uuid }
                taxId: { type: string, minLength: 11, maxLength: 13 }
                destinationKind: { type: string, enum: [cimbra_account, cbu, cvu, coelsa] }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: ECHEQ deposited or rejected for NSF }
        '200': { description: Idempotent replay }
        '400': { description: Invalid payload }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: ECHEQ not found }
        '409': { description: ECHEQ is not depositable }
        '422': { description: Not due, Coelsa clearing, mismatch, insufficient funds or risk decline }
  /api/v1/echeqs/{id}/cancel:
    post:
      operationId: cancelEcheq
      summary: Cancel an issued sandbox ECHEQ before acceptance
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: ECHEQ cancelled }
        '200': { description: Idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: ECHEQ not found }
        '409': { description: ECHEQ is not cancellable }
  /api/v1/echeqs/{id}/return:
    post:
      operationId: returnEcheq
      summary: Return an accepted or endorsed sandbox ECHEQ before deposit
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: ECHEQ returned }
        '200': { description: Idempotent replay }
        '403': { description: Missing transfers:write scope or finance.write permission }
        '404': { description: ECHEQ not found }
        '409': { description: ECHEQ is not returnable }
  /api/v1/payments:
    post:
      operationId: createPayment
      summary: Create a cash-in or cash-out against an account
      description: Posts to the tenant ledger settlement account. When payment.create maker/checker is enabled, returns 202 with a pending approval instead of settling immediately.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accountId, direction, counterparty, description, amount, currency]
              properties:
                accountId: { type: string, format: uuid }
                direction: { type: string, enum: [cash_in, cash_out] }
                counterparty: { type: string, minLength: 2, maxLength: 120 }
                description: { type: string, minLength: 2, maxLength: 180 }
                amount: { type: string, pattern: '^\d+(?:\.\d+)?$' }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201': { description: Payment created }
        '202': { description: Pending maker/checker approval }
        '200': { description: Idempotent replay }
        '404': { description: Account not found in tenant }
        '409': { description: Currency mismatch or inactive account }
        '422': { description: Insufficient available balance or risk declined }
  /api/v1/payments/{id}:
    get:
      operationId: retrievePayment
      summary: Retrieve a cash-in or cash-out by ID
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Payment }
        '404': { description: Payment not found in tenant }
  /api/v1/payments/{id}/reverse:
    post:
      operationId: reversePayment
      summary: Reverse a settled cash-in or cash-out with compensating ledger postings
      description: Canonical payment reversal. When payment.reverse maker/checker is enabled, returns 202 with a pending approval instead of posting compensation immediately. Generic transfer reverse returns 409 payment_reverse_required for cash movements.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Payment reversed }
        '202': { description: Pending maker/checker approval }
        '200': { description: Idempotent replay }
        '403': { description: Missing payments:write scope or finance.write permission }
        '404': { description: Payment not found in tenant }
        '409': { description: Payment not settled, already reversed, or not a cash payment }
  /api/v1/billers:
    get:
      operationId: listBillers
      summary: List the tenant biller and top-up catalog
      description: Returns Cimbra-owned catalog records. It never proxies or simulates a competitor or external biller response.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Tenant biller catalog
          content:
            application/json:
              schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Biller' } } } }
        '403': { description: Missing billers:read scope or console role }
    post:
      operationId: createBiller
      summary: Register a directly contracted or internal sandbox biller
      description: Owner/Admin only for sessions. The record is tenant-scoped, idempotent, audited and emitted as an event.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateBillerInput' } } }
      responses:
        '201': { description: Biller created }
        '200': { description: Idempotent replay }
        '403': { description: Missing billers:write scope or billers.manage permission }
        '409': { description: Duplicate code or idempotency mismatch }
  /api/v1/billers/{id}:
    get:
      operationId: retrieveBiller
      summary: Retrieve a tenant biller
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Biller, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Biller' } } } } } }
        '404': { description: Biller not found in tenant }
  /api/v1/billers/{id}/status:
    post:
      operationId: updateBillerStatus
      summary: Activate or suspend a biller
      description: Owner/Admin only for sessions. Lifecycle changes are idempotent, audited and emitted as events.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties: { action: { type: string, enum: [activate, suspend] } }
      responses:
        '200': { description: Biller lifecycle updated or replayed }
        '404': { description: Biller not found in tenant }
        '409': { description: Idempotency mismatch }
  /api/v1/billers/{id}/obligations:
    get:
      operationId: listBillerObligations
      summary: List debts issued by a tenant biller
      description: An optional raw subscriber reference is normalized and hashed inside the tenant boundary; only its last four characters are returned.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: subscriberReference
          in: query
          required: false
          description: Subscriber identifier used only to calculate a tenant-scoped digest.
          schema: { type: string, minLength: 4, maxLength: 120 }
      responses:
        '200':
          description: Biller obligations
          content:
            application/json:
              schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/BillerObligation' } } } }
        '404': { description: Biller not found in tenant }
    post:
      operationId: createBillerObligation
      summary: Issue a debt for a bill-payment biller
      description: Direct originator ingestion only. Cimbra persists the canonical obligation and never fabricates an external debt query.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateBillerObligationInput' } } }
      responses:
        '201': { description: Obligation issued }
        '200': { description: Idempotent replay }
        '403': { description: Missing billers:write scope or billers.manage permission }
        '409': { description: External reference conflict or idempotency mismatch }
  /api/v1/bill-payments:
    get:
      operationId: listBillPaymentOrders
      summary: List bill-payment, top-up and gift-card orders
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Tenant orders
          content:
            application/json:
              schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/BillPaymentOrder' } } } }
    post:
      operationId: createBillPaymentOrder
      summary: Pay an obligation or execute a top-up
      description: Uses the canonical account, double-entry ledger, risk evaluation, holds and settlement path. A review response remains pending until its hold is captured or released.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateBillPaymentInput' } } }
      responses:
        '201': { description: Order created }
        '200': { description: Idempotent replay }
        '403': { description: Missing payments:write scope or finance.write permission }
        '409': { description: Currency, obligation state or idempotency conflict }
        '422': { description: Insufficient balance, risk decline or amount outside biller controls }
  /api/v1/bill-payments/{id}:
    get:
      operationId: retrieveBillPaymentOrder
      summary: Retrieve a bill-payment or top-up order
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Bill-payment order, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/BillPaymentOrder' } } } } } }
        '404': { description: Order not found in tenant }
  /api/v1/bill-payments/{id}/reverse:
    post:
      operationId: reverseBillPaymentOrder
      summary: Reverse a settled bill-payment or top-up order
      description: Creates a compensating ledger transaction and reopens a linked obligation; financial postings are never destructively mutated.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: Order reversed or idempotently replayed }
        '404': { description: Order not found in tenant }
        '409': { description: Order state or idempotency conflict }
  /api/v1/recurring-mandates:
    get:
      operationId: listRecurringPaymentMandates
      summary: List recurring payment mandates
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Tenant mandates
          content:
            application/json:
              schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/RecurringPaymentMandate' } } } }
    post:
      operationId: createRecurringPaymentMandate
      summary: Create a weekly or monthly payment mandate
      description: Persists consent evidence, limits, cadence and retry policy. It does not claim homologated direct debit or a live biller rail.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateRecurringPaymentMandateInput' } } }
      responses:
        '201': { description: Mandate created }
        '200': { description: Idempotent replay }
        '403': { description: Missing payments:write scope or finance.write permission }
        '409': { description: Biller/account state or idempotency conflict }
  /api/v1/recurring-mandates/{id}:
    get:
      operationId: retrieveRecurringPaymentMandate
      summary: Retrieve a recurring payment mandate
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Recurring mandate, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/RecurringPaymentMandate' } } } } } }
        '404': { description: Mandate not found in tenant }
  /api/v1/recurring-mandates/{id}/executions:
    get:
      operationId: listRecurringPaymentMandateExecutions
      summary: List execution attempts for a recurring mandate
      description: Returns sandbox charge attempts (settled, skipped, failed). Does not imply a live direct-debit rail.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        '200':
          description: Mandate executions
          content:
            application/json:
              schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/RecurringPaymentExecution' } } } }
        '404': { description: Mandate not found in tenant }
  /api/v1/recurring-mandates/{id}/status:
    post:
      operationId: updateRecurringPaymentMandateStatus
      summary: Pause, resume or cancel a recurring mandate
      description: Lifecycle actions are tenant-scoped, idempotent, audited and emitted as webhook events. Cancelled is terminal.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties: { action: { type: string, enum: [pause, resume, cancel] } }
      responses:
        '200': { description: Mandate lifecycle updated or replayed }
        '404': { description: Mandate not found in tenant }
        '409': { description: Invalid lifecycle transition or idempotency mismatch }
  /api/v1/payout-beneficiaries:
    get:
      operationId: listPayoutBeneficiaries
      summary: List tenant-scoped payout beneficiaries
      description: Returns only a protected destination type and last four characters; the original destination is never returned.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Beneficiary catalog
          content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/PayoutBeneficiary' } } } } } }
        '403': { description: Missing payouts:read scope or console role }
    post:
      operationId: createPayoutBeneficiary
      summary: Create a protected payout beneficiary
      description: Owner/Admin only for sessions. Cimbra persists a tenant-scoped digest and last four characters, not the raw destination.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreatePayoutBeneficiaryInput' } } }
      responses:
        '201': { description: Beneficiary created }
        '200': { description: Idempotent replay }
        '403': { description: Missing payouts:write scope or payouts.beneficiaries.manage permission }
        '409': { description: Duplicate reference/destination or idempotency mismatch }
  /api/v1/payout-beneficiaries/{id}:
    get:
      operationId: retrievePayoutBeneficiary
      summary: Retrieve one payout beneficiary
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Protected beneficiary, content: { application/json: { schema: { $ref: '#/components/schemas/PayoutBeneficiary' } } } }
        '404': { description: Beneficiary not found in tenant }
  /api/v1/payout-beneficiaries/{id}/status:
    post:
      operationId: updatePayoutBeneficiaryStatus
      summary: Activate or suspend a payout beneficiary
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [action]
              properties: { action: { type: string, enum: [activate, suspend] } }
      responses:
        '200': { description: Beneficiary lifecycle updated or replayed }
        '403': { description: Missing payouts:write scope or beneficiary-management permission }
        '409': { description: Idempotency mismatch }
  /api/v1/payout-batches:
    get:
      operationId: listPayoutBatches
      summary: List payout batches with per-item results
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Tenant payout batches
          content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/PayoutBatch' } } } } } }
        '403': { description: Missing payouts:read scope or console role }
    post:
      operationId: createPayoutBatch
      summary: Create an immutable payout batch draft
      description: Accepts 1-100 items. Every beneficiary, account and currency is validated inside the tenant before persistence.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreatePayoutBatchInput' } } }
      responses:
        '201': { description: Immutable draft created }
        '200': { description: Idempotent replay }
        '403': { description: Missing payouts:write scope or finance.write permission }
        '409': { description: Account, beneficiary, currency, reference or idempotency conflict }
  /api/v1/payout-batches/{id}:
    get:
      operationId: retrievePayoutBatch
      summary: Retrieve a payout batch and all item results
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Payout batch, content: { application/json: { schema: { $ref: '#/components/schemas/PayoutBatch' } } } }
        '404': { description: Batch not found in tenant }
  /api/v1/payout-batches/{id}/submit:
    post:
      operationId: submitPayoutBatch
      summary: Submit a payout batch for approval, scheduling or execution
      description: Returns 202. When dual control is enabled, a different Owner/Admin with MFA must authorize the batch. Otherwise a leased worker processes each item independently through risk and the ledger.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202': { description: Approval pending, scheduled or processing }
        '200': { description: Idempotent replay of a terminal result }
        '403': { description: Missing payouts:write scope or finance.write permission }
        '409': { description: Invalid state, expired processing window or idempotency conflict }
  /api/v1/payout-batches/{id}/cancel:
    post:
      operationId: cancelPayoutBatch
      summary: Cancel a draft or scheduled payout batch
      description: Pending approvals are cancelled through the approval request. Processing and terminal batches cannot be cancelled destructively.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: Batch cancelled or idempotently replayed }
        '409': { description: Batch is no longer directly cancellable }
  /api/v1/payout-batches/{id}/result:
    get:
      operationId: downloadPayoutBatchResult
      summary: Download the real per-item payout result file
      description: CSV contains protected beneficiary data, amounts, transaction links, terminal/current states and failure codes from the sandbox worker.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: UTF-8 CSV result file
          content: { text/csv: { schema: { type: string } } }
        '403': { description: Missing payouts:read scope or console role }
        '404': { description: Batch not found in tenant }
  /api/v1/capabilities:
    get:
      operationId: listPlatformCapabilities
      summary: List Cimbra-native financial infrastructure services and their real availability
      description: Competitors are benchmark inputs only. Every returned service is owned by Cimbra; networkBoundary identifies the direct regulated-rail boundary required for live money movement.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Cimbra service catalog
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/PlatformCapability' }
                  meta:
                    type: object
                    required: [owner, strategy, competitorDependency, networkBoundary, availabilityModel, graduation, environment, liveReady]
                    properties:
                      owner: { type: string, const: Cimbra }
                      strategy: { type: string, const: build_native }
                      competitorDependency: { type: boolean, const: false }
                      networkBoundary: { type: string, const: direct_regulated_rails_only }
                      availabilityModel:
                        type: array
                        items: { type: string, enum: [live, sandbox, foundation, roadmap] }
                      graduation: { type: string, const: integracion_homologacion_go_live }
                      environment: { type: string, enum: [sandbox, live] }
                      liveReady: { type: boolean }
  /api/v1/live-readiness:
    get:
      operationId: getLiveReadiness
      summary: Read sandbox vs production hosts, Pomelo go-live stages, catalog products, official rail connections, the AR PSPCP fintech path and the USD 500 Gate 1 capital plan
      description: liveReady is true only when a production hostname is provisioned, at least one product is in go_live, every required official rail of that product is live, and a Cimbra adapter is registered. CIMBRA_OPERATING_MODE=live does not move funds without that. Products homologate independently. Rails are BCRA, Coelsa, sponsor banks or schemes — never BIND, Dock, tapi, Pismo, Pomelo or Wibond. capitalPlan is the authorized USD 500 Gate 1 envelope; spending it does not set liveReady.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Live readiness snapshot
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/LiveReadiness' }
                  meta:
                    type: object
                    required: [owner, competitorDependency, networkBoundary, graduation]
                    properties:
                      owner: { type: string, const: Cimbra }
                      competitorDependency: { type: boolean, const: false }
                      networkBoundary: { type: string, const: direct_regulated_rails_only }
                      graduation: { type: string, const: integracion_homologacion_go_live }
        '401': { description: Authentication required }
        '403': { description: Missing platform:read scope or console role }
  /api/v1/risk:
    get:
      operationId: getRiskState
      summary: List system policies, tenant rules, evaluations and cases
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200': { description: Tenant-scoped risk operational state }
  /api/v1/risk/evaluations:
    post:
      operationId: createRiskEvaluation
      summary: Run and persist an explainable risk decision
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [operationType, amount, currency, counterparty]
              properties:
                operationType: { type: string, enum: [transfer, cash_in, cash_out] }
                amount: { type: string, pattern: '^\d+(\.\d+)?$' }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                counterparty: { type: string, minLength: 2, maxLength: 120 }
                signals: { $ref: '#/components/schemas/RiskSignalsInput' }
      responses:
        '201':
          description: Evaluation persisted; review or decline also creates a case
          content:
            application/json:
              schema:
                type: object
                required: [ok, evaluation, replayed]
                properties:
                  ok: { type: boolean, const: true }
                  evaluation: { $ref: '#/components/schemas/RiskEvaluation' }
                  replayed: { type: boolean }
        '200': { description: Idempotent replay }
  /api/v1/risk/evaluations/{id}/step-up-challenges:
    get:
      operationId: listRiskStepUpChallenges
      summary: List OTP step-up challenges for one risk evaluation
      description: Returns lifecycle metadata only. Credentials, hashes and encrypted values are never exposed by reads.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Tenant-scoped challenge history
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/RiskStepUpChallenge' } }
    post:
      operationId: createRiskStepUpChallenge
      summary: Create a client-managed OTP challenge for an evaluation in review
      description: Cimbra generates and protects the OTP. The credential is returned only by this authenticated create request and its identical replay while pending; it is absent from lists, audit events and webhooks. This is a rail-agnostic sandbox control, not 3DS or an ACS.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                method: { type: string, const: otp, default: otp }
                delivery: { type: string, const: client_managed, default: client_managed }
                expiresInSeconds: { type: integer, minimum: 60, maximum: 900, default: 300 }
                maxAttempts: { type: integer, minimum: 1, maximum: 10, default: 5 }
      responses:
        '201':
          description: Challenge created; credential must be delivered by the integrator through its approved channel
          content:
            application/json:
              schema:
                type: object
                required: [ok, challenge, credential, replayed]
                properties:
                  ok: { type: boolean, const: true }
                  challenge: { $ref: '#/components/schemas/RiskStepUpChallenge' }
                  credential: { type: string, pattern: '^\d{6}$', writeOnly: true }
                  replayed: { type: boolean }
        '200': { description: Idempotent replay while pending; terminal replays return a null credential }
        '404': { description: Evaluation not found in tenant }
        '409': { description: Evaluation is not in review or already has a pending challenge }
  /api/v1/risk/evaluations/{id}/step-up-challenges/{challengeId}/verify:
    post:
      operationId: verifyRiskStepUpChallenge
      summary: Verify one OTP attempt under lock and persist its result
      description: Attempts are append-only, idempotent and limited. A successful step-up becomes evidence for the risk case but never bypasses case resolution or maker/checker policy.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - in: path
          name: challengeId
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [credential]
              properties:
                credential: { type: string, pattern: '^\d{6}$', writeOnly: true }
      responses:
        '200':
          description: Attempt persisted; verified indicates whether the credential matched
          content:
            application/json:
              schema:
                type: object
                required: [ok, challenge, attempt, verified, replayed]
                properties:
                  ok: { type: boolean, const: true }
                  challenge: { $ref: '#/components/schemas/RiskStepUpChallenge' }
                  attempt: { $ref: '#/components/schemas/RiskStepUpAttempt' }
                  verified: { type: boolean }
                  replayed: { type: boolean }
        '404': { description: Evaluation or challenge not found in tenant }
        '409': { description: Idempotency-Key payload mismatch }
  /api/v1/risk/evaluations/{id}/outcomes:
    post:
      operationId: reportRiskOutcome
      summary: Append a confirmed legitimate or fraud outcome to an evaluation
      description: Corrections are immutable revisions and must identify the active outcome being superseded. Raw device and identity references are never returned.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label]
              properties:
                label: { type: string, enum: [legitimate, fraud] }
                fraudType: { type: string, enum: [account_takeover, identity_fraud, scam, stolen_instrument, merchant_fraud, other] }
                lossAmount: { type: string, pattern: '^\d+(\.\d+)?$' }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                note: { type: string, maxLength: 500 }
                supersedesOutcomeId: { type: string, format: uuid }
      responses:
        '201': { description: Confirmed outcome appended }
        '200': { description: Idempotent replay }
        '404': { description: Evaluation not found in tenant }
        '409': { description: Active outcome exists or the specified revision is stale }
  /api/v1/risk/lists:
    post:
      operationId: createRiskListEntry
      summary: Add a tenant-scoped counterparty, device or identity decision-list entry
      description: The subject is converted to a tenant-scoped one-way digest before persistence; only a digest-derived preview is returned.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subjectType, subjectValue, category, reason]
              properties:
                subjectType: { type: string, enum: [counterparty, device, identity] }
                subjectValue: { type: string, minLength: 2, maxLength: 160, writeOnly: true }
                category: { type: string, enum: [allow, watch, block] }
                reason: { type: string, minLength: 3, maxLength: 240 }
                expiresAt: { type: string, format: date-time }
      responses:
        '201': { description: List entry created }
        '200': { description: Idempotent replay }
        '409': { description: An active entry already exists for this subject }
  /api/v1/risk/lists/{id}:
    delete:
      operationId: disableRiskListEntry
      summary: Disable an active tenant risk-list entry
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Entry disabled }
        '404': { description: Entry not found or already disabled }
  /api/v1/risk/rules:
    post:
      operationId: createRiskRule
      summary: Create the first active champion of a tenant risk-policy family
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, kind, operationType, scoreDelta, action, configuration]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                kind: { type: string, enum: [amount_threshold, velocity_count, counterparty_match] }
                operationType: { type: string, enum: [any, transfer, cash_in, cash_out] }
                scoreDelta: { type: integer, minimum: 0, maximum: 100 }
                action: { type: string, enum: [score, review, decline] }
                priority: { type: integer, minimum: 1, maximum: 1000, default: 100 }
                configuration: { type: object, additionalProperties: true }
      responses:
        '201': { description: Rule created and activated }
        '200': { description: Idempotent replay }
  /api/v1/risk/rules/{id}/versions:
    post:
      operationId: createRiskRuleVersion
      summary: Create an immutable challenger version in an existing policy family
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, kind, operationType, scoreDelta, action, configuration]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                kind: { type: string, enum: [amount_threshold, velocity_count, counterparty_match] }
                operationType: { type: string, enum: [any, transfer, cash_in, cash_out] }
                scoreDelta: { type: integer, minimum: 0, maximum: 100 }
                action: { type: string, enum: [score, review, decline] }
                priority: { type: integer, minimum: 1, maximum: 1000, default: 100 }
                configuration: { type: object, additionalProperties: true }
      responses:
        '201': { description: Challenger version created without affecting live decisions }
        '200': { description: Idempotent replay }
        '404': { description: Base policy not found in this tenant }
  /api/v1/risk/rules/{id}/promote:
    post:
      operationId: promoteRiskRule
      summary: Atomically promote a challenger and archive its previous champion
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: Promotion completed or replayed idempotently }
        '404': { description: Candidate policy not found in this tenant }
        '409': { description: Candidate is not an active challenger }
  /api/v1/risk/simulations:
    post:
      operationId: simulateRiskRule
      summary: Compare a challenger against live champions without changing production decisions
      description: Accepts up to 50 samples and persists only aggregate decision summaries, never the raw sample payload.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [candidateRuleId, samples]
              properties:
                candidateRuleId: { type: string, format: uuid }
                samples:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    required: [operationType, amount, currency, counterparty]
                    properties:
                      operationType: { type: string, enum: [transfer, cash_in, cash_out] }
                      amount: { type: string, pattern: '^\d+(\.\d+)?$' }
                      currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                      counterparty: { type: string, minLength: 2, maxLength: 120 }
      responses:
        '201': { description: Aggregate baseline and candidate comparison persisted }
        '200': { description: Idempotent replay }
        '409': { description: Candidate is not an active challenger }
  /api/v1/risk/rules/{id}:
    delete:
      operationId: disableRiskRule
      summary: Disable an active tenant rule
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Rule disabled }
        '404': { description: Rule not found or already disabled }
  /api/v1/risk/cases/{id}/resolve:
    post:
      operationId: resolveRiskCase
      summary: Resolve a case and capture or release its linked hold
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resolution, note]
              properties:
                resolution: { type: string, enum: [approved, declined] }
                note: { type: string, minLength: 3, maxLength: 500 }
      responses:
        '200': { description: Case and hold resolved idempotently when direct execution is allowed }
        '202': { description: Dual-control policy is active and an approval request is pending }
        '409': { description: Case or hold already resolved differently, or a conflicting approval is pending }
  /api/v1/reconciliation:
    get:
      operationId: getReconciliationState
      summary: List reconciliation runs and exception queue
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200': { description: Tenant-scoped runs and exceptions }
  /api/v1/reconciliation/runs:
    post:
      operationId: createReconciliationRun
      summary: Compare an external batch with Cimbra transactions
      description: Produces exact matches, amount mismatches, missing internal records and missing external records. The maximum period is 31 days and each request accepts at most 500 entries.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, source, currency, periodStart, periodEnd, entries]
              properties:
                name: { type: string, minLength: 2, maxLength: 100 }
                source: { type: string, enum: [bank, clearing, card_network, cash_network, internal] }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                periodStart: { type: string, format: date-time }
                periodEnd: { type: string, format: date-time }
                entries:
                  type: array
                  maxItems: 500
                  items:
                    type: object
                    required: [externalReference, direction, amount]
                    properties:
                      externalReference: { type: string, minLength: 2, maxLength: 120 }
                      transactionId: { type: string, format: uuid }
                      direction: { type: string, enum: [credit, debit] }
                      amount: { type: string, pattern: '^\d+(\.\d+)?$' }
      responses:
        '201':
          description: Reconciliation run, items and exceptions persisted atomically
          content:
            application/json:
              schema:
                type: object
                required: [ok, run, replayed]
                properties:
                  ok: { type: boolean, const: true }
                  run: { $ref: '#/components/schemas/ReconciliationRun' }
                  replayed: { type: boolean }
        '200': { description: Idempotent replay }
  /api/v1/reconciliation/imports:
    post:
      operationId: importReconciliationCsv
      summary: Import a canonical UTF-8 CSV and reconcile it atomically
      description: The raw file is parsed in memory and discarded. Cimbra persists its SHA-256 checksum, safe filename and normalized entries. Maximum size is 2 MB and 500 data rows.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [name, source, currency, periodStart, periodEnd, file]
              properties:
                name: { type: string, minLength: 2, maxLength: 100 }
                source: { type: string, enum: [bank, clearing, card_network, cash_network, internal] }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                periodStart: { type: string, format: date-time }
                periodEnd: { type: string, format: date-time }
                file: { type: string, format: binary }
      responses:
        '201': { description: CSV parsed and reconciliation persisted }
        '200': { description: Idempotent replay }
        '400': { description: 'Invalid CSV, metadata or entries' }
  /api/v1/reconciliation/runs/{id}:
    get:
      operationId: retrieveReconciliationRun
      summary: Retrieve a run with every compared item
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Run and item detail }
        '404': { description: Run not found in tenant }
  /api/v1/reconciliation/exceptions/{id}/resolve:
    post:
      operationId: resolveReconciliationException
      summary: Correct or explicitly accept an exception
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resolution, note]
              properties:
                resolution: { type: string, enum: [corrected, accepted] }
                note: { type: string, minLength: 3, maxLength: 500 }
      responses:
        '200': { description: Exception resolved with actor and audit trail when direct execution is allowed }
        '202': { description: Dual-control policy is active and an approval request is pending }
        '409': { description: Exception already resolved differently, or a conflicting approval is pending }
  /api/v1/disputes:
    get:
      operationId: listDisputes
      summary: List disputes and eligible settled debits
      description: Returns tenant-scoped native Cimbra disputes plus debits that remain eligible under the 90-day sandbox window.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200': { description: Disputes and eligible transactions }
        '403': { description: Missing disputes:read scope or role }
    post:
      operationId: createDispute
      summary: Open a partial or full dispute for a settled debit
      description: Creates one dispute per transaction. The original transaction and postings remain immutable. A requested provisional credit is posted only when review begins.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [transactionId, reason, description, amount, currency]
              additionalProperties: false
              properties:
                transactionId: { type: string, format: uuid }
                reason: { type: string, enum: [card_not_present, duplicate, amount_mismatch, service_not_received, credit_not_processed, cash_not_received, other] }
                description: { type: string, minLength: 3, maxLength: 500 }
                amount: { type: string, pattern: '^\d+(\.\d+)?$' }
                currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
                provisionalCreditRequested: { type: boolean, default: false }
      responses:
        '201': { description: Dispute opened atomically }
        '200': { description: Idempotent replay }
        '409': { description: Transaction not presented, outside the window, already disputed or idempotency conflict }
  /api/v1/disputes/{id}:
    get:
      operationId: retrieveDispute
      summary: Retrieve a dispute with its immutable timeline
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Dispute and ordered event timeline }
        '404': { description: Dispute not found in tenant }
  /api/v1/disputes/{id}/events:
    post:
      operationId: transitionDispute
      summary: Apply an allowed dispute lifecycle transition
      description: Validates the state machine, posts or compensates dispute credits in the double-entry ledger, and emits audit and webhook events. network_ready never means sent to a real card network.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event, note]
              additionalProperties: false
              properties:
                event: { type: string, enum: [start_review, mark_network_ready, resolve_won, resolve_lost, reject, cancel] }
                note: { type: string, minLength: 3, maxLength: 500 }
      responses:
        '200': { description: Transition executed or idempotently replayed }
        '202': { description: Dual-control policy is active and an approval request is pending }
        '409': { description: Invalid transition, conflicting approval or idempotency conflict }
  /api/v1/operations/work-items:
    get:
      operationId: listOperationalWorkItems
      summary: List the unified risk, reconciliation and disputes work queue
      description: Returns tenant-scoped work items, assignable members, private document metadata, notes and evidence links for an operational console.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Unified operational work queue
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [workItems, members, documents, notes, evidence]
                    properties:
                      workItems: { type: array, items: { $ref: '#/components/schemas/OperationalWorkItem' } }
                      members: { type: array, items: { type: object, additionalProperties: true } }
                      documents: { type: array, items: { type: object, additionalProperties: true } }
                      notes: { type: array, items: { type: object, additionalProperties: true } }
                      evidence: { type: array, items: { type: object, additionalProperties: true } }
        '401': { description: Authentication required }
        '403': { description: Missing operations:read scope or role }
  /api/v1/operations/work-items/{type}/{id}:
    patch:
      operationId: updateOperationalWorkItem
      summary: Assign, prioritize, schedule or escalate an open work item
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/WorkItemType'
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              additionalProperties: false
              properties:
                assignedToUserId: { type: [string, 'null'], format: uuid }
                priority: { type: string, enum: [low, medium, high, critical] }
                dueAt: { type: [string, 'null'], format: date-time }
                escalated: { type: boolean }
      responses:
        '200': { description: Work item updated or idempotently replayed }
        '400': { description: Invalid update or assignee }
        '404': { description: Work item not found in tenant }
        '409': { description: Closed item or idempotency conflict }
  /api/v1/operations/work-items/{type}/{id}/notes:
    post:
      operationId: addOperationalWorkItemNote
      summary: Add an immutable comment to a work item
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/WorkItemType'
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              additionalProperties: false
              properties:
                body: { type: string, minLength: 3, maxLength: 2000 }
      responses:
        '201': { description: Comment appended }
        '200': { description: Idempotent replay }
        '404': { description: Work item not found in tenant }
        '409': { description: Idempotency conflict }
  /api/v1/operations/work-items/{type}/{id}/evidence:
    post:
      operationId: linkOperationalWorkItemEvidence
      summary: Link a private compliance document to a work item
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/WorkItemType'
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [documentId]
              additionalProperties: false
              properties:
                documentId: { type: string, format: uuid }
      responses:
        '201': { description: Evidence linked }
        '200': { description: Idempotent replay }
        '404': { description: Work item or document not found in tenant }
        '409': { description: Duplicate evidence or idempotency conflict }
  /api/v1/settlements:
    get:
      operationId: listSettlementCycles
      summary: List tenant settlement cycles
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Settlement cycles
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/SettlementCycle' } }
    post:
      operationId: createSettlementCycle
      summary: Create one sandbox settlement cycle from a completed reconciliation
      description: This records and schedules an operational acknowledgement. It does not move real funds or connect to a rail.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reconciliationRunId, name]
              properties:
                reconciliationRunId: { type: string, format: uuid }
                name: { type: string, minLength: 2, maxLength: 100 }
                scheduledFor: { type: string, format: date-time }
      responses:
        '201': { description: Settlement cycle created }
        '200': { description: Idempotent replay }
        '409': { description: Reconciliation is open or already has a cycle }
  /api/v1/settlements/{id}:
    get:
      operationId: retrieveSettlementCycle
      summary: Retrieve a tenant settlement cycle
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Settlement cycle
          content: { application/json: { schema: { $ref: '#/components/schemas/SettlementCycle' } } }
        '404': { description: Cycle not found }
  /api/v1/settlements/{id}/execute:
    post:
      operationId: executeSettlementCycle
      summary: Execute a due sandbox settlement cycle idempotently
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: 'Cycle settled directly, or an already executed approval replayed' }
        '202': { description: Dual-control policy is active and an approval request is pending }
        '409': { description: Cycle is not due or was already executed differently }
  /api/v1/approvals:
    get:
      operationId: listApprovalRequests
      summary: List maker/checker requests for the tenant
      description: API keys require approvals:read. Decisions remain session-only so an S2S credential cannot act as a human checker.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Approval request history
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/ApprovalRequest' } }
  /api/v1/approvals/{id}:
    get:
      operationId: retrieveApprovalRequest
      summary: Retrieve one tenant approval request
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Approval request, content: { application/json: { schema: { $ref: '#/components/schemas/ApprovalRequest' } } } }
        '404': { description: Approval request not found }
  /api/v1/approvals/{id}/approve:
    post:
      operationId: approveRequest
      summary: Approve and atomically execute a protected operation
      description: Requires a different owner/admin with MFA and a console session. API keys are rejected.
      security: [{ session: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { reason: { type: string, maxLength: 500 } }
      responses:
        '200': { description: Request approved and the protected settlement, transfer, risk case or reconciliation command executed in the same transaction }
        '403': { description: 'MFA, checker role or human session required' }
        '409': { description: 'Maker attempted self-approval, request expired or is no longer pending' }
        '422': { description: Transfer approval was valid but current balance or risk prevented execution; request becomes failed }
  /api/v1/approvals/{id}/reject:
    post:
      operationId: rejectApprovalRequest
      summary: Reject a protected operation with a reason
      security: [{ session: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties: { reason: { type: string, minLength: 3, maxLength: 500 } }
      responses:
        '200': { description: Request rejected and operation left unapplied }
        '403': { description: 'MFA, checker role or human session required' }
        '409': { description: Request expired or is no longer pending }
  /api/v1/approvals/{id}/cancel:
    post:
      operationId: cancelApprovalRequest
      summary: Cancel a pending request as its maker
      security: [{ session: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { reason: { type: string, maxLength: 500 } }
      responses:
        '200': { description: Request cancelled and history preserved }
        '403': { description: Only the maker may cancel }
        '409': { description: Request expired or is no longer pending }
  /api/v1/transfers/{id}/reverse:
    post:
      operationId: reverseTransfer
      summary: Reverse a settled transfer with a compensating journal
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201': { description: Reversal posted }
        '200': { description: Existing reversal returned }
        '404': { description: Original transfer not found }
        '409': { description: Transfer is not reversible }
  /api/v1/transfers/{id}:
    get:
      operationId: retrieveTransfer
      summary: Retrieve a transfer by ID
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Transfer }
        '404': { description: Transfer not found in tenant }
  /api/v1/ledger:
    get:
      operationId: getLedger
      summary: Read currency-isolated balances, journals and active holds
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200': { description: Ledger snapshot }
        '401': { description: Authentication required }
  /api/v1/holds/{id}/capture:
    post:
      operationId: captureHold
      summary: Capture an active hold and post its financial journal
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: Hold captured }
        '404': { description: Hold not found }
        '409': { description: Hold already resolved or linked to an open risk case protected by dual approval }
  /api/v1/holds/{id}/release:
    post:
      operationId: releaseHold
      summary: Release an active hold without posting a financial journal
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { description: Hold released }
        '404': { description: Hold not found }
        '409': { description: Hold already resolved or linked to an open risk case protected by dual approval }
  /api/v1/events:
    get:
      operationId: listAuditEvents
      summary: List the immutable tenant audit log
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - { $ref: '#/components/parameters/PageLimit' }
        - { $ref: '#/components/parameters/PageCursor' }
      responses:
        '200': { description: Cursor-paginated audit event list }
        '400': { description: Invalid cursor or limit }
        '401': { description: Authentication required }
  /api/v1/due-diligence:
    get:
      operationId: getDueDiligenceState
      summary: Read the tenant KYC/KYB policy, metrics, cases, customers and evidence inventory
      description: Returns Cimbra's native due-diligence orchestration state. It records evidence and controlled decisions but does not claim biometric, government-registry or regulated approval without a direct certified source.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Tenant-scoped due-diligence state
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/DueDiligenceState' }
        '401': { description: Authentication required }
        '403': { description: compliance:read scope required for API keys }
  /api/v1/due-diligence/cases:
    post:
      operationId: createDueDiligenceCase
      summary: Create a KYC or KYB case from the canonical customer type
      description: Individual customers produce KYC cases and business customers produce KYB cases. Only one non-expired active case is allowed per customer.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [customerId]
              properties:
                customerId: { type: string, format: uuid }
                expiresInDays: { type: integer, minimum: 7, maximum: 365, default: 90 }
      responses:
        '201':
          description: Case created with a versioned policy snapshot
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  case: { $ref: '#/components/schemas/DueDiligenceCase' }
                  replayed: { type: boolean }
        '400': { description: Invalid case input }
        '404': { description: Customer not found in this tenant }
        '409': { description: Active case exists or idempotency key payload differs }
  /api/v1/due-diligence/cases/{id}:
    get:
      operationId: retrieveDueDiligenceCase
      summary: Retrieve one KYC/KYB case with parties, append-only checks and lifecycle events
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Hydrated due-diligence case
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/DueDiligenceCase' }
        '404': { description: Case not found in this tenant }
  /api/v1/due-diligence/cases/{id}/parties:
    post:
      operationId: addDueDiligenceParty
      summary: Append a subject or KYB related party while the case is in draft
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - { $ref: '#/components/parameters/ResourceId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [role, name, taxId]
              properties:
                role: { type: string, enum: [subject, legal_representative, beneficial_owner, director] }
                name: { type: string, minLength: 2, maxLength: 160 }
                taxId: { type: string, minLength: 4, writeOnly: true, description: Only the final four alphanumeric characters are retained. }
                ownershipPercentage: { type: number, exclusiveMinimum: 0, maximum: 100 }
                pepDeclared: { type: boolean, default: false }
      responses:
        '201': { description: Party appended; total beneficial ownership cannot exceed 100 percent }
        '400': { description: Invalid party input }
        '409': { description: Invalid role, non-draft case, ownership exceeded or idempotency mismatch }
  /api/v1/due-diligence/cases/{id}/checks:
    post:
      operationId: recordDueDiligenceCheck
      summary: Append an evidence-backed KYC/KYB check while the case is in draft
      description: Check history is append-only. New checks supersede older observations by check type without mutating prior evidence.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - { $ref: '#/components/parameters/ResourceId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [checkType, source, status, resultCode, note]
              properties:
                checkType: { $ref: '#/components/schemas/DueDiligenceCheckType' }
                source: { type: string, enum: [manual_review, official_registry, internal_list] }
                status: { type: string, enum: [pending, passed, failed, review] }
                resultCode: { type: string, minLength: 2, maxLength: 80 }
                note: { type: string, minLength: 3, maxLength: 1000 }
                evidenceDocumentId: { type: string, format: uuid }
      responses:
        '201': { description: Check appended and audit event emitted }
        '404': { description: Case or tenant-scoped evidence document not found }
        '409': { description: Case is not draft or idempotency key payload differs }
  /api/v1/due-diligence/cases/{id}/submit:
    post:
      operationId: submitDueDiligenceCase
      summary: Submit a complete draft to the independent review queue
      description: Every required check must have a non-pending observation; KYB also requires a legal representative and beneficial owner.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - { $ref: '#/components/parameters/ResourceId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      responses:
        '200': { description: Case moved to in_review or idempotent result replayed }
        '409': { description: Requirements are missing, case is expired or no longer draft }
  /api/v1/due-diligence/cases/{id}/decide:
    post:
      operationId: decideDueDiligenceCase
      summary: Approve or reject an in-review case under mandatory maker-checker control
      description: Session-only decision. Requires an owner or admin with MFA who is different from the submitter. Approval also requires every latest required check to have passed.
      security: [{ session: [] }]
      parameters:
        - { $ref: '#/components/parameters/ResourceId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [decision, riskRating, note]
              properties:
                decision: { type: string, enum: [approve, reject] }
                riskRating: { type: string, enum: [low, medium, high, prohibited] }
                note: { type: string, minLength: 3, maxLength: 1000 }
      responses:
        '200': { description: Independent decision recorded with audit and webhook events }
        '403': { description: Owner or admin with MFA required; API keys are not accepted }
        '409': { description: Self-decision, non-passing checks, invalid state or idempotency mismatch }
  /api/v1/due-diligence/cases/{id}/cancel:
    post:
      operationId: cancelDueDiligenceCase
      summary: Cancel a draft or in-review KYC/KYB case with a reason
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - { $ref: '#/components/parameters/ResourceId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [note]
              properties:
                note: { type: string, minLength: 3, maxLength: 1000 }
      responses:
        '200': { description: Case cancelled or idempotent result replayed }
        '409': { description: Case is already terminal }
  /api/v1/compliance/documents:
    post:
      operationId: uploadComplianceDocument
      summary: Upload private compliance evidence
      security: [{ session: [] }, { bearerApiKey: [] }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        '201': { description: Document stored }
        '400': { description: 'File must be PDF, JPG or PNG up to 5 MB with a valid signature' }
  /api/platform/access:
    get:
      operationId: listOrganizationAccess
      summary: List tenant members and invitation history for owner or admin
      security: [{ session: [] }]
      responses:
        '200':
          description: Effective organization access state
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      members: { type: array, items: { $ref: '#/components/schemas/OrganizationMember' } }
                      invitations: { type: array, items: { $ref: '#/components/schemas/OrganizationInvitation' } }
        '403': { description: Owner or admin role required }
    post:
      operationId: inviteOrganizationMember
      summary: Invite an email to the tenant with an assignable role
      description: The invitation expires after seven days. A verified email is required before membership is accepted automatically.
      security: [{ session: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, role]
              properties:
                email: { type: string, format: email, maxLength: 254 }
                role: { type: string, enum: [admin, operator, viewer] }
      responses:
        '201': { description: Invitation created or safely reissued }
        '403': { description: Role hierarchy violation }
        '409': { description: Email is already assigned or has another active invitation }
  /api/platform/access/members/{id}:
    patch:
      operationId: updateOrganizationMemberRole
      summary: Change a member role within the owner/admin hierarchy
      security: [{ session: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role: { type: string, enum: [admin, operator, viewer] }
      responses:
        '200': { description: Role changed and audit event emitted }
        '403': { description: Role hierarchy violation }
        '409': { description: Owner or own role is protected }
    delete:
      operationId: removeOrganizationMember
      summary: Remove a member from the tenant without deleting their identity
      security: [{ session: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Membership removed and audit event emitted }
        '403': { description: Role hierarchy violation }
        '409': { description: Owner or own membership is protected }
  /api/platform/access/invitations/{id}:
    delete:
      operationId: revokeOrganizationInvitation
      summary: Revoke a pending organization invitation
      security: [{ session: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200': { description: Invitation revoked }
        '409': { description: Invitation is no longer pending }
  /api/platform/approval-policy:
    get:
      operationId: getApprovalPolicies
      summary: Read dual-control policies for money movement and sensitive operational decisions
      security: [{ session: [] }]
      responses:
        '200': { description: Effective policies for the current tenant role }
    patch:
      operationId: configureApprovalPolicy
      summary: Enable or disable dual approval as owner with MFA
      description: Enabling requires another owner/admin with MFA. Disabling is blocked while requests are pending.
      security: [{ session: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled, expiresInMinutes]
              properties:
                actionType: { type: string, enum: [settlement.execute, transfer.create, payment.create, payment.reverse, payout_batch.execute, risk.case.resolve, reconciliation.exception.resolve, dispute.resolve], default: settlement.execute }
                enabled: { type: boolean }
                expiresInMinutes: { type: integer, minimum: 15, maximum: 10080 }
      responses:
        '200': { description: Policy updated and audit event emitted }
        '403': { description: Owner role and MFA required }
        '409': { description: No independent checker exists or requests remain pending }
  /api/v1/support/cases:
    get:
      operationId: listSupportCases
      summary: List the tenant support cases with their status and message count
      description: Support cases are tenant-scoped. A viewer can read them; opening or replying requires an operator role or the support:write scope.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Support cases ordered by last activity
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/SupportCase' } }
        '401': { description: Authentication required }
        '403': { description: Missing support:read scope or role }
    post:
      operationId: createSupportCase
      summary: Open a support case with its first message
      description: Limited to 20 open cases per tenant. Emits an audit event and a support.case_opened webhook.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category, subject, message]
              additionalProperties: false
              properties:
                category: { type: string, enum: [sandbox, api, console, compliance, commercial, other] }
                subject: { type: string, minLength: 4, maxLength: 140 }
                message: { type: string, minLength: 8, maxLength: 4000 }
      responses:
        '201': { description: Support case opened }
        '200': { description: Idempotently replayed case }
        '400': { description: Invalid support case payload }
        '409': { description: The tenant reached the open case limit }
  /api/v1/support/cases/{id}:
    get:
      operationId: retrieveSupportCase
      summary: Retrieve a support case with its full conversation
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Support case and ordered messages
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [case, messages]
                    properties:
                      case: { $ref: '#/components/schemas/SupportCase' }
                      messages: { type: array, items: { $ref: '#/components/schemas/SupportMessage' } }
        '404': { description: Support case not found in tenant }
    patch:
      operationId: updateSupportCaseStatus
      summary: Move a support case to another status
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              additionalProperties: false
              properties:
                status: { type: string, enum: [open, pending_cimbra, pending_tenant, resolved, closed] }
      responses:
        '200': { description: Status updated or already in that status }
        '400': { description: Invalid status }
        '404': { description: Support case not found in tenant }
  /api/v1/support/cases/{id}/messages:
    post:
      operationId: replySupportCase
      summary: Add a tenant message to an open support case
      description: Moves the case to pending_cimbra. Closed and resolved cases reject new messages.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              additionalProperties: false
              properties:
                body: { type: string, minLength: 3, maxLength: 4000 }
      responses:
        '201': { description: Message added }
        '200': { description: Idempotently replayed message }
        '409': { description: The case is closed or reached the message limit }
  /api/v1/organization:
    get:
      operationId: retrieveOrganization
      summary: Retrieve the tenant profile and member count
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Organization profile
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Organization' }
        '403': { description: Missing organization:read scope or role }
    patch:
      operationId: updateOrganization
      summary: Update the tenant display name or country
      description: Requires an owner or admin role, or the organization:write scope. Emits an audit event and an organization.updated webhook.
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              additionalProperties: false
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                country: { type: string, enum: [AR, MX, CO, BR, CL, PE] }
      responses:
        '200': { description: Organization updated or unchanged }
        '400': { description: Invalid organization payload }
        '403': { description: Owner or admin role required }
  /api/v1/services:
    get:
      operationId: getServiceTopology
      summary: Return the service catalog, ownership boundaries and extraction debt
      description: Every table and data-access module belongs to exactly one domain service. Services run in a shared runtime until a documented volume or risk gate justifies extracting them, and the cross-service write debt is reported explicitly.
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200':
          description: Service topology with per-service extraction readiness
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/ServiceTopology' }
        '403': { description: Missing platform:read scope or console role }
  /api/ops/overview:
    get:
      operationId: getPlatformOverview
      summary: Platform superadmin overview across every tenant
      description: Restricted to platform operators provisioned in CIMBRA_PLATFORM_OPERATOR_EMAILS. Sessions only, never API keys.
      security: [{ session: [] }]
      responses:
        '200': { description: Tenants, leads, support queue, service topology and live readiness }
        '403': { description: Caller is not a provisioned platform operator }
  /api/ops/leads/{id}:
    patch:
      operationId: updatePlatformLead
      summary: Move a commercial lead through its pipeline status
      security: [{ session: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              additionalProperties: false
              properties:
                status: { type: string, enum: [new, contacted, qualified, closed] }
      responses:
        '200': { description: Lead status updated }
        '404': { description: Lead not found }
  /api/ops/support/cases/{id}:
    get:
      operationId: retrievePlatformSupportCase
      summary: Retrieve any tenant support case with its conversation
      description: Restricted to platform operators provisioned in CIMBRA_PLATFORM_OPERATOR_EMAILS. Sessions only.
      security: [{ session: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      responses:
        '200':
          description: Support case and ordered messages
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [case, messages]
                    properties:
                      case: { $ref: '#/components/schemas/SupportCase' }
                      messages: { type: array, items: { $ref: '#/components/schemas/SupportMessage' } }
        '403': { description: Caller is not a provisioned platform operator }
        '404': { description: Support case not found }
    patch:
      operationId: updatePlatformSupportCase
      summary: Move any tenant support case to another status
      security: [{ session: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              additionalProperties: false
              properties:
                status: { type: string, enum: [open, pending_cimbra, pending_tenant, resolved, closed] }
      responses:
        '200': { description: Status updated }
        '404': { description: Support case not found }
  /api/ops/support/cases/{id}/messages:
    post:
      operationId: replyPlatformSupportCase
      summary: Answer a tenant support case as Cimbra
      description: Adds a platform-authored message and moves the case to pending_tenant.
      security: [{ session: [] }]
      parameters: [{ $ref: '#/components/parameters/ResourceId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              additionalProperties: false
              properties:
                body: { type: string, minLength: 3, maxLength: 4000 }
      responses:
        '201': { description: Platform reply added }
        '409': { description: The case is closed or reached the message limit }
  /api/platform/api-keys:
    get:
      operationId: listApiKeys
      summary: List safe API key metadata
      security: [{ session: [] }]
      responses:
        '200': { description: API key list without secrets }
    post:
      operationId: createApiKey
      summary: Create an S2S API key; the secret is returned once
      security: [{ session: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scopes]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                scopes:
                  type: array
                  minItems: 1
                  uniqueItems: true
                  items: { type: string, enum: ['customers:read', 'customers:write', 'accounts:read', 'accounts:write', 'cards:read', 'cards:write', 'transfers:read', 'transfers:write', 'payments:read', 'payments:write', 'payouts:read', 'payouts:write', 'billers:read', 'billers:write', 'wallets:read', 'wallets:write', 'risk:read', 'risk:write', 'disputes:read', 'disputes:write', 'reconciliation:read', 'reconciliation:write', 'settlements:read', 'settlements:write', 'approvals:read', 'operations:read', 'operations:write', 'support:read', 'support:write', 'organization:read', 'organization:write', 'platform:read', 'ledger:read', 'events:read', 'compliance:read', 'compliance:write', 'webhooks:manage'] }
                expiresInDays: { type: integer, minimum: 1, maximum: 365 }
      responses:
        '201': { description: API key created and secret returned once }
  /api/platform/api-keys/{id}:
    delete:
      operationId: revokeApiKey
      summary: Revoke an API key immediately
      security: [{ session: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: API key revoked }
        '404': { description: Active key not found }
  /api/platform/api-keys/{id}/rotate:
    post:
      operationId: rotateApiKey
      summary: Atomically replace and revoke an API key
      security: [{ session: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '201': { description: Replacement key created and old key revoked }
  /api/v1/webhooks:
    get:
      operationId: listWebhooks
      summary: List endpoints, deliveries and attempt logs
      security: [{ session: [] }, { bearerApiKey: [] }]
      responses:
        '200': { description: Webhook operational state }
    post:
      operationId: createWebhook
      summary: Register a public HTTPS webhook and return its signing secret once
      security: [{ session: [] }, { bearerApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url, eventTypes]
              properties:
                name: { type: string, minLength: 2, maxLength: 80 }
                url: { type: string, format: uri, maxLength: 2048 }
                eventTypes:
                  type: array
                  minItems: 1
                  uniqueItems: true
                  items: { type: string }
      responses:
        '201': { description: Webhook created and signing secret returned once }
        '400': { description: 'Invalid, private or non-HTTPS destination' }
  /api/v1/webhooks/{id}:
    delete:
      operationId: disableWebhook
      summary: Disable an endpoint and cancel pending deliveries
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Webhook disabled }
  /api/v1/webhooks/{id}/rotate:
    post:
      operationId: rotateWebhookSecret
      summary: Rotate a webhook signing secret
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Signing secret rotated and returned once }
  /api/v1/webhooks/deliveries/{id}/replay:
    post:
      operationId: replayWebhookDelivery
      summary: Requeue a webhook delivery while retaining prior attempt logs
      security: [{ session: [] }, { bearerApiKey: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Delivery requeued }
components:
  parameters:
    ResourceId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
    WorkItemType:
      in: path
      name: type
      required: true
      schema: { type: string, enum: [risk-case, reconciliation-exception, dispute] }
    PageLimit:
      in: query
      name: limit
      required: false
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    PageCursor:
      in: query
      name: cursor
      required: false
      description: Opaque cursor returned as nextCursor by the previous page.
      schema: { type: string }
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      description: Stable unique key for this logical write. Replays return the original resource; reusing it with a different payload returns 409.
      schema: { type: string, minLength: 8, maxLength: 100, pattern: '^[A-Za-z0-9._:-]+$' }
  schemas:
    DueDiligenceCheckType:
      type: string
      enum: [identity_document, address, sanctions, pep, business_registry, beneficial_ownership]
    DueDiligenceParty:
      type: object
      additionalProperties: false
      required: [id, caseId, role, name, taxIdLast4, ownershipPercentage, pepDeclared, createdBy, createdByName, createdAt]
      properties:
        id: { type: string, format: uuid }
        caseId: { type: string, format: uuid }
        role: { type: string, enum: [subject, legal_representative, beneficial_owner, director] }
        name: { type: string }
        taxIdLast4: { type: string, minLength: 4, maxLength: 4 }
        ownershipPercentage: { type: [number, 'null'], minimum: 0, maximum: 100 }
        pepDeclared: { type: boolean }
        createdBy: { type: string, format: uuid }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
    DueDiligenceCheck:
      type: object
      additionalProperties: false
      required: [id, caseId, checkType, source, status, resultCode, note, evidenceDocumentId, evidenceFileName, checkedBy, checkedByName, createdAt]
      properties:
        id: { type: string, format: uuid }
        caseId: { type: string, format: uuid }
        checkType: { $ref: '#/components/schemas/DueDiligenceCheckType' }
        source: { type: string, enum: [manual_review, official_registry, internal_list] }
        status: { type: string, enum: [pending, passed, failed, review] }
        resultCode: { type: string }
        note: { type: string }
        evidenceDocumentId: { type: [string, 'null'], format: uuid }
        evidenceFileName: { type: [string, 'null'] }
        checkedBy: { type: string, format: uuid }
        checkedByName: { type: string }
        createdAt: { type: string, format: date-time }
    DueDiligenceEvent:
      type: object
      additionalProperties: false
      required: [id, caseId, event, fromStatus, toStatus, payload, actorId, actorName, createdAt]
      properties:
        id: { type: string, format: uuid }
        caseId: { type: string, format: uuid }
        event: { type: string, enum: [created, submitted, approved, rejected, cancelled, expired] }
        fromStatus: { type: [string, 'null'], enum: [draft, in_review, approved, rejected, cancelled, expired, null] }
        toStatus: { type: string, enum: [draft, in_review, approved, rejected, cancelled, expired] }
        payload: { type: object, additionalProperties: true }
        actorId: { type: string, format: uuid }
        actorName: { type: string }
        createdAt: { type: string, format: date-time }
    DueDiligenceCase:
      type: object
      additionalProperties: false
      required: [id, customerId, customerName, customerType, country, taxIdLast4, kind, jurisdiction, policyVersion, requiredChecks, completedRequiredChecks, readyForReview, status, riskRating, expiresAt, createdBy, createdByName, submittedBy, submittedByName, submittedAt, resolvedBy, resolvedByName, resolutionNote, resolvedAt, createdAt, updatedAt, parties, checks, events]
      properties:
        id: { type: string, format: uuid }
        customerId: { type: string, format: uuid }
        customerName: { type: string }
        customerType: { type: string, enum: [individual, business] }
        country: { type: string, pattern: '^[A-Z]{2}$' }
        taxIdLast4: { type: string, minLength: 4, maxLength: 4 }
        kind: { type: string, enum: [kyc, kyb] }
        jurisdiction: { type: string, pattern: '^[A-Z]{2}$' }
        policyVersion: { type: string }
        requiredChecks: { type: array, items: { $ref: '#/components/schemas/DueDiligenceCheckType' } }
        completedRequiredChecks: { type: integer, minimum: 0 }
        readyForReview: { type: boolean }
        status: { type: string, enum: [draft, in_review, approved, rejected, cancelled, expired] }
        riskRating: { type: string, enum: [unassessed, low, medium, high, prohibited] }
        expiresAt: { type: string, format: date-time }
        createdBy: { type: string, format: uuid }
        createdByName: { type: string }
        submittedBy: { type: [string, 'null'], format: uuid }
        submittedByName: { type: [string, 'null'] }
        submittedAt: { type: [string, 'null'], format: date-time }
        resolvedBy: { type: [string, 'null'], format: uuid }
        resolvedByName: { type: [string, 'null'] }
        resolutionNote: { type: [string, 'null'] }
        resolvedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        parties: { type: array, items: { $ref: '#/components/schemas/DueDiligenceParty' } }
        checks: { type: array, items: { $ref: '#/components/schemas/DueDiligenceCheck' } }
        events: { type: array, items: { $ref: '#/components/schemas/DueDiligenceEvent' } }
    DueDiligenceState:
      type: object
      additionalProperties: false
      required: [policy, metrics, cases, customers, documents]
      properties:
        policy:
          type: object
          required: [version, kycRequiredChecks, kybRequiredChecks, boundary]
          properties:
            version: { type: string }
            kycRequiredChecks: { type: array, items: { $ref: '#/components/schemas/DueDiligenceCheckType' } }
            kybRequiredChecks: { type: array, items: { $ref: '#/components/schemas/DueDiligenceCheckType' } }
            boundary: { type: string }
        metrics:
          type: object
          required: [total, drafts, inReview, approved, rejected]
          properties:
            total: { type: integer, minimum: 0 }
            drafts: { type: integer, minimum: 0 }
            inReview: { type: integer, minimum: 0 }
            approved: { type: integer, minimum: 0 }
            rejected: { type: integer, minimum: 0 }
        cases: { type: array, items: { $ref: '#/components/schemas/DueDiligenceCase' } }
        customers:
          type: array
          items:
            type: object
            required: [id, type, name, country, taxIdLast4, status, createdAt]
            properties:
              id: { type: string, format: uuid }
              type: { type: string, enum: [individual, business] }
              name: { type: string }
              country: { type: string }
              taxIdLast4: { type: string }
              status: { type: string }
              createdAt: { type: string, format: date-time }
        documents:
          type: array
          items:
            type: object
            required: [id, fileName, contentType, size, status, createdAt]
            properties:
              id: { type: string, format: uuid }
              fileName: { type: string }
              contentType: { type: string }
              size: { type: integer, minimum: 0 }
              status: { type: string }
              createdAt: { type: string, format: date-time }
    PayoutBeneficiary:
      type: object
      additionalProperties: false
      required: [id, externalReference, name, entityType, country, currency, destinationType, destinationLast4, bankCode, status, createdBy, createdByName, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        externalReference: { type: string }
        name: { type: string }
        entityType: { type: string, enum: [individual, business] }
        country: { type: string, pattern: '^[A-Z]{2}$' }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        destinationType: { type: string, enum: [local_account, alias, iban, clabe, pix_key] }
        destinationLast4: { type: string, minLength: 4, maxLength: 4 }
        bankCode: { type: [string, 'null'] }
        status: { type: string, enum: [active, suspended] }
        createdBy: { type: string, format: uuid }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    CreatePayoutBeneficiaryInput:
      type: object
      additionalProperties: false
      required: [externalReference, name, entityType, country, currency, destinationType, destination]
      properties:
        externalReference: { type: string, minLength: 2, maxLength: 100 }
        name: { type: string, minLength: 2, maxLength: 160 }
        entityType: { type: string, enum: [individual, business] }
        country: { type: string, pattern: '^[A-Za-z]{2}$' }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        destinationType: { type: string, enum: [local_account, alias, iban, clabe, pix_key] }
        destination: { type: string, minLength: 4, maxLength: 160, writeOnly: true, description: Converted to a tenant-scoped one-way digest before persistence. }
        bankCode: { type: string, minLength: 2, maxLength: 40 }
    PayoutItem:
      type: object
      additionalProperties: false
      required: [id, batchId, beneficiaryId, beneficiaryName, beneficiaryReference, destinationType, destinationLast4, externalReference, amountMinor, amount, currency, description, status, transactionId, failureCode, failureMessage, attemptCount, processedAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        batchId: { type: string, format: uuid }
        beneficiaryId: { type: string, format: uuid }
        beneficiaryName: { type: string }
        beneficiaryReference: { type: string }
        destinationType: { type: string, enum: [local_account, alias, iban, clabe, pix_key] }
        destinationLast4: { type: string, minLength: 4, maxLength: 4 }
        externalReference: { type: string }
        amountMinor: { type: string, pattern: '^\d+$' }
        amount: { type: number }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        description: { type: string }
        status: { type: string, enum: [pending, processing, review, settled, failed, cancelled] }
        transactionId: { type: [string, 'null'], format: uuid }
        failureCode: { type: [string, 'null'] }
        failureMessage: { type: [string, 'null'] }
        attemptCount: { type: integer, minimum: 0, maximum: 3 }
        processedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    PayoutBatch:
      type: object
      additionalProperties: false
      required: [id, sourceAccountId, sourceAccountReference, externalReference, description, currency, status, totalAmountMinor, totalAmount, itemCount, scheduledFor, processBefore, submittedAt, startedAt, completedAt, cancelledAt, createdBy, createdByName, createdAt, updatedAt, items]
      properties:
        id: { type: string, format: uuid }
        sourceAccountId: { type: string, format: uuid }
        sourceAccountReference: { type: string }
        externalReference: { type: string }
        description: { type: string }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        status: { type: string, enum: [draft, pending_approval, scheduled, processing, requires_attention, completed, partially_failed, failed, cancelled] }
        totalAmountMinor: { type: string, pattern: '^\d+$' }
        totalAmount: { type: number }
        itemCount: { type: integer, minimum: 1, maximum: 100 }
        scheduledFor: { type: [string, 'null'], format: date-time }
        processBefore: { type: [string, 'null'], format: date-time }
        submittedAt: { type: [string, 'null'], format: date-time }
        startedAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        cancelledAt: { type: [string, 'null'], format: date-time }
        createdBy: { type: string, format: uuid }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        items: { type: array, minItems: 1, maxItems: 100, items: { $ref: '#/components/schemas/PayoutItem' } }
    CreatePayoutBatchInput:
      type: object
      additionalProperties: false
      required: [sourceAccountId, externalReference, description, currency, items]
      properties:
        sourceAccountId: { type: string, format: uuid }
        externalReference: { type: string, minLength: 2, maxLength: 100 }
        description: { type: string, minLength: 2, maxLength: 240 }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        scheduledFor: { type: string, format: date-time, description: Optional start time, at most 365 days ahead. }
        processBefore: { type: string, format: date-time, description: Optional processing deadline strictly after the effective start. }
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required: [externalReference, beneficiaryId, amount, description]
            properties:
              externalReference: { type: string, minLength: 2, maxLength: 100 }
              beneficiaryId: { type: string, format: uuid }
              amount: { type: string, pattern: '^\d+(?:\.\d+)?$' }
              description: { type: string, minLength: 2, maxLength: 240 }
    Biller:
      type: object
      additionalProperties: false
      description: Tenant-owned service catalog record. It is not a proxy to another fintech.
      required: [id, code, name, country, category, serviceType, currency, amountMode, minAmountMinor, minAmount, maxAmountMinor, maxAmount, status, contractReference, createdBy, createdByName, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        country: { type: string, pattern: '^[A-Z]{2}$' }
        category: { type: string, enum: [utilities, telecom, tax, education, health, insurance, transport, entertainment, other] }
        serviceType: { type: string, enum: [bill_payment, mobile_topup, gift_card] }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        amountMode: { type: string, enum: [exact, range, fixed] }
        minAmountMinor: { type: [string, 'null'] }
        minAmount: { type: [number, 'null'] }
        maxAmountMinor: { type: [string, 'null'] }
        maxAmount: { type: [number, 'null'] }
        status: { type: string, enum: [active, suspended] }
        contractReference: { type: [string, 'null'] }
        createdBy: { type: string, format: uuid }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    CreateBillerInput:
      type: object
      additionalProperties: false
      required: [code, name, country, category, serviceType, currency, amountMode]
      properties:
        code: { type: string, minLength: 2, maxLength: 40, pattern: '^[A-Za-z0-9_-]+$' }
        name: { type: string, minLength: 2, maxLength: 160 }
        country: { type: string, pattern: '^[A-Za-z]{2}$' }
        category: { type: string, enum: [utilities, telecom, tax, education, health, insurance, transport, entertainment, other] }
        serviceType: { type: string, enum: [bill_payment, mobile_topup, gift_card] }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        amountMode:
          type: string
          enum: [exact, range, fixed]
          description: bill_payment requires exact; top-ups and gift cards require range or fixed.
        minAmount: { type: string, pattern: '^\d+(?:\.\d+)?$' }
        maxAmount: { type: string, pattern: '^\d+(?:\.\d+)?$' }
        contractReference: { type: string, minLength: 3, maxLength: 120 }
    BillerObligation:
      type: object
      additionalProperties: false
      required: [id, billerId, billerName, externalReference, subscriberReferenceLast4, amountMinor, amount, currency, dueAt, description, status, paidAt, createdByName, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        billerId: { type: string, format: uuid }
        billerName: { type: string }
        externalReference: { type: string }
        subscriberReferenceLast4: { type: string, minLength: 4, maxLength: 4 }
        amountMinor: { type: string, pattern: '^\d+$' }
        amount: { type: number }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        dueAt: { type: string, format: date-time }
        description: { type: string }
        status: { type: string, enum: [open, paid, cancelled, expired] }
        paidAt: { type: [string, 'null'], format: date-time }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    CreateBillerObligationInput:
      type: object
      additionalProperties: false
      required: [externalReference, subscriberReference, amount, dueAt, description]
      properties:
        externalReference: { type: string, minLength: 2, maxLength: 100 }
        subscriberReference:
          type: string
          minLength: 4
          maxLength: 120
          description: Normalized and hashed tenant-locally; only the last four characters are exposed.
        amount: { type: string, pattern: '^\d+(?:\.\d+)?$' }
        dueAt: { type: string, format: date-time }
        description: { type: string, minLength: 2, maxLength: 240 }
    BillPaymentOrder:
      type: object
      additionalProperties: false
      required: [id, billerId, billerName, accountId, accountReference, obligationId, mandateId, transactionId, reversalTransactionId, serviceType, destinationReferenceLast4, amountMinor, amount, currency, status, failureCode, createdByName, createdAt, updatedAt, settledAt, reversedAt]
      properties:
        id: { type: string, format: uuid }
        billerId: { type: string, format: uuid }
        billerName: { type: string }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        obligationId: { type: [string, 'null'], format: uuid }
        mandateId: { type: [string, 'null'], format: uuid }
        transactionId: { type: [string, 'null'], format: uuid }
        reversalTransactionId: { type: [string, 'null'], format: uuid }
        serviceType: { type: string, enum: [bill_payment, mobile_topup, gift_card] }
        destinationReferenceLast4: { type: string, minLength: 4, maxLength: 4 }
        amountMinor: { type: string, pattern: '^\d+$' }
        amount: { type: number }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        status: { type: string, enum: [declined, review, settled, reversed, cancelled] }
        failureCode: { type: [string, 'null'] }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        settledAt: { type: [string, 'null'], format: date-time }
        reversedAt: { type: [string, 'null'], format: date-time }
    CreateBillPaymentInput:
      type: object
      additionalProperties: false
      required: [accountId, billerId]
      description: bill_payment requires obligationId. mobile_topup and gift_card require destinationReference and amount.
      properties:
        accountId: { type: string, format: uuid }
        billerId: { type: string, format: uuid }
        obligationId: { type: string, format: uuid }
        destinationReference: { type: string, minLength: 4, maxLength: 120 }
        amount: { type: string, pattern: '^\d+(?:\.\d+)?$' }
    RecurringPaymentMandate:
      type: object
      additionalProperties: false
      required: [id, billerId, billerName, serviceType, currency, accountId, accountReference, subscriberReferenceLast4, frequency, amountMinor, amount, amountLimitMinor, amountLimit, consentReference, consentedAt, status, nextChargeAt, lastExecutedAt, retryCount, maxRetries, cancelledAt, createdBy, createdByName, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        billerId: { type: string, format: uuid }
        billerName: { type: string }
        serviceType: { type: string, enum: [bill_payment, mobile_topup, gift_card] }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        subscriberReferenceLast4: { type: string, minLength: 4, maxLength: 4 }
        frequency: { type: string, enum: [weekly, monthly] }
        amountMinor: { type: [string, 'null'] }
        amount: { type: [number, 'null'] }
        amountLimitMinor: { type: string, pattern: '^\d+$' }
        amountLimit: { type: number }
        consentReference: { type: string }
        consentedAt: { type: string, format: date-time }
        status: { type: string, enum: [active, paused, cancelled, expired] }
        nextChargeAt: { type: string, format: date-time }
        lastExecutedAt: { type: [string, 'null'], format: date-time }
        retryCount: { type: integer, minimum: 0 }
        maxRetries: { type: integer, minimum: 0, maximum: 10 }
        cancelledAt: { type: [string, 'null'], format: date-time }
        createdBy: { type: string, format: uuid }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    RecurringPaymentExecution:
      type: object
      additionalProperties: false
      required: [id, mandateId, orderId, scheduledFor, attemptNumber, status, errorCode, attemptedAt]
      properties:
        id: { type: string, format: uuid }
        mandateId: { type: string, format: uuid }
        orderId: { type: [string, 'null'], format: uuid }
        scheduledFor: { type: string, format: date-time }
        attemptNumber: { type: integer, minimum: 1 }
        status: { type: string, enum: [settled, review, declined, skipped_no_debt, failed] }
        errorCode: { type: [string, 'null'] }
        attemptedAt: { type: string, format: date-time }
    CreateRecurringPaymentMandateInput:
      type: object
      additionalProperties: false
      required: [accountId, billerId, subscriberReference, frequency, amountLimit, consentReference, consentedAt, nextChargeAt]
      properties:
        accountId: { type: string, format: uuid }
        billerId: { type: string, format: uuid }
        subscriberReference: { type: string, minLength: 4, maxLength: 120 }
        frequency: { type: string, enum: [weekly, monthly] }
        amount:
          type: string
          pattern: '^\d+(?:\.\d+)?$'
          description: Required for mobile_topup and gift_card; omitted for debt-based bill_payment mandates.
        amountLimit: { type: string, pattern: '^\d+(?:\.\d+)?$' }
        consentReference: { type: string, minLength: 3, maxLength: 120 }
        consentedAt: { type: string, format: date-time }
        nextChargeAt: { type: string, format: date-time }
        maxRetries: { type: integer, minimum: 0, maximum: 10, default: 3 }
    Dispute:
      type: object
      required: [id, transactionId, reason, description, amountMinor, amount, currency, status, open, priority, provisionalCreditRequested, creditStatus, possibleEvents, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        transactionId: { type: string, format: uuid }
        reason: { type: string, enum: [card_not_present, duplicate, amount_mismatch, service_not_received, credit_not_processed, cash_not_received, other] }
        description: { type: string }
        amountMinor: { type: string }
        amount: { type: number }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        status: { type: string, enum: [opened, under_review, network_ready, won, lost, rejected, cancelled] }
        open: { type: boolean }
        priority: { type: string, enum: [low, medium, high, critical] }
        provisionalCreditRequested: { type: boolean }
        creditStatus: { type: string, enum: [none, posted, final, reversed] }
        creditTransactionId: { type: [string, 'null'], format: uuid }
        creditReversalTransactionId: { type: [string, 'null'], format: uuid }
        possibleEvents: { type: array, items: { type: string, enum: [start_review, mark_network_ready, resolve_won, resolve_lost, reject, cancel] } }
        dueAt: { type: [string, 'null'], format: date-time }
        resolutionNote: { type: [string, 'null'] }
        resolvedAt: { type: [string, 'null'], format: date-time }
        originalTransaction: { type: object, additionalProperties: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    DisputeTimelineEvent:
      type: object
      required: [id, event, fromStatus, toStatus, note, actorId, actorName, createdAt]
      properties:
        id: { type: string, format: uuid }
        event: { type: string, enum: [created, start_review, mark_network_ready, resolve_won, resolve_lost, reject, cancel] }
        fromStatus: { type: [string, 'null'] }
        toStatus: { type: string }
        note: { type: string }
        actorId: { type: string, format: uuid }
        actorName: { type: string }
        createdAt: { type: string, format: date-time }
    Card:
      type: object
      required: [id, programId, programName, accountId, customerId, product, format, last4, status, statusReason, activatedAt, terminatedAt, createdAt, updatedAt]
      additionalProperties: false
      description: Sandbox card representation. It never contains PAN, CVV, track data or a network token.
      properties:
        id: { type: string, format: uuid }
        programId: { type: [string, 'null'], format: uuid }
        programName: { type: [string, 'null'] }
        accountId: { type: string, format: uuid }
        customerId: { type: string, format: uuid }
        product: { type: string, enum: [debit, credit, prepaid] }
        format: { type: string, enum: [virtual, physical] }
        last4: { type: string, pattern: '^\d{4}$' }
        status: { type: string, enum: [created, active, frozen, terminated] }
        statusReason: { type: [string, 'null'] }
        activatedAt: { type: [string, 'null'], format: date-time }
        terminatedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    CardProgram:
      type: object
      required: [id, name, product, formats, defaultCurrency, status, createdAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        product: { type: string, enum: [debit, credit, prepaid] }
        formats: { type: array, items: { type: string, enum: [virtual, physical] } }
        defaultCurrency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        status: { type: string, enum: [active, inactive] }
        createdAt: { type: string, format: date-time }
    CardLifecycleEvent:
      type: object
      required: [id, cardId, fromStatus, toStatus, reason, actorId, actorName, createdAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        cardId: { type: string, format: uuid }
        fromStatus: { type: [string, 'null'], enum: [created, active, frozen, terminated, null] }
        toStatus: { type: string, enum: [created, active, frozen, terminated] }
        reason: { type: string }
        actorId: { type: string, format: uuid }
        actorName: { type: string }
        createdAt: { type: string, format: date-time }
    CardControls:
      type: object
      required: [id, cardId, version, currency, perTransactionLimitMinor, perTransactionLimit, dailyLimitMinor, dailyLimit, monthlyLimitMinor, monthlyLimit, allowedChannels, allowedMccs, blockedMccs, status, createdBy, createdByName, createdAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        cardId: { type: string, format: uuid }
        version: { type: integer, minimum: 1 }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        perTransactionLimitMinor: { type: [string, 'null'] }
        perTransactionLimit: { type: [string, 'null'] }
        dailyLimitMinor: { type: [string, 'null'] }
        dailyLimit: { type: [string, 'null'] }
        monthlyLimitMinor: { type: [string, 'null'] }
        monthlyLimit: { type: [string, 'null'] }
        allowedChannels: { type: array, items: { type: string, enum: [ecommerce, contactless, chip, magstripe, atm] } }
        allowedMccs: { type: array, items: { type: string, pattern: '^\d{4}$' } }
        blockedMccs: { type: array, items: { type: string, pattern: '^\d{4}$' } }
        status: { type: string, enum: [active, inactive] }
        createdBy: { type: string, format: uuid }
        createdByName: { type: string }
        createdAt: { type: string, format: date-time }
    WalletProgram:
      type: object
      required: [id, name, displayName, supportUrl, termsUrl, accentColor, defaultCurrency, allowedCurrencies, pocketKinds, status, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        displayName: { type: string }
        supportUrl: { type: [string, 'null'] }
        termsUrl: { type: [string, 'null'] }
        accentColor: { type: [string, 'null'] }
        defaultCurrency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        allowedCurrencies: { type: array, items: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] } }
        pocketKinds: { type: array, items: { type: string, enum: [available, pending, rewards] } }
        status: { type: string, enum: [active, inactive] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    Wallet:
      type: object
      required: [id, programId, programName, programDisplayName, customerId, customerName, externalReference, status, statusReason, pocketCount, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        programId: { type: string, format: uuid }
        programName: { type: string }
        programDisplayName: { type: string }
        customerId: { type: string, format: uuid }
        customerName: { type: string }
        externalReference: { type: string }
        status: { type: string, enum: [active, frozen, closed] }
        statusReason: { type: [string, 'null'] }
        pocketCount: { type: integer, minimum: 1 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    WalletPocket:
      type: object
      required: [id, walletId, accountId, accountReference, kind, label, currency, status, balanceMinor, balance, createdAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        walletId: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        kind: { type: string, enum: [available, pending, rewards] }
        label: { type: string }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        status: { type: string }
        balanceMinor: { type: string }
        balance: { type: number }
        createdAt: { type: string, format: date-time }
    WalletLifecycleEvent:
      type: object
      required: [id, walletId, fromStatus, toStatus, reason, actorId, actorName, createdAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        walletId: { type: string, format: uuid }
        fromStatus: { type: [string, 'null'], enum: [active, frozen, closed, null] }
        toStatus: { type: string, enum: [active, frozen, closed] }
        reason: { type: string }
        actorId: { type: string, format: uuid }
        actorName: { type: string }
        createdAt: { type: string, format: date-time }
    RailInstrument:
      type: object
      required: [id, accountId, accountReference, customerName, kind, value, last4, holderName, taxIdLast4, status, createdAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        customerName: { type: string }
        kind: { type: string, enum: [cvu, alias] }
        value: { type: string }
        last4: { type: string }
        holderName: { type: string }
        taxIdLast4: { type: string }
        status: { type: string, enum: [active, revoked] }
        createdAt: { type: string, format: date-time }
    InstantTransfer:
      type: object
      required: [id, scheme, direction, sourceAccountId, sourceAccountReference, destinationAccountId, destinationAccountReference, counterpartyKind, counterpartyLast4, counterpartyHolderName, counterpartyTaxLast4, amountMinor, amount, currency, description, externalReference, status, rail, transactionId, reversalTransactionId, qrPayload, expiresAt, collectionTillId, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        scheme: { type: string, enum: [credit_push, debit_pull, qr_collect] }
        direction: { type: string, enum: [outbound, inbound, internal] }
        sourceAccountId: { type: [string, 'null'], format: uuid }
        sourceAccountReference: { type: [string, 'null'] }
        destinationAccountId: { type: [string, 'null'], format: uuid }
        destinationAccountReference: { type: [string, 'null'] }
        counterpartyKind: { type: string, enum: [cvu, cbu, alias] }
        counterpartyLast4: { type: string }
        counterpartyHolderName: { type: [string, 'null'] }
        counterpartyTaxLast4: { type: [string, 'null'] }
        amountMinor: { type: string }
        amount: { type: number }
        currency: { type: string, enum: [ARS] }
        description: { type: string }
        externalReference: { type: string }
        status: { type: string, enum: [pending, accepted, rejected, settled, returned, expired, cancelled] }
        rail: { type: string, enum: [cimbra_sandbox] }
        transactionId: { type: [string, 'null'], format: uuid }
        reversalTransactionId: { type: [string, 'null'], format: uuid }
        qrPayload: { type: [string, 'null'] }
        expiresAt: { type: [string, 'null'], format: date-time }
        collectionTillId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    PaymentQr:
      type: object
      required: [id, accountId, accountReference, amountMinor, amount, currency, description, payload, kind, owner, collectionTillId, status, expiresAt, paidTransferId, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        amountMinor: { type: [string, 'null'] }
        amount: { type: [number, 'null'] }
        currency: { type: string, enum: [ARS] }
        description: { type: string }
        payload: { type: string }
        kind: { type: string, enum: [dynamic, static, debt] }
        owner: { type: string, enum: [account, till] }
        collectionTillId: { type: [string, 'null'], format: uuid }
        status: { type: string, enum: [active, paid, expired, cancelled] }
        expiresAt: { type: [string, 'null'], format: date-time }
        paidTransferId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    QrSaleOrder:
      type: object
      required: [id, paymentQrId, qrPayload, accountId, accountReference, amountMinor, amount, currency, description, externalReference, status, expiresAt, paidTransferId, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        paymentQrId: { type: string, format: uuid }
        qrPayload: { type: string }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        amountMinor: { type: string }
        amount: { type: number }
        currency: { type: string, enum: [ARS] }
        description: { type: string }
        externalReference: { type: string }
        status: { type: string, enum: [pending, paid, expired, cancelled, superseded] }
        expiresAt: { type: string, format: date-time }
        paidTransferId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    QrDebt:
      type: object
      required: [id, accountId, accountReference, paymentQrId, payload, amountMinor, amount, currency, description, externalReference, status, expiresAt, paidTransferId, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        paymentQrId: { type: string, format: uuid }
        payload: { type: string }
        amountMinor: { type: string }
        amount: { type: number }
        currency: { type: string, enum: [ARS] }
        description: { type: string }
        externalReference: { type: string }
        status: { type: string, enum: [open, paid, expired, cancelled] }
        expiresAt: { type: string, format: date-time }
        paidTransferId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    PaymentLink:
      type: object
      required: [id, accountId, accountReference, customerName, amountMinor, amount, collectedMinor, collectedAmount, remainingMinor, remainingAmount, refundedMinor, refundedAmount, partiallyCollected, partiallyRefunded, checkoutUrl, currency, description, externalReference, allowedMethods, payload, qrDebtId, collectionTillId, qrPayload, cvu, items, credits, refunds, status, expiresAt, paidMethod, payerAccountId, payerAccountReference, transactionId, reversalTransactionId, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        customerName: { type: string }
        amountMinor: { type: string }
        amount: { type: number }
        collectedMinor: { type: string }
        collectedAmount: { type: number }
        remainingMinor: { type: string }
        remainingAmount: { type: number }
        refundedMinor: { type: string }
        refundedAmount: { type: number }
        partiallyCollected: { type: boolean }
        partiallyRefunded: { type: boolean }
        checkoutUrl: { type: string }
        currency: { type: string, enum: [ARS] }
        description: { type: string }
        externalReference: { type: string }
        allowedMethods:
          type: array
          items: { type: string, enum: [internal, sandbox_inbound, cimbra_qr, cimbra_cvu] }
        payload: { type: string }
        qrDebtId: { type: [string, 'null'], format: uuid }
        collectionTillId: { type: [string, 'null'], format: uuid }
        qrPayload: { type: [string, 'null'] }
        cvu: { type: [string, 'null'] }
        items:
          type: array
          items: { $ref: '#/components/schemas/PaymentLinkItem' }
        credits:
          type: array
          items: { $ref: '#/components/schemas/PaymentLinkCredit' }
        refunds:
          type: array
          items: { $ref: '#/components/schemas/PaymentLinkRefund' }
        status: { type: string, enum: [open, pending, paid, expired, cancelled, refunded] }
        expiresAt: { type: string, format: date-time }
        paidMethod: { type: [string, 'null'], enum: [internal, sandbox_inbound, cimbra_qr, cimbra_cvu] }
        payerAccountId: { type: [string, 'null'], format: uuid }
        payerAccountReference: { type: [string, 'null'] }
        transactionId: { type: [string, 'null'], format: uuid }
        reversalTransactionId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    PaymentLinkItemInput:
      type: object
      additionalProperties: false
      required: [description, amount]
      properties:
        description: { type: string, minLength: 2, maxLength: 180 }
        amount:
          oneOf:
            - { type: string, pattern: '^\d+(?:\.\d+)?$' }
            - { type: number, exclusiveMinimum: 0, maximum: 10000000 }
        quantity: { type: integer, minimum: 1, maximum: 9999 }
        code: { type: [string, 'null'], maxLength: 80 }
        additional: { type: [string, 'null'], maxLength: 180 }
    PaymentLinkItem:
      type: object
      additionalProperties: false
      required: [description, amountMinor, amount, quantity, code, additional]
      properties:
        description: { type: string }
        amountMinor: { type: string }
        amount: { type: number }
        quantity: { type: integer }
        code: { type: [string, 'null'] }
        additional: { type: [string, 'null'] }
    PaymentLinkCredit:
      type: object
      additionalProperties: false
      required: [id, amountMinor, amount, refundedMinor, refundedAmount, remainingMinor, remainingAmount, method, payerAccountId, transactionId, instantTransferId, createdAt]
      properties:
        id: { type: string, format: uuid }
        amountMinor: { type: string }
        amount: { type: number }
        refundedMinor: { type: string }
        refundedAmount: { type: number }
        remainingMinor: { type: string }
        remainingAmount: { type: number }
        method: { type: string, enum: [cimbra_cvu] }
        payerAccountId: { type: [string, 'null'], format: uuid }
        transactionId: { type: string, format: uuid }
        instantTransferId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
    PaymentLinkRefund:
      type: object
      additionalProperties: false
      required: [id, amountMinor, amount, creditId, transactionId, createdAt]
      properties:
        id: { type: string, format: uuid }
        amountMinor: { type: string }
        amount: { type: number }
        creditId: { type: [string, 'null'], format: uuid }
        transactionId: { type: string, format: uuid }
        createdAt: { type: string, format: date-time }
    CollectionTill:
      type: object
      required: [id, accountId, accountReference, customerName, name, externalReference, cvu, alias, aliasChangedAt, paymentQrId, qrPayload, presence, closedAmountOnly, status, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        accountReference: { type: string }
        customerName: { type: string }
        name: { type: string }
        externalReference: { type: string }
        cvu: { type: string }
        alias: { type: [string, 'null'] }
        aliasChangedAt: { type: [string, 'null'], format: date-time }
        paymentQrId: { type: [string, 'null'], format: uuid }
        qrPayload: { type: [string, 'null'] }
        presence: { type: string, enum: [present, not_present] }
        closedAmountOnly: { type: boolean }
        status: { type: string, enum: [active, disabled] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    Echeq:
      type: object
      required: [id, drawerAccountId, drawerAccountReference, drawerCustomerName, holderAccountId, holderAccountReference, amountMinor, amount, currency, description, externalReference, payload, toOrder, paymentDate, expiresAt, status, beneficiaryName, beneficiaryTaxLast4, endorsementCount, rejectReason, transactionId, rail, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        drawerAccountId: { type: string, format: uuid }
        drawerAccountReference: { type: string }
        drawerCustomerName: { type: string }
        holderAccountId: { type: [string, 'null'], format: uuid }
        holderAccountReference: { type: [string, 'null'] }
        amountMinor: { type: string }
        amount: { type: number }
        currency: { type: string, enum: [ARS] }
        description: { type: string }
        externalReference: { type: string }
        payload: { type: string }
        toOrder: { type: boolean }
        paymentDate: { type: string, format: date }
        expiresAt: { type: string, format: date-time }
        status: { type: string, enum: [issued, accepted, endorsed, pending, deposited, cancelled, returned, rejected, expired] }
        beneficiaryName: { type: string }
        beneficiaryTaxLast4: { type: string }
        endorsementCount: { type: integer }
        rejectReason: { type: [string, 'null'] }
        transactionId: { type: [string, 'null'], format: uuid }
        rail: { type: string, enum: [cimbra_sandbox] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    SupportCase:
      type: object
      required: [id, organizationId, openedBy, openedByName, category, subject, status, createdAt, updatedAt, messageCount]
      properties:
        id: { type: string, format: uuid }
        organizationId: { type: string, format: uuid }
        openedBy: { type: string, format: uuid }
        openedByName: { type: string }
        category: { type: string, enum: [sandbox, api, console, compliance, commercial, other] }
        subject: { type: string }
        status: { type: string, enum: [open, pending_cimbra, pending_tenant, resolved, closed] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        messageCount: { type: integer }
    SupportMessage:
      type: object
      required: [id, caseId, authorId, authorName, authorKind, body, createdAt]
      properties:
        id: { type: string, format: uuid }
        caseId: { type: string, format: uuid }
        authorId: { type: string, format: uuid }
        authorName: { type: string }
        authorKind: { type: string, enum: [tenant, platform], description: platform identifies a Cimbra operator reply }
        body: { type: string }
        createdAt: { type: string, format: date-time }
    Organization:
      type: object
      required: [id, name, slug, country, status, createdAt, memberCount]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        country: { type: string, enum: [AR, MX, CO, BR, CL, PE] }
        status: { type: string }
        createdAt: { type: string, format: date-time }
        memberCount: { type: integer }
    ServiceTopology:
      type: object
      required: [services, totals, kernelContract, posture]
      properties:
        services:
          type: array
          items:
            type: object
            required: [id, name, mission, runtime, ownedTables, modules, publishes, extractable, extractionDebt, extractionGate, benchmark]
            properties:
              id: { type: string }
              name: { type: string }
              mission: { type: string }
              runtime: { type: string, enum: [in_process, standalone] }
              ownedTables: { type: integer }
              modules: { type: integer }
              publishes: { type: array, items: { type: string } }
              extractable: { type: boolean, description: True when the service writes no table owned by another service }
              extractionDebt:
                type: array
                items:
                  type: object
                  required: [table, owner, reason]
                  properties:
                    table: { type: string }
                    owner: { type: string }
                    reason: { type: string }
              extractionGate: { type: string }
              benchmark: { type: string }
        totals:
          type: object
          required: [services, standalone, extractable, ownedTables, extractionDebt]
          properties:
            services: { type: integer }
            standalone: { type: integer }
            extractable: { type: integer }
            ownedTables: { type: integer }
            extractionDebt: { type: integer }
        kernelContract: { type: array, items: { type: string } }
        posture: { type: string }
    OperationalWorkItem:
      type: object
      required: [id, type, status, open, priority, assignee, dueAt, escalatedAt, slaStatus, reference, summary, amountMinor, amount, currency, noteCount, evidenceCount, metadata, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [risk_case, reconciliation_exception, dispute] }
        status: { type: string, enum: [open, resolved, accepted, opened, under_review, network_ready, won, lost, rejected, cancelled] }
        open: { type: boolean }
        priority: { type: string, enum: [low, medium, high, critical] }
        assignee: { type: [object, 'null'], additionalProperties: true }
        dueAt: { type: [string, 'null'], format: date-time }
        escalatedAt: { type: [string, 'null'], format: date-time }
        slaStatus: { type: string, enum: [none, overdue, due_soon, on_track] }
        reference: { type: string }
        summary: { type: string }
        amountMinor: { type: string }
        amount: { type: number }
        currency: { type: string }
        noteCount: { type: integer, minimum: 0 }
        evidenceCount: { type: integer, minimum: 0 }
        metadata: { type: object, additionalProperties: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    ApprovalRequest:
      type: object
      required: [id, actionType, resourceType, resourceId, status, requestPayload, requestedBy, requestedByName, expiresAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        actionType: { type: string, enum: [settlement.execute, transfer.create, payment.create, payment.reverse, payout_batch.execute, risk.case.resolve, reconciliation.exception.resolve, dispute.resolve] }
        resourceType: { type: string, enum: [settlement_cycle, transfer, book_transfer, payment, payout_batch, risk_case, reconciliation_exception, dispute] }
        resourceId: { type: string, format: uuid }
        status: { type: string, enum: [pending, executed, rejected, cancelled, expired, failed] }
        requestPayload: { type: object, additionalProperties: true }
        requestedBy: { type: string, format: uuid }
        requestedByName: { type: string }
        resolvedBy: { type: [string, 'null'], format: uuid }
        resolvedByName: { type: [string, 'null'] }
        resolutionReason: { type: [string, 'null'] }
        expiresAt: { type: string, format: date-time }
        resolvedAt: { type: [string, 'null'], format: date-time }
        executedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    BookTransfer:
      type: object
      additionalProperties: false
      required: [id, externalReference, sourceAccountId, sourceAccountReference, sourceCustomerName, destinationAccountId, destinationAccountReference, destinationCustomerName, transactionId, reversalTransactionId, description, amountMinor, amount, currency, status, riskScore, holdId, createdBy, reversedAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        externalReference: { type: string }
        sourceAccountId: { type: string, format: uuid }
        sourceAccountReference: { type: string }
        sourceCustomerName: { type: string }
        destinationAccountId: { type: string, format: uuid }
        destinationAccountReference: { type: string }
        destinationCustomerName: { type: string }
        transactionId: { type: string, format: uuid }
        reversalTransactionId: { type: [string, 'null'], format: uuid }
        description: { type: string }
        amountMinor: { type: string, pattern: '^\d+$' }
        amount: { type: number, exclusiveMinimum: 0 }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        status: { type: string, enum: [review, settled, reversed, cancelled] }
        riskScore: { type: integer, minimum: 0, maximum: 100 }
        holdId: { type: [string, 'null'], format: uuid }
        createdBy: { type: string, format: uuid }
        reversedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    AccountStatementEntry:
      type: object
      additionalProperties: false
      required: [id, journalId, transactionId, kind, description, direction, amountMinor, amount, signedAmountMinor, signedAmount, currency, status, reversalOf, createdAt]
      properties:
        id: { type: string, format: uuid }
        journalId: { type: string, format: uuid }
        transactionId: { type: [string, 'null'], format: uuid }
        kind: { type: string }
        description: { type: string }
        direction: { type: string, enum: [debit, credit] }
        amountMinor: { type: string, pattern: '^\d+$' }
        amount: { type: number, exclusiveMinimum: 0 }
        signedAmountMinor: { type: string, pattern: '^-?\d+$' }
        signedAmount: { type: number }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        status: { type: [string, 'null'] }
        reversalOf: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
    AccountStatement:
      type: object
      additionalProperties: false
      required: [account, period, data, hasMore, nextCursor]
      properties:
        account:
          type: object
          required: [id, accountReference, currency, status]
          properties:
            id: { type: string, format: uuid }
            accountReference: { type: string }
            currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
            status: { type: string }
        period:
          type: object
          required: [from, to, openingBalanceMinor, openingBalance, closingBalanceMinor, closingBalance]
          properties:
            from: { type: string, format: date-time }
            to: { type: string, format: date-time }
            openingBalanceMinor: { type: string, pattern: '^-?\d+$' }
            openingBalance: { type: number }
            closingBalanceMinor: { type: string, pattern: '^-?\d+$' }
            closingBalance: { type: number }
        data: { type: array, items: { $ref: '#/components/schemas/AccountStatementEntry' } }
        hasMore: { type: boolean }
        nextCursor: { type: [string, 'null'] }
    OrganizationMember:
      type: object
      required: [id, userId, email, displayName, role, emailVerified, mfaEnabled, createdAt]
      properties:
        id: { type: string, format: uuid }
        userId: { type: string, format: uuid }
        email: { type: string, format: email }
        displayName: { type: string }
        role: { type: string, enum: [owner, admin, operator, viewer] }
        emailVerified: { type: boolean }
        mfaEnabled: { type: boolean }
        createdAt: { type: string, format: date-time }
    OrganizationInvitation:
      type: object
      required: [id, email, role, status, invitedBy, invitedByName, expiresAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        role: { type: string, enum: [admin, operator, viewer] }
        status: { type: string, enum: [pending, accepted, revoked, expired] }
        invitedBy: { type: string, format: uuid }
        invitedByName: { type: string }
        acceptedBy: { type: [string, 'null'], format: uuid }
        expiresAt: { type: string, format: date-time }
        acceptedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    RiskEvaluation:
      type: object
      required: [id, operationType, resourceType, resourceId, amountMinor, amount, currency, counterparty, score, decision, matchedRuleIds, matchedListEntryIds, reasons, signals, decisionLatencyMs, outcome, createdAt, requestFingerprint, replayed]
      properties:
        id: { type: string, format: uuid }
        operationType: { type: string, enum: [transfer, cash_in, cash_out] }
        resourceType: { type: string }
        resourceId: { type: [string, 'null'] }
        amountMinor: { type: string }
        amount: { type: number }
        currency: { type: string }
        counterparty: { type: string }
        score: { type: integer, minimum: 0, maximum: 100 }
        decision: { type: string, enum: [approve, review, decline] }
        matchedRuleIds: { type: array, items: { type: string } }
        matchedListEntryIds: { type: array, items: { type: string, format: uuid } }
        reasons: { type: array, items: { type: string } }
        signals: { $ref: '#/components/schemas/RiskSignals' }
        decisionLatencyMs: { type: [integer, 'null'], minimum: 0, description: Persisted server-side decision latency for measured SLOs. }
        outcome:
          oneOf:
            - { $ref: '#/components/schemas/RiskOutcome' }
            - { type: 'null' }
        createdAt: { type: string, format: date-time }
        requestFingerprint: { type: string }
        replayed: { type: boolean }
    RiskStepUpChallenge:
      type: object
      additionalProperties: false
      required: [id, evaluationId, method, delivery, status, attemptCount, remainingAttempts, maxAttempts, expiresAt, verifiedAt, failedAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        evaluationId: { type: string, format: uuid }
        method: { type: string, const: otp }
        delivery: { type: string, const: client_managed }
        status: { type: string, enum: [pending, verified, failed, expired, cancelled] }
        attemptCount: { type: integer, minimum: 0 }
        remainingAttempts: { type: integer, minimum: 0 }
        maxAttempts: { type: integer, minimum: 1, maximum: 10 }
        expiresAt: { type: string, format: date-time }
        verifiedAt: { type: [string, 'null'], format: date-time }
        failedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    RiskStepUpAttempt:
      type: object
      additionalProperties: false
      required: [id, attemptNumber, result, createdAt]
      properties:
        id: { type: string, format: uuid }
        attemptNumber: { type: integer, minimum: 1 }
        result: { type: string, enum: [matched, mismatch, expired, locked] }
        createdAt: { type: string, format: date-time }
    RiskSignalsInput:
      type: object
      additionalProperties: false
      properties:
        deviceReference: { type: string, minLength: 2, maxLength: 160, writeOnly: true }
        identityReference: { type: string, minLength: 2, maxLength: 160, writeOnly: true }
        deviceTrust: { type: string, enum: [trusted, unknown, suspicious] }
        identityVerified: { type: boolean }
        ipCountry: { type: string, pattern: '^[A-Za-z]{2}$' }
        countryMismatch: { type: boolean }
    RiskSignals:
      type: object
      required: [deviceReferencePresent, identityReferencePresent]
      properties:
        deviceReferencePresent: { type: boolean }
        identityReferencePresent: { type: boolean }
        deviceTrust: { type: string, enum: [trusted, unknown, suspicious] }
        identityVerified: { type: boolean }
        ipCountry: { type: string, pattern: '^[A-Z]{2}$' }
        countryMismatch: { type: boolean }
    RiskOutcome:
      type: object
      required: [id, label, fraudType, lossAmountMinor, currency, note, createdAt]
      properties:
        id: { type: string, format: uuid }
        label: { type: string, enum: [legitimate, fraud] }
        fraudType: { type: [string, 'null'], enum: [account_takeover, identity_fraud, scam, stolen_instrument, merchant_fraud, other, null] }
        lossAmountMinor: { type: string }
        currency: { type: string, enum: [ARS, USD, MXN, COP, BRL, CLP, PEN] }
        note: { type: string }
        createdAt: { type: string, format: date-time }
    RiskListEntry:
      type: object
      required: [id, subjectType, subjectPreview, category, reason, status, expiresAt, createdAt]
      properties:
        id: { type: string, format: uuid }
        subjectType: { type: string, enum: [counterparty, device, identity] }
        subjectPreview: { type: string, description: Masked non-secret recognition hint; never the original subject. }
        category: { type: string, enum: [allow, watch, block] }
        reason: { type: string }
        status: { type: string, enum: [active, disabled] }
        expiresAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
    ReconciliationRun:
      type: object
      required: [id, name, source, currency, ingestionMode, fileName, fileSha256, periodStart, periodEnd, status, expectedMinor, actualMinor, differenceMinor, matchedCount, exceptionCount]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        source: { type: string, enum: [bank, clearing, card_network, cash_network, internal] }
        currency: { type: string }
        ingestionMode: { type: string, enum: [api, csv] }
        fileName: { type: [string, 'null'] }
        fileSha256: { type: [string, 'null'] }
        periodStart: { type: string, format: date-time }
        periodEnd: { type: string, format: date-time }
        status: { type: string, enum: [open, completed] }
        expectedMinor: { type: string }
        actualMinor: { type: string }
        differenceMinor: { type: string }
        matchedCount: { type: integer }
        exceptionCount: { type: integer }
    SettlementCycle:
      type: object
      required: [id, reconciliationRunId, name, rail, currency, periodStart, periodEnd, netMinor, differenceMinor, status, scheduledFor, settledAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        reconciliationRunId: { type: string, format: uuid }
        name: { type: string }
        rail: { type: string, enum: [bank, clearing, card_network, cash_network, internal] }
        currency: { type: string }
        periodStart: { type: string, format: date-time }
        periodEnd: { type: string, format: date-time }
        netMinor: { type: string }
        differenceMinor: { type: string }
        status: { type: string, enum: [ready, scheduled, settled] }
        scheduledFor: { type: [string, 'null'], format: date-time }
        settledAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    PlatformCapability:
      type: object
      required: [id, name, domain, summary, features, interfaces, availability, delivery, regulatoryBoundary]
      properties:
        id: { type: string }
        name: { type: string }
        domain: { type: string, enum: [core, payments, cards, commerce, credit, risk, operations, platform] }
        summary: { type: string }
        features: { type: array, items: { type: string } }
        interfaces:
          type: array
          items: { type: string, enum: [rest_api, webhooks, sdk, console, iso8583, files, streaming] }
        availability: { type: string, enum: [live, sandbox, foundation, roadmap] }
        delivery: { type: string, const: cimbra_native }
        regulatoryBoundary: { type: string }
    LiveReadiness:
      type: object
      required: [requestedMode, effectiveMode, liveReady, liveBlocked, blockReason, goLive, environments, products, rails, fintechPath, capitalPlan, references, summary]
      properties:
        requestedMode: { type: string, enum: [sandbox, live] }
        effectiveMode: { type: string, enum: [sandbox, live] }
        liveReady: { type: boolean }
        liveBlocked: { type: boolean }
        blockReason: { type: ['string', 'null'] }
        goLive:
          type: object
          required: [benchmark, documentationUrl, current, stages]
          properties:
            benchmark: { type: string, const: Pomelo }
            documentationUrl: { type: string, format: uri }
            current: { type: string, enum: [integracion, homologacion, go_live] }
            stages:
              type: array
              items:
                type: object
                required: [id, name, summary]
                properties:
                  id: { type: string, enum: [integracion, homologacion, go_live] }
                  name: { type: string }
                  summary: { type: string }
        environments:
          type: array
          items:
            type: object
            required: [id, status, hostname, pciHostname, credentialsPrefix, benchmark]
            properties:
              id: { type: string, enum: [sandbox, production] }
              status: { type: string, enum: [active, provisioned, not_provisioned] }
              hostname: { type: ['string', 'null'] }
              pciHostname: { type: ['string', 'null'] }
              credentialsPrefix: { type: string }
              benchmark: { type: string }
        products:
          type: array
          items:
            type: object
            required: [id, name, country, benchmark, documentationUrl, network, sandboxCoverage, missingForProduction, status, requiredRailIds, missingOfficialRails, adapterReady]
            properties:
              id: { type: string }
              name: { type: string }
              country: { type: string, enum: [AR, LATAM] }
              benchmark: { type: string }
              documentationUrl: { type: string, format: uri }
              network: { type: string }
              sandboxCoverage: { type: string }
              missingForProduction: { type: string }
              status: { type: string, enum: [integracion, homologacion, go_live] }
              requiredRailIds: { type: array, items: { type: string } }
              missingOfficialRails: { type: array, items: { type: string } }
              adapterReady: { type: boolean }
        rails:
          type: array
          items:
            type: object
            required: [id, name, country, kind, counterpartyKind, counterparty, officialUrl, summary, wiringContract, productIds, status, adapterRegistered]
            properties:
              id: { type: string }
              name: { type: string }
              country: { type: string, enum: [AR, LATAM] }
              kind: { type: string, enum: [corporate, regulatory_registry, safeguarding, directory, instant_credit, instant_debit, echeq_clearing, cvu_assignment, qr_pct, card_issuing, bill_payments] }
              counterpartyKind: { type: string, enum: [clearing_house, bank, card_scheme, official_registry, regulated_sponsor, corporate] }
              counterparty: { type: string }
              officialUrl: { type: string, format: uri }
              summary: { type: string }
              wiringContract: { type: string }
              productIds: { type: array, items: { type: string } }
              status: { type: string, enum: [unwired, negotiating, contracted, certified, live] }
              adapterRegistered: { type: boolean }
        fintechPath:
          type: object
          required: [jurisdiction, intendedFigure, summary, officialSources, gates, metCount, gateCount]
          properties:
            jurisdiction: { type: string, const: AR }
            intendedFigure: { type: string, const: PSPCP }
            summary: { type: string }
            officialSources:
              type: array
              items:
                type: object
                required: [name, url, surface]
                properties:
                  name: { type: string }
                  url: { type: string, format: uri }
                  surface: { type: string }
            gates:
              type: array
              items:
                type: object
                required: [id, connectionId, name, summary, status, met]
                properties:
                  id: { type: string }
                  connectionId: { type: ['string', 'null'] }
                  name: { type: string }
                  summary: { type: string }
                  status: { type: string, enum: [unwired, negotiating, contracted, certified, live] }
                  met: { type: boolean }
            metCount: { type: integer }
            gateCount: { type: integer }
        capitalPlan:
          type: object
          required: [currency, envelope, allocated, remaining, spent, commercialGate, liveReadyAfterSpend, summary, officialSources, allocations, forbidden, seedUses, raise]
          properties:
            currency: { type: string, const: USD }
            envelope: { type: integer, const: 500 }
            allocated: { type: integer }
            remaining: { type: integer }
            spent: { type: integer, const: 0 }
            commercialGate: { type: string, const: gate_1_design_partners }
            liveReadyAfterSpend: { type: boolean, const: false }
            summary: { type: string }
            officialSources:
              type: array
              items:
                type: object
                required: [name, url, surface]
                properties:
                  name: { type: string }
                  url: { type: string, format: uri }
                  surface: { type: string }
            allocations:
              type: array
              items:
                type: object
                required: [id, amount, status, name, summary]
                properties:
                  id: { type: string }
                  amount: { type: integer }
                  status: { type: string, enum: [authorized_unspent, spent, exhausted] }
                  name: { type: string }
                  summary: { type: string }
            forbidden:
              type: array
              items:
                type: object
                required: [id, name, summary]
                properties:
                  id: { type: string }
                  name: { type: string }
                  summary: { type: string }
            seedUses: { type: array, items: { type: string } }
            raise:
              type: object
              required: [instrument, amountUsd, thesis]
              properties:
                instrument: { type: string, const: pre_seed }
                amountUsd: { type: ['integer', 'null'] }
                thesis: { type: string }
        references:
          type: array
          items:
            type: object
            required: [benchmark, url, surface]
            properties:
              benchmark: { type: string }
              url: { type: string, format: uri }
              surface: { type: string }
        summary:
          type: object
          required: [integracion, homologacion, goLive, officialRailsLive, officialRailsTotal]
          properties:
            integracion: { type: integer }
            homologacion: { type: integer }
            goLive: { type: integer }
            officialRailsLive: { type: integer }
            officialRailsTotal: { type: integer }
  securitySchemes:
    session:
      type: apiKey
      in: cookie
      name: __Host-cimbra_session
      description: First-party opaque session token. Only its SHA-256 hash is stored server-side.
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: Cimbra API key
      description: S2S key. Sandbox secrets use cim_sk_test_. Live secrets cim_sk_live_ are reserved until a production hostname is provisioned and a product reaches Go Live. The plaintext secret is shown once and only its SHA-256 hash is stored.
