openapi: 3.1.0
info:
  title: OxaBank Platform API
  version: 0.1.0
  description: >
    The single public surface of the OxaBank platform. Every capability is reached through here; nothing above
    this layer talks to the accounting core, the payment engine or any external provider directly. This
    document is the authoritative contract and the source from which all client SDKs are generated - it is
    written first and the code follows.


    **Rate limiting**: every request that carries a bearer token is counted against that credential, verified
    or not: `PLATFORM_RATE_LIMIT_BURST` at once (60), refilled at `PLATFORM_RATE_LIMIT_PER_MINUTE` (600). Past
    it, any path answers 429 `too-many-requests` with `Retry-After` in whole seconds and the correlation id,
    before the request reaches anything. Requests without a token (health, the contract, metrics, signed
    deliveries) are not counted. The limit is each process's own: several replicas limit each.
  license:
    name: Proprietary
  x-published-by: Oxagile
  x-public-subset: operations, sandbox and provider-facing paths left out
servers:
  - url: https://api.your-bank.example/v1
    description: 'Your deployment: on your infrastructure, under your domain'
security:
  - oidc: []
tags:
  - name: tenants
  - name: approvals
  - name: audit
  - name: rules
  - name: proposals
  - name: statements
  - name: events
  - name: disputes
  - name: exchange
  - name: notifications
  - name: sessions
paths:
  /devices/key:
    get:
      tags:
        - sessions
      summary: Whether this device has a key to confirm with
      description: >
        Spec 044. The key this device registered, by its session's device, and whether the deployment requires
        every confirmation to be signed by it (PLATFORM_CONFIRMATION=device_key).
      responses:
        '200':
          description: This device's key, or none
          content:
            application/json:
              example:
                registered: true
                id: 01a10200-0000-7000-8000-000000000001
                registered_at: '2026-10-03T12:00:00Z'
                required: false
        '401':
          description: '`signed-out`'
    post:
      tags:
        - sessions
      summary: Register this device's key
      description: >
        Spec 044. The public half of a P-256 key the phone made in its secure hardware, usable only after the
        person's face or fingerprint: base64 of its SubjectPublicKeyInfo, DER. Bound to the tenant, the person
        and this session's device; one live key per device, another retiring it; the same key again changes
        nothing. Audited as device_key.registered.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - public_key
              properties:
                public_key:
                  type: string
                  description: base64 of a P-256 SubjectPublicKeyInfo
                  DER: null
      responses:
        '201':
          description: The key registered
          content:
            application/json:
              example:
                id: 01a10200-0000-7000-8000-000000000001
                algorithm: ES256
                registered_at: '2026-10-03T12:00:00Z'
        '400':
          description: Not a P-256 public key
        '401':
          description: '`signed-out`'
  /passkeys:
    get:
      tags:
        - sessions
      summary: The customer's passkeys, and what the browser needs to add one
      description: >
        Spec 046. The passkeys that confirm this customer's payments on the web; the relying party
        (PLATFORM_WEBAUTHN_RP_ID, absent where passkeys are not set up), this customer's registration
        challenge and user handle (base64url), and whether the deployment requires a verified key.
      responses:
        '200':
          description: The customer's passkeys
          content:
            application/json:
              example:
                available: true
                rp_id: bank.example
                registration_challenge: q83vEjRWeJA
                user_handle: 3q2-7w
                required: false
                passkeys:
                  - id: 01a10300-0000-7000-8000-000000000001
                    name: MacBook
                    credential_id: AAECAw
                    registered_at: '2026-10-03T12:00:00Z'
                    last_used_at: null
              schema:
                type: object
                required:
                  - available
                  - rp_id
                  - registration_challenge
                  - user_handle
                  - required
                  - passkeys
                properties:
                  available:
                    type: boolean
                  rp_id:
                    type: string
                    nullable: true
                  registration_challenge:
                    type: string
                  user_handle:
                    type: string
                  required:
                    type: boolean
                  passkeys:
                    type: array
                    items:
                      $ref: '#/components/schemas/Passkey'
        '401':
          description: '`signed-out`'
    post:
      tags:
        - sessions
      summary: Keep a passkey the customer's browser made
      description: >
        Spec 046. What navigator.credentials.create returned, base64url: the client data (webauthn.create,
        this customer's registration challenge, one of PLATFORM_WEBAUTHN_ORIGINS) and the attestation object,
        whose authenticator data must be for the relying party, with the person present and verified, and
        carry an ES256 key. The attestation statement is not read. A credential is one customer's; the same
        one again changes nothing. Audited as passkey.registered.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - client_data_json
                - attestation_object
              properties:
                name:
                  type: string
                  maxLength: 64
                  description: What the customer calls it; Passkey when absent
                client_data_json:
                  type: string
                  description: base64url of response.clientDataJSON
                attestation_object:
                  type: string
                  description: base64url of response.attestationObject
      responses:
        '201':
          description: The passkey kept
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Passkey'
        '400':
          description: Not a passkey for this bank, by this person, verified
        '401':
          description: '`signed-out`'
        '403':
          description: Passkeys are not set up for this bank
        '409':
          description: The credential is someone else's
  /passkeys/{id}:
    delete:
      tags:
        - sessions
      summary: Remove one of the customer's passkeys
      description: Spec 046. It confirms nothing after. Audited as passkey.removed.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Removed
        '401':
          description: '`signed-out`'
        '404':
          description: Not one of the customer's passkeys
  /fx-wires:
    get:
      tags:
        - transfers
      summary: What a wire abroad may be paid in, and the customer's wires in other currencies
      description: >
        Spec 053. Offered where the FX provider is bound for it (PLATFORM_FX_WIRE_RAIL) and a rate provider is
        configured; `currencies` are what a payee abroad may be paid in, from a dollar account.
      responses:
        '200':
          description: The offer and the wires
          content:
            application/json:
              schema:
                type: object
                required:
                  - available
                  - sell_currency
                  - currencies
                  - wires
                properties:
                  available:
                    type: boolean
                  sell_currency:
                    type: string
                  currencies:
                    type: array
                    items:
                      type: string
                  wires:
                    type: array
                    items:
                      $ref: '#/components/schemas/FxWire'
              example:
                available: true
                sell_currency: USD
                currencies:
                  - EUR
                  - GBP
                wires:
                  - id: 0199c000-0000-7000-8000-0000000000aa
                    state: sent
                    said: Sent by SWIFT. It usually arrives in 1 to 3 business days.
                    from_account: '11'
                    payee:
                      id: 0199c000-0000-7000-8000-000000000001
                      display_name: Lena Schneider
                    sell_minor: 50000
                    sell_currency: USD
                    buy_minor: 42909
                    buy_currency: EUR
                    rate: '0.858058'
                    spread_bps: 50
                    spread_minor: 215
                    rate_source: European Central Bank reference rate
                    rate_as_of: '2026-10-02'
                    reference: null
                    authorize:
                      amount: '500.00'
                      currency: USD
                      payee: Lena Schneider, who receives 429.09 EUR
                      expires_in_minutes: 10
                    money_moved: true
                    return_reason: null
                    created_at: '2026-10-05T10:00:00Z'
                    updated_at: '2026-10-05T10:01:00Z'
    post:
      tags:
        - transfers
      summary: Quote a wire abroad in the payee's currency
      description: >
        Spec 053. From the customer's own dollar account to a SWIFT payee with an address, at the platform's
        rate and spread (PLATFORM_EXCHANGE_SPREAD_BPS), as an exchange is quoted. The answer carries both
        figures and what to sign. Nothing moves until it is confirmed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - from_account
                - payee_id
                - sell_minor
                - buy_currency
              properties:
                from_account:
                  type: string
                payee_id:
                  type: string
                  format: uuid
                sell_minor:
                  type: integer
                  minimum: 1
                buy_currency:
                  type: string
                reference:
                  type: string
                  maxLength: 140
                  nullable: true
      responses:
        '201':
          description: Quoted, awaiting the customer's confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FxWire'
        '400':
          description: Not a SWIFT payee with an address
          not a dollar account: null
          or a currency not offered: null
        '404':
          description: Not the customer's payee or account
        '409':
          description: The rate is too old to quote on
        '503':
          description: Not offered here
  /fx-wires/{id}:
    get:
      tags:
        - transfers
      summary: One of the customer's wires in another currency
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The wire
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FxWire'
        '404':
          description: Not the customer's
  /fx-wires/{id}/confirm:
    post:
      tags:
        - transfers
      summary: Confirm a wire in another currency, and have it sent
      description: >
        Spec 053. Confirmed as a transfer is (the kind is fx_wire and the id the wire's; the payee signed
        reads "<payee>, who receives <amount> <currency>"). The core debits the customer's dollars to
        PLATFORM_FX_WIRE_SETTLEMENT_ACCOUNT; the FX provider buys exactly what the payee receives and pays it
        by SWIFT, each once under the wire's key. A payment that cannot be made is refunded in full on a
        second person's approval (fx_wire_refund).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Sent, being sent, refused or returned, with why
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FxWire'
        '400':
          description: The confirmation does not hold
        '409':
          description: Already confirmed
          or the quote has expired: null
        '503':
          description: The account could not be debited just now; nothing was sent
  /linked-accounts:
    get:
      tags:
        - transfers
      summary: The customer's accounts at other banks, to add money from
      description: >
        Spec 048. Each by its holder, routing number and last four digits; whether pulling is offered here
        (PLATFORM_ACH_RAIL=column with a settlement account) and its limits in cents.
      responses:
        '200':
          description: The linked accounts
          content:
            application/json:
              schema:
                type: object
                required:
                  - available
                  - verification_required
                  - max_minor
                  - daily_minor
                  - accounts
                properties:
                  available:
                    type: boolean
                  verification_required:
                    type: boolean
                    description: >
                      Spec 054. Where the bank verifies (PLATFORM_MICRO_DEPOSIT_ACCOUNT), a pull is taken only
                      from an account verified by its two small deposits.
                  max_minor:
                    type: integer
                    nullable: true
                  daily_minor:
                    type: integer
                    nullable: true
                  accounts:
                    type: array
                    items:
                      $ref: '#/components/schemas/LinkedAccount'
              example:
                available: true
                verification_required: true
                max_minor: 100000
                daily_minor: 250000
                accounts:
                  - id: 0199b000-0000-7000-8000-000000000001
                    holder_name: Ana Rivera
                    routing_number: '021000021'
                    last4: '6789'
                    account_type: checking
                    label: From Ana Rivera ••6789
                    verified: false
                    verification:
                      id: 0199b000-0000-7000-8000-0000000000dd
                      linked_account_id: 0199b000-0000-7000-8000-000000000001
                      state: sent
                      said: >-
                        Two deposits of less than 1.00 USD are on their way. They usually show on that
                        account's statement in 1 to 3 business days; then say what they were. 3 tries left.
                      tries_left: 3
                      authorize: null
                      created_at: '2026-10-05T09:00:00Z'
                      updated_at: '2026-10-05T09:01:00Z'
                    created_at: '2026-10-05T09:00:00Z'
    post:
      tags:
        - transfers
      summary: Link the customer's own account at another US bank
      description: >-
        Spec 048. A valid ABA routing number, 4 to 17 digits, checking or savings. The same account again is
        the same link.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - holder_name
                - routing_number
                - account_number
                - account_type
              properties:
                holder_name:
                  type: string
                  maxLength: 127
                routing_number:
                  type: string
                account_number:
                  type: string
                account_type:
                  type: string
                  enum:
                    - checking
                    - savings
      responses:
        '201':
          description: Linked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkedAccount'
        '400':
          description: Not a routing number, an account number, or a type
  /linked-accounts/{id}:
    delete:
      tags:
        - transfers
      summary: Unlink one; pulls already made keep naming it
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Unlinked
        '404':
          description: Not one of the customer's
  /linked-accounts/{id}/micro-deposits:
    post:
      tags:
        - transfers
      summary: Verify a linked account with two small deposits
      description: >
        Spec 054. Nothing is sent until it is confirmed. The answer carries what to sign: 0.00 USD, and "Two
        small deposits from OxaBank to <holder> ••<last4>". At most three verifications sent in 30 days per
        customer; one under way per account is that one again.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '201':
          description: Awaiting the customer's confirmation, or the one under way
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MicroDeposit'
        '400':
          description: Three sent in 30 days
        '404':
          description: Not one of the customer's linked accounts
        '409':
          description: Already verified
          or failed three times: null
        '503':
          description: Verification is not set up here
  /micro-deposits/{id}/confirm:
    post:
      tags:
        - transfers
      summary: Confirm, and have the two deposits sent
      description: >
        Spec 054. Confirmed as a transfer is (the kind is micro_deposit and the id the verification's). The
        core moves the two together from PLATFORM_MICRO_DEPOSIT_ACCOUNT to the ACH settlement account; Column
        sends each under its own key. Posting, it may be confirmed again: the core is not asked twice. A
        deposit the other bank refuses or sends back closes the verification, and putting its cents back is a
        second person's approval (micro_deposit_return).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Sent, or refused or returned with why
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MicroDeposit'
        '400':
          description: The confirmation does not hold
        '409':
          description: Already sent
          or expired: null
        '503':
          description: Not sent just now; confirm again
  /micro-deposits/{id}/check:
    post:
      tags:
        - transfers
      summary: Say the two amounts, in either order
      description: >
        Spec 054. Right, the account is verified; wrong three times, the verification fails, and the account
        is removed and added again to start over.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amounts_minor
              properties:
                amounts_minor:
                  type: array
                  minItems: 2
                  maxItems: 2
                  items:
                    type: integer
                    minimum: 1
                    maximum: 99
      responses:
        '200':
          description: What it came to
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/MicroDeposit'
                  - type: object
                    required:
                      - outcome
                    properties:
                      outcome:
                        type: string
                        enum:
                          - verified
                          - wrong
                          - failed
                          - not_open
        '400':
          description: Not two amounts of 1 to 99 cents
        '404':
          description: Not the customer's
  /pulls:
    get:
      tags:
        - transfers
      summary: The customer's pulls, the newest first
      responses:
        '200':
          description: Up to 20
          content:
            application/json:
              schema:
                type: object
                required:
                  - pulls
                properties:
                  pulls:
                    type: array
                    items:
                      $ref: '#/components/schemas/Pull'
              example:
                pulls:
                  - id: 0199b000-0000-7000-8000-0000000000aa
                    state: sent
                    said: Asked of your bank. Money pulled by ACH usually arrives in 1 to 3 business days.
                    linked_account_id: 0199b000-0000-7000-8000-000000000001
                    from: From Ana Rivera ••6789
                    to_account: '4'
                    amount_minor: 2500
                    currency: USD
                    mandate: >-
                      I authorise OxaBank to debit 25.00 USD once, on or after 2026-10-05, from the checking
                      account ending 6789 at routing number 021000021, held by Ana Rivera, and to credit it to
                      my OxaBank account. I can cancel before it is sent. If it was not authorised, I can
                      dispute it with my bank.
                    authorize:
                      amount: '25.00'
                      currency: USD
                      payee: From Ana Rivera ••6789
                      expires_in_minutes: 10
                    auth_method: device_biometric
                    money_moved: false
                    return_reason: null
                    recurring_id: null
                    due_on: null
                    created_at: '2026-10-05T09:00:00Z'
                    updated_at: '2026-10-05T09:01:00Z'
    post:
      tags:
        - transfers
      summary: Say how much to pull in, from which linked account, to which dollar account
      description: >
        Spec 048. Nothing happens until it is confirmed. Into the customer's own dollar account; refused above
        PLATFORM_ACH_PULL_MAX_MINOR, or when the customer's pulls of the last 24 hours would pass
        PLATFORM_ACH_PULL_DAILY_MINOR. The answer carries the authorisation text and the figures to sign.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - linked_account_id
                - to_account
                - amount_minor
              properties:
                linked_account_id:
                  type: string
                  format: uuid
                to_account:
                  type: string
                amount_minor:
                  type: integer
                  minimum: 1
      responses:
        '201':
          description: Awaiting the customer's authorisation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pull'
        '400':
          description: Over a limit
          not a dollar account: null
          or nothing to pull: null
        '404':
          description: Not the customer's linked account or account
        '503':
          description: Pulling is not set up here
  /pulls/{id}:
    get:
      tags:
        - transfers
      summary: One of the customer's pulls
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The pull
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pull'
        '404':
          description: Not the customer's
  /backoffice/people:
    get:
      tags:
        - operators
      summary: Who the back office's ids are
      description: >
        Spec 057. Each member of the operator's tenant (up to 500, operators first) by the identity provider's
        subject: an operator by the label they were added with, a customer by their client's name in the core
        where they have one. Operators only.
      responses:
        '200':
          description: The tenant's people
          content:
            application/json:
              schema:
                type: object
                required:
                  - people
                properties:
                  people:
                    type: array
                    items:
                      type: object
                      required:
                        - subject
                        - name
                        - role
                      properties:
                        subject:
                          type: string
                        name:
                          type: string
                          nullable: true
                        role:
                          type: string
                          enum:
                            - operator
                            - customer
        '403':
          description: Not an operator of this tenant
  /backoffice/lending/portfolio:
    get:
      tags:
        - lending
        - back-office
      summary: Every loan at its stage, and the portfolio at risk
      description: >
        Spec 069, as Mambu shows a loan book: partial_application, pending_approval, approved, rejected,
        withdrawn, active, active_in_arrears, closed_obligations_met, written_off; what is owed on loans out
        and how much of it is on loans late more than 1, 30 and 90 days (PAR1/PAR30/PAR90, also in basis
        points); credit lines by state with the open limits.
      responses:
        '200':
          description: The portfolio
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficePortfolio'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/loans/{id}:
    get:
      tags:
        - lending
        - back-office
      summary: One loan whole — its stage, schedule, payments and position
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The loan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeLoan'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such loan
  /backoffice/loans/{id}/write-off:
    post:
      tags:
        - lending
        - back-office
      summary: Propose writing a drawn loan off
      description: >
        Spec 069. **This changes nothing.** Proposed with a reason for what it owes now (loan_write_off); a
        credit officer within their limit (25,000.00) or the credit committee approves, and it is written off
        for what it owes then. A payment that still comes in is still taken.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such loan
        '409':
          description: Not a drawn loan still owing, or a write-off already waits
  /backoffice/settings/products:
    get:
      tags:
        - back-office
      summary: The savings and loan products the core offers
      description: >
        Spec 071. As the core defines them: rate, amounts and terms, whether active, and how the core books
        them (none, cash, accrual). Made in the core; read here.
      responses:
        '200':
          description: The products, savings first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeProducts'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/settings/calendars:
    get:
      tags:
        - back-office
      summary: The days each rail is shut in a year
      parameters:
        - name: year
          in: query
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: Each rail's rules and holidays
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeCalendars'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/settings/branches:
    get:
      tags:
        - back-office
      summary: The bank's branches, open and closed, with their staff and customers counted
      responses:
        '200':
          description: The branches
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeBranches'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - back-office
      summary: Propose a branch
      description: >
        Spec 071. **This adds nothing.** A code (3 to 12 capitals, digits or dashes), a name, a city, the core
        account its vault's cash is booked in, a currency (USD when none) and a reason (branch_change); a
        supervisor approves.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - code
                - name
                - city
                - vault_account
                - reason
              properties:
                code:
                  type: string
                name:
                  type: string
                city:
                  type: string
                vault_account:
                  type: string
                currency:
                  type: string
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason, or a code, name, city or vault that does not read
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: The code is taken, or the same proposal already waits
  /backoffice/settings/branches/{id}:
    post:
      tags:
        - back-office
      summary: Propose renaming, moving the vault of, closing or opening again a branch
      description: >
        Spec 071. **This changes nothing.** `closed` true closes it (refused while a till is open there),
        false opens it again; otherwise the name, city and vault given replace the ones there. A supervisor
        approves (branch_change). A closed branch opens no till.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                name:
                  type: string
                city:
                  type: string
                vault_account:
                  type: string
                closed:
                  type: boolean
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason, a vault that does not read, or nothing would change
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such branch
        '409':
          description: Closed or open already, tills open there, or the same proposal already waits
  /backoffice/settings/authority:
    get:
      tags:
        - back-office
        - iam
      summary: The authority matrix with the bank's amounts
      description: >
        Spec 071. For each role: how much it posts at the desk and how much it decides in each domain it
        decides, with the matrix's own amount and who set the bank's, when. `limit_minor` null is without
        limit.
      responses:
        '200':
          description: The matrix
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeAuthority'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - back-office
        - iam
      summary: Propose an amount in the authority matrix
      description: >
        Spec 071. **This changes nothing.** Only where the matrix gives the role an amount (a teller's
        posting, a head teller's or a branch manager's teller overrides, a credit officer's lending); whether
        a role decides a domain at all, or without limit, is not a setting. An IAM administrator proposes and
        another approves (authority_limit).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
                - scope
                - limit_minor
                - reason
              properties:
                role:
                  type: string
                scope:
                  type: string
                  description: A domain (teller, lending, …) or posting
                limit_minor:
                  type: integer
                  format: int64
                  minimum: 1
                  maximum: 100000000000
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason, no such role or scope, no amount there to set, out of bounds, or the amount already
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: The same proposal already waits
  /backoffice/accounting/trial-balance:
    get:
      tags:
        - back-office
      summary: The chart of accounts with what was debited and credited to each
      description: >
        Spec 070. Every general ledger account the core keeps, with its debits, credits and balance on its own
        side (assets and expenses debit, the rest credit) per currency, from the journal up to `to`; and per
        currency whether all debits equal all credits. Accountants and supervisors read it.
      parameters:
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
      responses:
        '200':
          description: The trial balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTrialBalance'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/accounting/journal:
    get:
      tags:
        - back-office
      summary: What was booked, newest first
      parameters:
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: gl
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The journal (at most 500 entries)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeJournal'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - back-office
      summary: Propose a manual journal entry
      description: >
        Spec 070. **This books nothing.** A debit and a credit of the same amount between two accounts that
        take manual entries, with a memo (manual_journal); another accountant or a supervisor approves, and
        the core books it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - debit_gl
                - credit_gl
                - amount_minor
                - currency
                - memo
              properties:
                debit_gl:
                  type: string
                credit_gl:
                  type: string
                amount_minor:
                  type: integer
                  format: int64
                  minimum: 1
                currency:
                  type: string
                memo:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No memo, no amount, the same account twice, or an account that takes no manual entries
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/accounting/closures:
    get:
      tags:
        - back-office
      summary: The days the books are closed up to, latest first
      responses:
        '200':
          description: The closures
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeClosures'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - back-office
      summary: Propose closing the books up to a day
      description: >
        Spec 070. **This closes nothing.** Only a day already over (books_closure); another accountant or a
        supervisor approves, and from then the core books nothing on or before it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - closing_date
                - comments
              properties:
                closing_date:
                  type: string
                  format: date
                comments:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No comments, or a day not over
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: That closure already waits for approval
  /backoffice/reports/overview:
    get:
      tags:
        - back-office
      summary: What a manager reads first
      description: >
        Spec 070. Customers by state and by branch with what they hold, the tills' cash by branch with what
        closed tills were over or short, what waits for approval by kind, and the staff's open and overdue
        tasks.
      responses:
        '200':
          description: The overview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeOverview'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/customers/{subject}/accounts/{account}/actions:
    post:
      tags:
        - customers
        - accounts
        - back-office
      summary: Propose blocking, holding, releasing or closing a customer's account
      description: >
        Spec 068, as Mambu's lock, block-and-seize and close. **This changes nothing.** Proposed with a reason
        (account_control); a branch manager, a compliance officer or a supervisor approves, and the core does
        it then. A hold sets part of the balance aside (`amount_minor`); a release lets one go (`hold_ref`,
        from the account's standing); only an empty account closes.
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
        - name: account
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
                - reason
              properties:
                action:
                  type: string
                  enum:
                    - block
                    - unblock
                    - block_debits
                    - unblock_debits
                    - hold
                    - release
                    - close
                amount_minor:
                  type: integer
                  minimum: 1
                  description: For a hold
                hold_ref:
                  type: string
                  description: For a release
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason, an unknown action, a hold with no amount, a release with no hold
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a customer the caller sees, or not their account
        '409':
          description: A balance on an account to close, or the same change waiting
  /backoffice/customers/{subject}/state:
    post:
      tags:
        - customers
        - back-office
      summary: Propose a customer's state
      description: >
        Spec 068. active, inactive, exited or blacklisted, with a reason (client_state); a branch manager, a
        compliance officer or a supervisor approves. An exited or blacklisted customer signs in no more (POST
        /sessions answers 403) and their sessions are ended when it is approved.
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - state
                - reason
              properties:
                state:
                  type: string
                  enum:
                    - active
                    - inactive
                    - exited
                    - blacklisted
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason or no such state
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a customer the caller sees
        '409':
          description: They are that already, or the same change waits
  /backoffice/customers/{subject}/notes:
    post:
      tags:
        - customers
        - back-office
      summary: Write a note on a customer
      description: Spec 068. At once; shown on the customer's page, newest first, with its author.
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  minLength: 1
                  maxLength: 4000
      responses:
        '201':
          description: Written
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - author
                  - body
                properties:
                  id:
                    type: string
                    format: uuid
                  author:
                    type: string
                  body:
                    type: string
        '400':
          description: Empty or too long
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a customer the caller sees
  /backoffice/tasks:
    get:
      tags:
        - tasks
        - back-office
      summary: Tasks — the caller's, a customer's, or everyone's
      description: Spec 068. The open first, the soonest due first.
      parameters:
        - name: scope
          in: query
          required: false
          schema:
            type: string
            enum:
              - mine
              - all
        - name: customer
          in: query
          required: false
          schema:
            type: string
        - name: open
          in: query
          required: false
          schema:
            type: boolean
      responses:
        '200':
          description: The tasks
          content:
            application/json:
              schema:
                type: object
                required:
                  - tasks
                properties:
                  tasks:
                    type: array
                    items:
                      $ref: '#/components/schemas/BackofficeTask'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - tasks
        - back-office
      summary: Set a task, for oneself or another member of staff
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                  minLength: 1
                  maxLength: 200
                detail:
                  type: string
                due_on:
                  type: string
                  format: date
                assignee:
                  type: string
                  description: A member of staff; the caller where none
                customer:
                  type: string
                  description: The customer it is about
      responses:
        '201':
          description: Set
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTask'
        '400':
          description: No title, or not for a member of staff
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/tasks/{id}/done:
    post:
      tags:
        - tasks
        - back-office
      summary: Mark a task done
      description: Spec 068. By whoever it is for, or whoever set it.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Done
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTask'
        '403':
          description: Somebody else's
        '409':
          description: Done already
  /backoffice/branches:
    get:
      tags:
        - operators
        - branches
      summary: The bank's branches
      description: Spec 067. Any member of staff.
      responses:
        '200':
          description: The branches
          content:
            application/json:
              schema:
                type: object
                required:
                  - branches
                properties:
                  branches:
                    type: array
                    items:
                      $ref: '#/components/schemas/BackofficeBranch'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/tills:
    get:
      tags:
        - tills
        - back-office
      summary: A branch's tills, open first, and its tellers
      description: >-
        Spec 067. Head tellers, branch managers, supervisors; a branch-scoped caller sees their own branch
        only.
      parameters:
        - name: branch
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The branch's tills
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTills'
        '400':
          description: No branch given and the caller works none
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - tills
        - back-office
      summary: Open a till for a teller, with a float
      description: >
        Spec 067. A head teller opens a till for a teller of their branch; one open till per teller. The float
        is cash already in the branch; nothing moves in the core. Audited (till.opened).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - teller
                - opening_minor
              properties:
                teller:
                  type: string
                branch:
                  type: string
                  format: uuid
                opening_minor:
                  type: integer
                  minimum: 0
      responses:
        '201':
          description: Opened
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTill'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: Not a teller, not of this branch, or a till already open
  /backoffice/tills/mine:
    get:
      tags:
        - tills
        - back-office
      summary: The caller's open till and their posting limit
      responses:
        '200':
          description: Their till, or null
          content:
            application/json:
              schema:
                type: object
                required:
                  - till
                  - posting_limit_minor
                properties:
                  till:
                    nullable: true
                    allOf:
                      - $ref: '#/components/schemas/BackofficeTill'
                  posting_limit_minor:
                    type: integer
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/tills/{id}:
    get:
      tags:
        - tills
        - back-office
      summary: A till and its entries
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The till
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTill'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not the caller's
          nor of a branch they manage: null
  /backoffice/tills/{id}/cash:
    post:
      tags:
        - tills
        - back-office
      summary: Top a drawer up from the vault, or take cash back
      description: Spec 067. Head tellers. Cash moving inside the branch; nothing moves in the core. Audited.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - amount_minor
              properties:
                kind:
                  type: string
                  enum:
                    - buy
                    - sell
                amount_minor:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: The till after it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTill'
        '403':
          description: Not a head teller
        '409':
          description: The till is closed
          or the drawer does not hold that much: null
  /backoffice/tills/{id}/close:
    post:
      tags:
        - tills
        - back-office
      summary: Count and close a till
      description: >
        Spec 067. Its teller or whoever manages its branch's tills. What it should hold (the float and every
        entry), what was counted and the difference are written down and audited (till.closed); a count that
        differs needs a note of eight characters or more.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - counted_minor
              properties:
                counted_minor:
                  type: integer
                  minimum: 0
                note:
                  type: string
      responses:
        '200':
          description: Closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeTill'
        '400':
          description: A difference with no note
        '409':
          description: Already closed
  /backoffice/me:
    get:
      tags:
        - operators
      summary: Who is signed in, their roles and what they may do
      description: >
        Spec 066. Any member of staff. The roles carry the permissions; every operator route needs one
        (platform-iam's ROUTES) and refuses with 403 "Your role does not allow this." otherwise.
      responses:
        '200':
          description: The signed-in member of staff
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMe'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/iam/staff:
    get:
      tags:
        - operators
        - iam
      summary: The staff, their roles, the roles a bank gives, and changes waiting
      description: Spec 066. IAM administrators (and, as every read, auditors).
      responses:
        '200':
          description: The staff
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeIamStaff'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/iam/staff/{subject}/roles:
    post:
      tags:
        - operators
        - iam
      summary: Propose giving or taking a member of staff's role
      description: >
        Spec 066. **This changes nothing.** One IAM administrator proposes with a reason (role_change);
        another approves at POST /approvals/{id}/decision, and only an IAM administrator may. Nobody proposes
        or decides a change to their own roles. An auditor holds no other role and an IAM administrator none
        but that (409 with why). A person new to the staff needs a `label`; a customer of the bank cannot be
        given a role.
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
                - grant
                - reason
              properties:
                role:
                  type: string
                  enum:
                    - teller
                    - head_teller
                    - branch_manager
                    - service_agent
                    - operations
                    - credit_officer
                    - credit_committee
                    - compliance
                    - accountant
                    - supervisor
                    - auditor
                    - iam_admin
                grant:
                  type: boolean
                  description: True gives the role
                  false takes it away: null
                label:
                  type: string
                  description: The name of a person new to the staff
                branch:
                  type: string
                  format: uuid
                  description: 'Spec 067: the branch they work, set with the role'
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed, awaiting another IAM administrator
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No such role
          no reason: null
          or a new member with no name: null
        '403':
          description: Not an IAM administrator, or their own roles
        '404':
          description: Taking a role from nobody
        '409':
          description: Already held, not held, roles that may not go together, a customer, or the same change waiting
  /backoffice/customers:
    get:
      tags:
        - customers
        - back-office
      summary: Find a customer
      description: >
        Spec 065. By any words of their name (the core's), the identity provider's subject (six characters or
        more), the core's client number, an account number or a card's last four. Empty lists the first 50.
        Operators only.
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Customers found, at most 50
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeCustomers'
              example:
                term: eriksson
                customers:
                  - subject: CiQwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMGN1MDESBWxvY2Fs
                    name: Anna Maria Eriksson
                    client_id: '7'
                    matched_by: name
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/customers/{subject}:
    get:
      tags:
        - customers
        - back-office
      summary: Everything the bank holds about a customer
      description: >
        Spec 065. Profile and identity, screening, accounts with balances, the credit line and loans, cards,
        payments, crypto, disputes, support, devices, businesses, and what operators proposed and decided
        about them. A part that could not be read is named in `unavailable`; the rest is shown.
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The customer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeCustomer'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a customer of this tenant
  /backoffice/customers/{subject}/accounts/{account}/movements:
    get:
      tags:
        - customers
        - back-office
      summary: One of a customer's accounts, as its statement reads
      description: Spec 065. The account has to be one the customer holds, as the core says now.
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
        - name: account
          in: path
          required: true
          schema:
            type: string
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
      responses:
        '200':
          description: The account and its latest movements, newest first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeCustomerMovements'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not this customer's account
        '503':
          description: The core did not answer
  /backoffice/customers/{subject}/teller:
    post:
      tags:
        - customers
        - back-office
      summary: Propose a teller's movement for a customer
      description: >
        Spec 065. **This moves nothing.** Cash in (from the bank's till, PLATFORM_TELLER_TILL_ACCOUNT, to one
        of their accounts), cash out (the other way), or between two of their accounts in one currency, with a
        reason. A second person approves (teller_movement) and the core moves it then. Cash out and between
        are checked against the balance now and again by the core.
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - movement
                - account
                - amount_minor
                - reason
              properties:
                movement:
                  type: string
                  enum:
                    - cash_in
                    - cash_out
                    - between
                account:
                  type: string
                  description: Theirs; the one money comes from
                  or for cash in goes to: null
                to_account:
                  type: string
                  description: For between
                  the other of theirs: null
                amount_minor:
                  type: integer
                  minimum: 1
                reason:
                  type: string
                  minLength: 8
      responses:
        '200':
          description: >
            Spec 067. Posted at once, on the teller's own authority with the customer at the desk: within
            their posting limit, cash through their open till (audited teller.posted).
          content:
            application/json:
              schema:
                type: object
                required:
                  - posted
                  - id
                  - core_ref
                  - note
                properties:
                  posted:
                    type: boolean
                    enum:
                      - true
                  id:
                    type: string
                    format: uuid
                  core_ref:
                    type: string
                  till_expected_minor:
                    type: integer
                    nullable: true
                  note:
                    type: string
        '202':
          description: >-
            Above the teller's limit — proposed, for a head teller, manager or supervisor whose limit covers
            it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason, nothing to move, no till for cash, or two currencies
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a customer
          or not their account: null
        '409':
          description: The account or the drawer does not hold that much, or no till is open for cash
  /backoffice/customers/{subject}/cards/{card}/freeze:
    post:
      tags:
        - customers
        - cards
        - back-office
      summary: Freeze a customer's card now
      description: >
        Spec 065. At once, by one operator — protecting a customer does not wait for a colleague — at the
        issuer first, with the reason written to the audit chain (operator.card_frozen).
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
        - name: card
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 8
      responses:
        '200':
          description: Frozen
          content:
            application/json:
              schema:
                type: object
                required:
                  - card_id
                  - status
                properties:
                  card_id:
                    type: string
                    format: uuid
                  status:
                    type: string
        '400':
          description: No reason
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not this customer's card
        '409':
          description: The card cannot be frozen from where it is
  /backoffice/customers/{subject}/cards/{card}/unfreeze:
    post:
      tags:
        - customers
        - cards
        - back-office
      summary: Propose unfreezing a customer's card
      description: Spec 065. The card stays frozen until a different person approves (card_unfreeze).
      parameters:
        - name: subject
          in: path
          required: true
          schema:
            type: string
        - name: card
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed, awaiting a second person
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: No reason
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not this customer's card
        '409':
          description: The card is not frozen
  /credit-line:
    get:
      tags:
        - lending
      summary: The customer's revolving credit line, where it stands today
      description: >
        Spec 056. Whether a line is offered here (PLATFORM_CREDIT_LINE_PRODUCT) and the customer's line: asked
        for, open, declined or refused. Open, what is drawn, available, owed today, the next instalment and
        anything overdue are read from the core, which keeps the line as a loan; the last draws and repayments
        with it.
      responses:
        '200':
          description: The offer and the line
          content:
            application/json:
              schema:
                type: object
                required:
                  - available
                  - line
                properties:
                  available:
                    type: boolean
                  line:
                    nullable: true
                    allOf:
                      - $ref: '#/components/schemas/CreditLine'
              example:
                available: true
                line:
                  id: 0199d000-0000-7000-8000-0000000000c1
                  state: open
                  said: Open. Draw from it into your account, and pay it back at any time.
                  currency: USD
                  asked_minor: 300000
                  limit_minor: 200000
                  drawn_minor: 70000
                  available_minor: 130000
                  payoff_minor: 70412
                  overdue_minor: 0
                  next_due:
                    'on': '2026-11-06'
                    amount_minor: 6619
                  days_past_due: 0
                  closed_by: null
                  closed_at: null
                  annual_rate: '24'
                  term_months: 12
                  account_id: '4'
                  provenance: live
                  movements:
                    - id: 0199d000-0000-7000-8000-0000000000d1
                      kind: draw
                      amount_minor: 100000
                      currency: USD
                      account_id: '4'
                      state: done
                      said: In your account.
                      authorize: null
                      money_moved: true
                      auth_method: device_key
                      created_at: '2026-10-06T09:00:00Z'
                  agreement:
                    id: 0199d000-0000-7000-8000-0000000000e1
                    state: signed
                    sha256: 9f2c4d8e1a7b3c5f6e0d2b4a8c1e3f5a7b9d0c2e4f6a8b0d1c3e5f7a9b0c2d4e
                    signed_at: '2026-10-05T21:04:00Z'
                    auth_method: device_key
                    authorize: null
                    pdf: /v1/documents/0199d000-0000-7000-8000-0000000000e1/pdf
                    signed_pdf: /v1/documents/0199d000-0000-7000-8000-0000000000e1/pdf?signed=true
                    created_at: '2026-10-05T21:00:00Z'
                  autopay:
                    id: 0199d000-0000-7000-8000-0000000000a1
                    state: active
                    said: 'On: the amount due each month, up to 200.00 USD, from account 4.'
                    from_account: '4'
                    cap_minor: 20000
                    authorize: null
                    runs:
                      - due_on: '2026-11-06'
                        amount_minor: 6619
                        state: done
                        said: Paid.
                    created_at: '2026-10-06T09:00:00Z'
                  created_at: '2026-10-05T09:00:00Z'
    post:
      tags:
        - lending
      summary: Ask for a credit line
      description: >
        Spec 056. Into the customer's own dollar account, at least 100.00 USD. The bank's credit policy
        decides (013): approved, at the lesser of what was asked and what it allows, and its rate; referred,
        at what was asked and PLATFORM_CREDIT_LINE_RATE; declined, kept and said. Approved or referred, a
        second person opens it in the core (credit_line_opening). One asked for or open per customer.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - limit_minor
                - to_account
                - monthly_income_minor
              properties:
                limit_minor:
                  type: integer
                  minimum: 10000
                to_account:
                  type: string
                monthly_income_minor:
                  type: integer
                  minimum: 1
                monthly_obligations_minor:
                  type: integer
                  minimum: 0
      responses:
        '201':
          description: Asked for, or declined
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditLine'
        '400':
          description: Under the least
          not a dollar account: null
          or no income said: null
        '404':
          description: Not the customer's account
        '409':
          description: A line already asked for or open, or one the bank closed or charged off still owes
        '503':
          description: No line is offered here
    delete:
      tags:
        - lending
      summary: Close the customer's own line
      description: >
        Spec 059. Only when the core says nothing is owed on it; its autopay stops with it. A closed line is
        never drawn again (the core would open a paid-off loan again on the next draw, so the closing is the
        platform's).
      responses:
        '200':
          description: Closed
          content:
            application/json:
              schema:
                type: object
                required:
                  - available
                  - line
                properties:
                  available:
                    type: boolean
                  line:
                    $ref: '#/components/schemas/CreditLine'
        '409':
          description: Something is owed on it, closed already, or no open line
        '503':
          description: The core did not answer
          or no line is offered here: null
  /credit-line/agreement:
    get:
      tags:
        - lending
      summary: The line's agreement, to read and sign
      description: >
        Spec 060. Rendered once from the line's terms and kept as those bytes with their SHA-256. Unsigned, it
        carries what the customer confirms: the limit and "OxaBank credit line agreement, SHA-256 …" with the
        whole hash, confirmed as a payment is (the kind is line_agreement and the id the agreement's). A line
        is not drawn until it is signed.
      responses:
        '200':
          description: The agreement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineAgreement'
              example:
                id: 0199d000-0000-7000-8000-0000000000e1
                state: unsigned
                sha256: 9f2c4d8e1a7b3c5f6e0d2b4a8c1e3f5a7b9d0c2e4f6a8b0d1c3e5f7a9b0c2d4e
                signed_at: null
                auth_method: null
                authorize:
                  amount: '3000.00'
                  currency: USD
                  payee: >-
                    OxaBank credit line agreement, SHA-256
                    9f2c4d8e1a7b3c5f6e0d2b4a8c1e3f5a7b9d0c2e4f6a8b0d1c3e5f7a9b0c2d4e
                pdf: /v1/documents/0199d000-0000-7000-8000-0000000000e1/pdf
                signed_pdf: null
                created_at: '2026-10-05T21:00:00Z'
        '409':
          description: No line the core keeps
  /credit-line/agreement/sign:
    post:
      tags:
        - lending
      summary: Sign the line's agreement
      description: >
        Spec 060. The customer's confirmation over the limit and the agreement's SHA-256, under the same
        freshness and strength as a payment's; the signed copy, which says who signed, how, when and what, is
        kept beside it. Once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Signed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineAgreement'
        '400':
          description: The confirmation does not hold
        '409':
          description: Signed already
          or no line: null
  /documents/{id}/pdf:
    get:
      tags:
        - lending
      summary: A document of the customer's own, as read and signed or the signed copy
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: signed
          in: query
          required: false
          schema:
            type: boolean
      responses:
        '200':
          description: The PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '404':
          description: Not the caller's
          or not signed yet: null
  /credit-line/autopay:
    post:
      tags:
        - lending
      summary: Pay the line's amount due automatically each month, up to a ceiling
      description: >
        Spec 058. From the customer's own dollar account (the line's when none is named). Nothing is taken
        until it is confirmed, and then on each due day the core's amount due, never more than the ceiling —
        the figure the customer signs. One asked for or on per line.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - cap_minor
              properties:
                cap_minor:
                  type: integer
                  minimum: 1
                from_account:
                  type: string
                  nullable: true
      responses:
        '201':
          description: Awaiting the customer's confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineAutopay'
        '404':
          description: Not the customer's account
        '409':
          description: No open line
          or one already asked for or on: null
  /credit-line/autopay/{id}:
    delete:
      tags:
        - lending
      summary: Stop it; nothing more is taken
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Stopped
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineAutopay'
        '404':
          description: Not the customer's
  /credit-line/autopay/{id}/confirm:
    post:
      tags:
        - lending
      summary: Confirm the standing authorisation
      description: >
        Spec 058. Confirmed as a transfer is (the kind is line_autopay and the id the autopay's), over the
        ceiling and "Your OxaBank credit line's amount due each month, from account …". Each due day — the
        core's business day — is then taken once.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: 'On'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineAutopay'
        '400':
          description: The confirmation does not hold
        '409':
          description: Already on or stopped
          or expired: null
  /credit-line/movements:
    post:
      tags:
        - lending
      summary: Say what to draw from the line or pay back to it
      description: >
        Spec 056. Nothing moves until it is confirmed. A draw goes into the customer's own dollar account (the
        line's when none is named) and is at most the limit less what is drawn, as the core says it now; a
        repayment comes from one and is at most what paying it off today takes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - amount_minor
              properties:
                kind:
                  type: string
                  enum:
                    - draw
                    - repayment
                amount_minor:
                  type: integer
                  minimum: 1
                account_id:
                  type: string
                  nullable: true
      responses:
        '201':
          description: Awaiting the customer's confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineMovement'
        '400':
          description: Over what is available
          or over the payoff: null
        '404':
          description: Not the customer's account
        '409':
          description: No open line
  /credit-line/movements/{id}/confirm:
    post:
      tags:
        - lending
      summary: Confirm a draw or a repayment, and have the core make it
      description: >
        Spec 056. Confirmed as a transfer is (the kind is line_draw or line_repayment and the id the
        movement's). A draw waits on the line's signed agreement. The line is locked, what is drawn read from
        the core again, and a draw goes only if it still fits: the core would take one past the limit. Once
        per movement, under its key.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Done, or refused with why
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LineMovement'
        '400':
          description: The confirmation does not hold
          or it no longer fits: null
        '409':
          description: Already done
          or expired: null
        '503':
          description: The core could not be asked; confirm again
          it is done once: null
  /recurring-pulls:
    get:
      tags:
        - transfers
      summary: The customer's standing authorisations to pull every week or month
      description: Spec 055. Up to 20, the newest first; refused ones are not listed.
      responses:
        '200':
          description: Whether pulling is offered here, and the standing authorisations
          content:
            application/json:
              schema:
                type: object
                required:
                  - available
                  - recurring
                properties:
                  available:
                    type: boolean
                  recurring:
                    type: array
                    items:
                      $ref: '#/components/schemas/RecurringPull'
              example:
                available: true
                recurring:
                  - id: 0199b000-0000-7000-8000-0000000000ee
                    state: active
                    said: 50.00 USD every month. Next on 2026-11-05.
                    linked_account_id: 0199b000-0000-7000-8000-000000000001
                    from: From Ana Rivera ••6789
                    to_account: '4'
                    amount_minor: 5000
                    currency: USD
                    every: month
                    first_on: '2026-10-05'
                    next_on: '2026-11-05'
                    taken: 1
                    mandate: >-
                      I authorise OxaBank to debit 50.00 USD every month, starting on 2026-10-05, from the
                      checking account ending 6789 at routing number 021000021, held by Ana Rivera, and to
                      credit it to my OxaBank account, until I stop it. I can stop it in the app at any time
                      before a debit is due. If a debit was not authorised, I can dispute it with my bank.
                    authorize: null
                    auth_method: device_key
                    created_at: '2026-10-05T09:00:00Z'
                    stopped_at: null
    post:
      tags:
        - transfers
      summary: Say what to pull, every week or month, from which day
      description: >
        Spec 055. Nothing is taken until it is confirmed, and then only on the days it names. From a linked
        account (verified, where the bank verifies) into the customer's own dollar account; each pull at most
        PLATFORM_ACH_PULL_MAX_MINOR; the first day today or later, within a year. The answer carries the
        recurring authorisation's text and what to sign: the amount and "<From holder ••last4>, every
        week|month from <first day>".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - linked_account_id
                - to_account
                - amount_minor
                - every
                - first_on
              properties:
                linked_account_id:
                  type: string
                  format: uuid
                to_account:
                  type: string
                amount_minor:
                  type: integer
                  minimum: 1
                every:
                  type: string
                  enum:
                    - week
                    - month
                first_on:
                  type: string
                  format: date
      responses:
        '201':
          description: Awaiting the customer's confirmation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecurringPull'
        '400':
          description: Over the per-pull limit
          unverified: null
          not a dollar account: null
          or not a schedule: null
        '404':
          description: Not the customer's linked account or account
        '503':
          description: Pulling is not set up here
  /recurring-pulls/{id}:
    delete:
      tags:
        - transfers
      summary: Stop it; nothing more is taken
      description: Spec 055. Pulls already sent go on as any pull. Stopping twice is the same.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Stopped
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecurringPull'
        '404':
          description: Not the customer's
  /recurring-pulls/{id}/confirm:
    post:
      tags:
        - transfers
      summary: Confirm the standing authorisation
      description: >
        Spec 055. Confirmed as a transfer is (the kind is recurring_pull and the id the authorisation's): the
        recurring authorisation on file. Nothing is taken now. On each due day the platform starts one pull
        under it, for exactly its amount from exactly its account, never before the day and once per day; a
        pull over the 24-hour limit is refused that day and said.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Active
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecurringPull'
        '400':
          description: The confirmation does not hold
          or the account is not verified: null
        '409':
          description: Already confirmed or stopped
          or expired: null
  /pulls/{id}/confirm:
    post:
      tags:
        - transfers
      summary: Authorise the debit, and have it asked of the customer's bank
      description: >
        Spec 048. Confirmed as a transfer is (the kind is pull and the id the pull's): this confirmation is
        the debit's authorisation on file. Column is asked for one ACH debit (WEB) under the pull's key. A
        settled debit is proposed as a credit to a second person (pull_credit); one returned after it was
        credited is proposed back (pull_reversal).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Sent, or refused with why
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pull'
        '400':
          description: The confirmation does not hold
          or over a limit: null
        '409':
          description: Already confirmed
          or expired: null
        '503':
          description: The bank could not be asked; nothing was taken
  /sign-in:
    get:
      tags:
        - sessions
      summary: Where the apps send a person to sign in or sign up
      description: >
        Spec 043. The identity provider by its own name, which is the issuer in its tokens and where browsers
        and apps reach it, the client each app signs in as (the authorization-code flow with PKCE, RFC 8252 on
        the phone), and whether this is a sandbox, where the sandbox's people can be signed in as by name.
        Asked before anybody is signed in; it says nothing of anybody.
      security: []
      responses:
        '200':
          description: The provider and the apps' clients
          content:
            application/json:
              example:
                issuer: https://id.your-bank.example/realms/oxabank
                clients:
                  app: oxabank-app
                  backoffice: oxabank-backoffice
                scope: openid profile email
                sandbox: true
              schema:
                type: object
                required:
                  - issuer
                  - clients
                  - scope
                  - sandbox
                properties:
                  issuer:
                    type: string
                  clients:
                    type: object
                    required:
                      - app
                      - backoffice
                    properties:
                      app:
                        type: string
                      backoffice:
                        type: string
                  scope:
                    type: string
                  sandbox:
                    type: boolean
        '503':
          description: The identity provider did not answer
  /tenants:
    get:
      tags:
        - tenants
      summary: List the tenants the caller belongs to
      description: >
        Every tenant the caller is a member of, by slug, and no other. A caller who belongs to several has to
        say which, and every route refuses to guess; this is where they find out which. Asked before there is
        a session, since a session is begun in one tenant: a valid token is all it needs. A token that belongs
        nowhere gets an empty list. Nothing of how the core addresses a tenant.
      responses:
        '200':
          description: The caller's tenants
          content:
            application/json:
              example:
                - id: 01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e10
                  slug: oxabank
                  display_name: OxaBank
                  status: active
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Tenant'
        '401':
          description: No token, or one the issuer did not sign
        '503':
          description: No store
  /approvals:
    get:
      tags:
        - approvals
      summary: List approvals
      parameters:
        - name: state
          in: query
          schema:
            $ref: '#/components/schemas/ApprovalState'
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApprovalView'
          description: Bare array, newest proposed first. Not PagedApprovals - there is no total and no items wrapper.
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - approvals
      summary: Propose a change to a system of record
      description: >
        Creates an approval in state `proposed`. Nothing is written to the system of record here. The proposer
        is taken from the token, never from the body.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewApproval'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalView'
          description: >-
            Not in this group's list; noted because it returns the same ApprovalView (state always "proposed",
            decided_by null).
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /approvals/{id}/decision:
    post:
      tags:
        - approvals
      summary: Approve or reject
      description: >
        The decider is taken from the token. A decider identical to the proposer is refused with 409; role
        separation is enforced here, on the server, and cannot be bypassed by a client. Concurrent decisions
        resolve to exactly one winner.
      parameters:
        - $ref: '#/components/parameters/PathId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - decision
              properties:
                decision:
                  type: string
                  enum:
                    - approve
                    - reject
                reason:
                  type: string
                  nullable: true
                  description: Optional; null accepted. Stored as rejection_reason on BOTH approve and reject.
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalView'
          description: >
            Re-read after execution, so for an approve the state is usually already `executed` or `failed`,
            not `approved`; for a reject it is `rejected`.
        '400':
          description: decision is neither "approve" nor "reject"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: Already decided, or the decider is the proposer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /audit:
    get:
      tags:
        - audit
      summary: Search the audit trail
      description: Searching by correlation id returns the whole journey of one payment (SC-003).
      parameters:
        - name: correlation_id
          in: query
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AuditEntry'
          description: >-
            Bare array, the tenant's WHOLE chain in ascending id order (or its entries for one
            correlation_id). offset/limit are ignored.
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /audit/verify:
    post:
      tags:
        - audit
      summary: Re-derive the hash chain and report the first divergence
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainVerification'
          description: Documented from the code
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /proposals/{id}:
    get:
      tags:
        - proposals
      summary: Retrieve an automated proposal with its evidence and provenance
      description: >
        Who answered, which version, and the SHA-256 of exactly what was asked, so the enquiry can be
        reproduced. The caller's tenant only; anyone else's, or an id that is not a uuid, is 404.
      parameters:
        - $ref: '#/components/parameters/PathId'
      responses:
        '200':
          description: The proposal as recorded
          content:
            application/json:
              example:
                id: 01a0f1c2-0000-7000-8000-000000000001
                subject_type: screening_match
                subject_ref: cust-1|ofac|12345@2026-09-30
                classification: likely_false_positive
                explanation: >-
                  The names are close, but the dates of birth disagree: most likely two different people with
                  similar names.
                suggested_action:
                  verdict: cleared
                confidence: 0.8
                evidence:
                  - code: dob_conflict
                    detail: Born 1970-03-02; the entry says 1981-11-05.
                provenance: live
                provider: rule-based
                provider_version: screening-rules-1
                prompt_hash: 4f2a0c9e0b1d5e6a7c8b9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c
                created_at: '2026-09-30T14:02:11Z'
              schema:
                $ref: '#/components/schemas/Proposal'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such proposal for this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /events/replay:
    post:
      tags:
        - events
      summary: Replay a topic's published events from a chosen position
      description: >
        Sends the tenant's published events for `topic` again, in outbox order, with their original ids, from
        `earliest`, `latest` or a position — a count of the tenant's published events, all topics together.
        Consumers deduplicate on the id, so replay converges rather than duplicating effects. Nothing is
        marked. At most `limit` events (default 1000) are looked at per call; `next` is where the following
        call resumes. Audited.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReplayRequest'
            example:
              topic: platform.core
              from: earliest
              limit: 1000
      responses:
        '202':
          description: Replayed
          content:
            application/json:
              example:
                topic: platform.core
                sent: 42
                from: 0
                next: 57
              schema:
                $ref: '#/components/schemas/BackofficeReplayResult'
        '400':
          description: Not a topic the platform publishes to, or not a position
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '415':
          description: Content-Type is not application/json (axum Json rejection, plain text).
        '422':
          description: >
            Body is JSON but not a ReplayBody: missing `topic`/`from`, `from` a negative or fractional number
            or a non-string non-number, `limit` negative (axum, plain text).
        '500':
          description: The outbox could not be read (ReplayError::Store). Nothing sent, not audited.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: The bus could not be reached or stopped answering; `next` says where to resume
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/BackofficeReplayInterrupted'
                  - $ref: '#/components/schemas/Problem'
  /backoffice/events/dead-letters:
    get:
      tags:
        - events
      summary: What the relay gave up on
      description: |
        Outbox rows dead-lettered after their bounded attempts, or at once when not decodable. Oldest first.
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int64
            default: 100
            minimum: 1
            maximum: 500
          description: Clamped to 1..500 rather than refused; 0 or negatives become 1.
      responses:
        '200':
          description: Dead letters
          content:
            application/json:
              example:
                dead_letters:
                  - id: 01a0e88f-2b58-7e09-094e-9720341ea93d
                    type: transfer.posted
                    attempts: 8
                    last_error: 'bus unavailable: connection refused'
                    created_at: '2026-09-28T12:00:00Z'
              schema:
                $ref: '#/components/schemas/BackofficeDeadLetterList'
        '400':
          description: '`limit` is not an integer. axum Query rejection: plain-text body, not a Problem.'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '500':
          description: The outbox could not be read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: No database configured (Problem), or the identity provider is unreachable (plain text).
  /backoffice/events/dead-letters/{id}/requeue:
    post:
      tags:
        - events
      summary: Put a dead letter back in the queue
      description: Attempts cleared; the relay sends it like any other. Audited.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Back in the queue
          content:
            application/json:
              example:
                id: 01a0e88f-2b58-7e09-094e-9720341ea93d
                state: pending
              schema:
                $ref: '#/components/schemas/BackofficeDeadLetterRequeued'
        '400':
          description: >-
            `id` is not a UUID (axum Path rejection, plain text), or the caller has several tenant memberships
            (Problem).
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No dead letter with that id for this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '500':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: No database configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /loans/mine:
    get:
      tags:
        - lending
      summary: The caller's own drawn loans, with where each stands today
      description: >
        Every figure is derived at the moment of asking from the schedule frozen at disbursement and the
        repayments received. The platform holds no balance, so a client that cached one would cache a number
        that changes at midnight with no write.
      responses:
        '200':
          description: Loans, each with a derived position or a statement that it is not drawn
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyLoans'
  /loans/{id}/schedule:
    get:
      tags:
        - lending
      summary: One loan's instalments, each marked paid, part-paid or due
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The frozen schedule and the position derived from it
        '404':
          description: Not the caller's loan. Never 403 - saying an id exists is saying an id exists
        '409':
          description: The loan has not been drawn, so it has no schedule yet
  /backoffice/loans/disbursements:
    post:
      tags:
        - lending
        - back-office
      summary: Propose drawing a loan
      description: >
        Proposes; does not disburse. A second person approves, and approving freezes the schedule, records the
        day and - with `to_account` named and the bank's lending account configured
        (`PLATFORM_LENDING_ACCOUNT`) - pays the principal into it through the core under that approval, keyed
        by it. `to_account` must be one of the borrower's own accounts in the loan's currency, checked against
        the core when proposed and again when carried out; any other is refused with 400. Without it the
        approval records the day and its execution record says `money_moved: false`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - application_id
                - note
              properties:
                application_id:
                  type: string
                  format: uuid
                note:
                  type: string
                  minLength: 10
                  description: Trimmed, char count >= 10, else 400.
                to_account:
                  type: string
                  nullable: true
                  description: Optional; must be one of the borrower's core accounts in the loan's currency, else 400.
            example:
              application_id: 01a0d3b6-95d6-76fe-9497-a001c02eb20a
              note: draw into the borrower's account
              to_account: '1'
      responses:
        '202':
          description: Proposed. Nothing is drawn until a second person agrees
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeLoanDisbursementProposal'
        '400':
          description: No reason given, or an account that is not the borrower's
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/loans/repayments:
    post:
      tags:
        - lending
        - back-office
      summary: Propose recording a repayment received
      description: >
        `received_on` is the day the money arrived, not today. An operator who could record a repayment alone
        could clear a customer's arrears alone, so this proposes. With `from_account` - one of the borrower's
        own accounts, checked against the core - approving it takes the amount from that account into the
        bank's lending account through the core. Without it, the money came from outside (a rail, a branch)
        and is recorded as received.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - application_id
                - amount_minor
                - received_on
                - note
              properties:
                application_id:
                  type: string
                  format: uuid
                amount_minor:
                  type: integer
                  format: int64
                  minimum: 1
                  description: <= 0 is 400. The currency is the agreement's, never the request's.
                received_on:
                  type: string
                  format: date
                  description: The day the money arrived. After today (UTC) is 400.
                external_ref:
                  type: string
                  nullable: true
                note:
                  type: string
                  minLength: 10
                  description: Trimmed, char count >= 10, else 400.
                from_account:
                  type: string
                  nullable: true
                  description: >
                    One of the borrower's own core accounts, in the loan's currency (checked against the core;
                    any failure is 400). Absent/null = money from outside, recorded as received.
            example:
              application_id: 01a0d3b6-95d6-76fe-9497-a001c02eb20a
              amount_minor: 350
              received_on: '2026-09-24'
              note: first instalment from the borrower's account
              from_account: '1'
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposed'
        '400':
          description: Nothing to repay, a day that has not happened, or an account that is not the borrower's
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such loan application in this tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: The loan has not been drawn, so there is nothing to repay.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/loans/arrears:
    get:
      tags:
        - lending
        - back-office
      summary: Loans in arrears, worst first
      description: >
        Ordered by days past due and not by amount. A customer three months behind on a small loan is a worse
        problem than one three days behind on a large one.
      responses:
        '200':
          description: Late loans, longest overdue first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeArrearsQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/exchange/completions:
    post:
      tags:
        - exchange
        - back-office
      summary: Propose completing an exchange left half done
      description: >
        For a quote whose money out moved and whose money in did not. Proposes the missing leg; a different
        person approves it. The executor first asks the core whether the leg exists after all — its reference
        starts with the quote's id and `:in` — and records it if so. Otherwise it moves the buy amount from
        the bank's account in the buy currency to the customer's buy account under that approval, keyed by the
        quote and the approval, never under the original leg's key, whose refusal the core may have cached.
        Only a half-done quote can be proposed; any other is 409.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - quote_id
                - note
              properties:
                quote_id:
                  type: string
                  format: uuid
                note:
                  type: string
                  minLength: 10
                  description: Trimmed, char count >= 10, else 400.
            example:
              quote_id: 01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
              note: bank USD account refunded, completing the customer's exchange
      responses:
        '202':
          description: Proposed. Nothing moves until a second person approves
          content:
            application/json:
              example:
                approval_id: 01992d20-1a2b-7c3d-8e4f-5a6b7c8d9e02
                state: proposed
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposed'
        '400':
          description: No reason given
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a quote in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: >
            The quote is not half done — finished, never started, or refused — or a completion for it is
            already waiting for a second person
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/transfers:
    get:
      tags:
        - transfers
        - back-office
      summary: Transfers held for a person to decide about
      description: >
        Ordered by how long the customer has waited, not by amount. Each row names the assessment that stopped
        it so an analyst reads the reasons rather than guessing.
      responses:
        '200':
          description: Held transfers, longest waiting first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeRiskHeldTransferQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/transfers/{id}/resolution:
    post:
      tags:
        - transfers
        - back-office
      summary: Propose releasing or refusing a held transfer
      description: >
        Releasing held funds is named by the platform’s rules as something no operator completes alone, so
        this proposes and a second person approves. A refusal must say why; the customer is entitled to the
        reason.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '202':
          description: Proposed. The transfer stays held until a second person agrees
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: decision is not release|refuse, or note shorter than 10 trimmed chars.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: >-
            The id is not among the (first 500) held transfers of this tenant; it may already have been
            resolved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - decision
                - note
              properties:
                decision:
                  type: string
                  enum:
                    - release
                    - refuse
                  description: Anything else is 400.
                note:
                  type: string
                  minLength: 10
                  description: >
                    Trimmed, char count >= 10, else 400. Stored trimmed as the approval subject's `why` (for a
                    refusal it is the reason the customer is given).
  /backoffice/funding/{id}/credit:
    post:
      tags:
        - crypto
        - back-office
      summary: Propose crediting a funding that has settled
      description: >
        The state the platform could not previously express: funding could be started and failed, and nothing
        recorded the money arriving, so a converted deposit sat unconverted for ever. Crediting a customer is
        money moving, so this proposes and a second person approves.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '202':
          description: Proposed. The customer is not credited until a second person agrees
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: credited_minor <= 0, or note shorter than 8 trimmed BYTES.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such funding attempt in this tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: The funding is not at a stage that can be credited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - credited_minor
                - note
              properties:
                credited_minor:
                  type: integer
                  format: int64
                  minimum: 1
                  description: What the customer is credited, in their currency's minor units. <= 0 is 400.
                note:
                  type: string
                  minLength: 8
                  description: Trimmed BYTE length (str::len) >= 8, else 400.
  /accounts:
    get:
      tags:
        - accounts
      summary: Every account the caller holds
      description: >
        Figures come from the accounting core, relayed. Where the core cannot answer, the balance field is
        absent rather than zero - a screen can recover from "we could not reach your balance" and cannot
        recover from being told there is nothing there.
      responses:
        '200':
          description: Accounts, each with its provenance and a balance that may be absent
          content:
            application/json:
              example:
                - id: '4'
                  name: OxaBank Account
                  kind: savings
                  currency: USD
                  balance_minor: 113833
                  provenance: live
                  account_number: '000002000000'
                  routing_number: '121145433'
                - id: '9'
                  name: OxaBank EUR Account
                  kind: savings
                  currency: EUR
                  balance_minor: 0
                  provenance: live
                  iban: GB45TCCL00000000000001
                  bic: TCCLGB31
                - id: '12'
                  name: Global Legal Entity Identifier Foundation
                  kind: business
                  currency: USD
                  balance_minor: 400000
                  provenance: live
                  business: Global Legal Entity Identifier Foundation
                  business_role: payer
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AccountView'
  /accounts/usd:
    post:
      tags:
        - accounts
      summary: Open the caller's own USD account
      description: >
        Spec 052. For a person the bank opened only as a profile acting for a company. One each: refused when
        the caller already holds a USD account of their own. Opened in the core on the customer's own
        confirmation of "A new USD account", 0.00 USD, as for a payment.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - authenticated_at
                - evidence_ref
                - signed_amount
                - signed_currency
                - signed_payee
              properties:
                method:
                  type: string
                authenticated_at:
                  type: string
                  format: date-time
                evidence_ref:
                  type: string
                signed_amount:
                  type: string
                  example: '0.00'
                signed_currency:
                  type: string
                  example: USD
                signed_payee:
                  type: string
                  example: A new USD account
                signature:
                  type: string
                  nullable: true
      responses:
        '201':
          description: Opened
          content:
            application/json:
              example:
                account_id: '14'
                currency: USD
                money_moved: false
              schema:
                type: object
        '400':
          description: Not an authentication method, or a confirmation of something else
        '409':
          description: The bank has not opened the caller, or they already hold a USD account
        '503':
          description: No core, or no USD product configured
  /accounts/euro:
    post:
      tags:
        - accounts
      summary: Open the caller's own euro account, with its IBAN, or carry on opening it
      description: >
        Spec 032. One per customer. Opens a EUR account in the core and an account in the customer's name at
        the provider, both under the platform's own reference, so asking again resumes rather than repeats.
        Takes the customer's confirmation bound to "0.00 EUR to A new EUR account". The holder's name, email,
        phone and address go to the provider and are not kept by the platform.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EuroAccountOpenBody'
            example:
              first_name: Ana
              last_name: García
              email: ana@example.com
              phone: +34 600 000 000
              address:
                line_1: Calle Mayor 1
                city: Madrid
                postal_code: '28013'
                country_code: ES
              method: device_biometric
              authenticated_at: '2026-10-01T10:00:00Z'
              evidence_ref: no-identity-provider:euro-account
              signed_amount: '0.00'
              signed_currency: EUR
              signed_payee: A new EUR account
      responses:
        '200':
          description: The account, `open` with its IBAN and BIC, or `opening` with why it stopped
          content:
            application/json:
              example:
                id: '9'
                currency: EUR
                iban: GB45TCCL00000000000001
                bic: TCCLGB31
                account_number: null
                routing_number: null
                state: open
                failure: null
              schema:
                $ref: '#/components/schemas/EuroAccountView'
        '400':
          description: >
            A holder detail that will not do (the answer says which), not an authentication method, a
            confirmation of something else, or the provider refusing to open for them
        '409':
          description: The caller has no account with the bank to open it beside
        '503':
          description: >
            Euro accounts are not offered here (no provider or no euro product configured), or the core or the
            provider did not answer; asking again carries on
  /accounts/us-details:
    post:
      tags:
        - accounts
      summary: Issue the caller's US account details for their dollar account, or show them
      description: >
        Spec 037. Column issues an account number under the bank's own Column account, once per customer,
        under the platform's own reference, so asking again shows the same number. Takes the customer's
        confirmation bound to "0.00 USD to Your US account details". Column is sent no holder's details. Money
        that arrives by ACH or wire is proposed as a credit and credited on a second person's approval.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - method
                - authenticated_at
                - evidence_ref
                - signed_amount
                - signed_currency
                - signed_payee
              properties:
                method:
                  type: string
                authenticated_at:
                  type: string
                  format: date-time
                evidence_ref:
                  type: string
                signed_amount:
                  type: string
                signed_currency:
                  type: string
                signed_payee:
                  type: string
                signature:
                  type: string
                  description: >-
                    With method device_key, the device key's signature over oxabank-confirm/1, the kind, the
                    id, the amount, the currency and the payee, one per line; base64, DER or r and s; the kind
                    is us_details and the id -. With method passkey, the WebAuthn assertion as four base64url
                    parts joined by dots: the credential id, the authenticator data, the client data JSON and
                    the signature; its challenge is SHA-256 of that same message.
            example:
              method: device_biometric
              authenticated_at: '2026-10-01T10:00:00Z'
              evidence_ref: no-identity-provider:us-details
              signed_amount: '0.00'
              signed_currency: USD
              signed_payee: Your US account details
      responses:
        '200':
          description: The details, `open` once issued, or `opening` with why they are not yet
          content:
            application/json:
              example:
                id: '4'
                currency: USD
                iban: null
                bic: null
                account_number: '000002000001'
                routing_number: '121145433'
                state: open
                failure: null
              schema:
                $ref: '#/components/schemas/EuroAccountView'
        '400':
          description: Not an authentication method, a confirmation of something else, or Column refusing
        '409':
          description: The caller has no account with the bank, or no dollar account yet
        '503':
          description: >
            US account details are not offered here (no Column configured), or the core or Column did not
            answer; asking again carries on
  /accounts/{id}/days:
    get:
      tags:
        - accounts
      summary: What the caller's account held at the end of each closed day, newest first
      description: >
        Spec 033. Each balance is the core's answer when the bank closed the day, kept with the day and the
        subject it belonged to; the last 90 closed days. Somebody else's account, or one with no closed day,
        answers an empty list. `closing_minor` is null where the core could not answer for the account at the
        close.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The closed days
          content:
            application/json:
              example:
                days:
                  - day: '2026-09-30'
                    closing_minor: 112833
                    currency: USD
              schema:
                $ref: '#/components/schemas/ClosingBalanceList'
  /accounts/{id}/statement:
    get:
      tags:
        - accounts
      summary: One account's statement, newest first
      description: >
        The account's own movements as the core booked them — never the general ledger. `amount_minor` is
        signed from this account's point of view. A transfer between two accounts the same customer holds
        appears on both, negative on one and positive on the other. A reversed movement, a hold and a hold's
        release are not listed.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Movements, each with the day it was booked and the platform's reference where the platform made it
          content:
            application/json:
              example:
                - id: '75'
                  posted_on: '2026-09-29'
                  amount_minor: -100
                  currency: USD
                  reference: 01a0ed3d-9514-7369-9754-6a0de877aa82
                  provenance: live
                - id: '70'
                  posted_on: '2026-09-28'
                  amount_minor: 5000000
                  currency: USD
                  reference: null
                  provenance: live
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/StatementEntryView'
        '404':
          description: Not the caller's account. Never 403
  /accounts/{id}/statement.pdf:
    get:
      tags:
        - accounts
      summary: One month of an account, as a PDF to keep
      description: >
        Spec 060. Any account the caller may act on, for one calendar month: its opening and closing balances,
        what came in and went out, and every movement with the balance after it. Worked out from the core's
        balance today and the movements since, rendered by Typst; nothing is kept, so the same month asked
        twice is the same document.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: month
          in: query
          required: true
          schema:
            type: string
            pattern: ^[0-9]{4}-[0-9]{2}$
          example: 2026-10
      responses:
        '200':
          description: The statement
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '400':
          description: Not a month
          or one not begun: null
        '404':
          description: Not an account the caller may act on
        '503':
          description: The core could not be read
  /backoffice/cards:
    get:
      tags:
        - cards
        - back-office
      summary: Cards issued in this tenant
      responses:
        '200':
          description: Cards, with state and expiry. Never a card number (Principle VIII)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CardView'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/cards/limits:
    post:
      tags:
        - cards
        - back-office
      summary: Propose changing a card's spending limits
      description: >
        Proposes; does not change. An operator who could raise a customer's limit alone is the control not
        existing, and raising a limit is exactly what somebody who has taken over an account wants done.
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: reason shorter than 8 trimmed BYTES, or a resulting limit is negative.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such card in this tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - card_id
                - reason
              properties:
                card_id:
                  type: string
                  format: uuid
                per_transaction:
                  type: integer
                  format: int64
                  minimum: 0
                  nullable: true
                  description: Absent = unchanged; null = no limit (allowed on this operator path); negative = 400.
                daily:
                  type: integer
                  format: int64
                  minimum: 0
                  nullable: true
                  description: Absent = unchanged; null = no limit; negative = 400.
                monthly:
                  type: integer
                  format: int64
                  minimum: 0
                  nullable: true
                  description: Absent = unchanged; null = no limit; negative = 400.
                reason:
                  type: string
                  minLength: 8
                  description: Trimmed BYTE length >= 8, else 400. Stored UNtrimmed on the proposal.
  /backoffice/cards/holds/expire:
    post:
      tags:
        - cards
        - back-office
      summary: Release holds nobody resolved
      description: >
        Not optional. A capture or a release is a message and messages are lost; without this one dropped
        message holds a customer's money until somebody notices, and nobody notices.
      responses:
        '200':
          description: What was released
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyHoldsExpired'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/fraud:
    get:
      tags:
        - fraud
        - back-office
      summary: Payments flagged and not yet looked at, oldest first
      description: >
        Oldest rather than highest-scoring: a held payment is somebody standing still. The health block
        reports how many were decided with no answer from the provider, and a false-positive rate that is
        absent rather than zero when nothing has been reviewed.
      responses:
        '200':
          description: Open assessments and the queue's health
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeFraudQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/fraud/{id}:
    get:
      tags:
        - fraud
        - back-office
      summary: One assessment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The assessment, including whether it can still be stopped
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeFraudAssessment'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such assessment in this tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
    post:
      tags:
        - fraud
        - back-office
      summary: Record what an analyst found
      description: >
        A verdict on a payment that already settled records a loss; it does not reach into the past and stop
        it. The response says which of the two happened, because the word on the button and what it did are
        not the same thing.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The verdict and its consequence
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeFraudVerdictResult'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: Somebody else reviewed this first
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - verdict
                - note
              properties:
                verdict:
                  type: string
                  enum:
                    - legitimate
                    - fraudulent
                note:
                  type: string
                  minLength: 10
                  description: Trimmed, char count >= 10 (fraud::MINIMUM_NOTE; DB CHECK agrees), else 400.
  /cards:
    get:
      tags:
        - cards
      summary: The caller's own cards
      description: >
        State, expiry, limits, and what each card has spent and is holding today. Never a card number, a
        verification value or track data: none of the three is representable anywhere under `platform/`
        (Principle VIII), so this is not a field a client may ask for and be refused - it does not exist to
        ask for.
      responses:
        '200':
          description: Cards, each with its state and the room left under its limits
          content:
            application/json:
              example:
                - id: 8c2e5a10-9d33-4b72-8f01-6e7a9b0c1d2e
                  status: active
                  status_detail: Ready to use
                  last_four: '4417'
                  network: visa
                  expires_on: '2029-09-30'
                  residency: virtual
                  currency: USD
                  actions:
                    - freeze
                    - cancel
                  limits:
                    per_transaction: 500000
                    daily: 1000000
                    monthly: 5000000
                    remaining_today: 955500
                    remaining_this_month: 4870000
                  provenance: live
                  business_id: 01a10d4f-03dd-70dd-9c93-e83b3cfa4e7c
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CardView'
    post:
      tags:
        - cards
      summary: Get a card, for one of one's own accounts
      description: >
        Issued by the card issuer for one of the caller's own core accounts, in its currency. The platform
        stores the issuer's token and the last four — never a card number (Principle VIII) — and the card
        starts `issued`, for the customer to activate. At most five cards not cancelled. With no issuer
        connected, 503 and nothing is created. A sandbox issuer's card is `provenance: synthetic`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - account_ref
                - form
                - cardholder_name
              properties:
                account_ref:
                  type: string
                form:
                  type: string
                  enum:
                    - virtual
                    - physical
                cardholder_name:
                  type: string
                currency:
                  type: string
                  nullable: true
                  description: Defaults to the account's
            example:
              account_ref: '4'
              form: virtual
              cardholder_name: ANA RIVERA
      responses:
        '201':
          description: Issued
          content:
            application/json:
              example:
                id: 01a0e8c0-2b58-7e09-894e-9720341ea93d
                status: issued
                status_detail: this card has not been activated yet
                last_four: '0025'
                network: visa
                expires_on: '2029-09-30'
                residency: delegated
                currency: USD
                actions:
                  - activate
                  - cancel
                limits:
                  per_transaction: null
                  daily: null
                  monthly: null
                  remaining_today: null
                  remaining_this_month: null
                provenance: synthetic
              schema:
                $ref: '#/components/schemas/CardView'
        '400':
          description: Not one of the caller's accounts
          another currency: null
          or not a card form: null
        '409':
          description: Five cards already
          or the issuer refused: null
        '503':
          description: No card issuer is connected
          or it could not be reached: null
  /cards/{id}/actions:
    post:
      tags:
        - cards
      summary: Activate, freeze, unfreeze or cancel one's own card
      description: >
        The cardholder is the authoriser and no second person is involved (Principle II). Freezing only ever
        removes capability and is what somebody reaches for when their bag is gone; cancelling is terminal and
        also theirs, because it is their card. A transition that does not apply is refused saying so, never
        silently ignored.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - activate
                    - freeze
                    - unfreeze
                    - cancel
      responses:
        '200':
          description: The card as it now stands
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardView'
        '400':
          description: Not something that can be done to a card
        '404':
          description: Not the caller's card. Never 403 - saying an id exists is saying an id exists
        '409':
          description: The transition does not apply from the state the card is in
  /cards/{id}/limits:
    post:
      tags:
        - cards
      summary: Lower one's own spending limits
      description: >
        Lowering only. Raising is an operator decision under maker-checker at `/backoffice/cards/limits`,
        because a limit the customer can raise is not a limit - and the customer whose phone has been taken is
        exactly the one it protects. An omitted field keeps its current value. Removing a limit is raising it
        to infinity and is refused as a raise.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardLimitsBody'
      responses:
        '200':
          description: The card with its new limits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardView'
        '400':
          description: The change raises a limit, or the three are not consistent with one another
        '404':
          description: Not the caller's card
  /loans:
    get:
      tags:
        - lending
      summary: The caller's own loan applications, at whatever stage each has reached
      description: >
        `decision` is what the credit policy decided and stays so after a referral is decided by a person;
        that person's decision is `operator_decision`, null until then, and carries what was decided and why
        but never the members of staff who proposed and approved it. `decision.operator_note` is never sent
        here.
      responses:
        '200':
          description: Applications, each with its stage and the reason where it was declined
          content:
            application/json:
              example:
                - id: b41d7e88-0c25-4f39-a6d0-5e8f7a2b3c14
                  state: under_review
                  awaiting_us: true
                  is_final: false
                  amount_minor: 4500000
                  currency: USD
                  term_months: 24
                  purpose: A used car
                  decision: null
                  operator_decision: null
                  offer: null
                  core_loan_ref: null
                  created_at: '2026-09-18T08:30:00Z'
                  your_moves:
                    - withdraw
                - id: 01926f3a-8c21-7d3f-8e52-6b7c8d9e0f1a
                  state: approved
                  awaiting_us: true
                  is_final: false
                  amount_minor: 13000000
                  currency: USD
                  term_months: 60
                  purpose: null
                  decision:
                    outcome: referred
                    reasons:
                      - code: more_than_a_year_of_income
                        detail: more than a year of income
                    provider: zen
                    policy_version: sha256:90c2377d6a57d781b19ce19d921a4927f0ca0ef3d35f021e744b232eb905e8b6
                    provenance: synthetic
                    decided_at: '2026-09-28T12:00:00Z'
                  operator_decision:
                    decision: approve
                    reason: income verified against three payslips
                    decided_at: '2026-09-28T13:00:00Z'
                  offer: null
                  core_loan_ref: null
                  created_at: '2026-09-28T12:00:00Z'
                  your_moves:
                    - withdraw
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/LoanApplicationView'
    post:
      tags:
        - lending
      summary: Apply for a loan
      description: >
        Submitted straight away rather than saved as a draft: a draft nobody submits is a state the app has to
        explain, and an app can hold an unsent form perfectly well by itself. The request is quoted before it
        is stored, so one that cannot be scheduled is refused with the reason rather than accepted and found
        unworkable later. Affordability is the platform's own check and runs before anything external is
        asked.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount_minor
                - currency
                - term_months
                - monthly_income_minor
              properties:
                amount_minor:
                  type: integer
                  format: int64
                  minimum: 1
                currency:
                  type: string
                term_months:
                  type: integer
                  format: int32
                  minimum: 1
                  maximum: 480
                monthly_income_minor:
                  type: integer
                  format: int64
                monthly_obligations_minor:
                  type: integer
                  format: int64
                  default: 0
                purpose:
                  type: string
                  nullable: true
                credit_check:
                  type: object
                  nullable: true
                  required:
                    - consent
                    - consent_version
                    - subject
                  properties:
                    consent:
                      type: boolean
                      description: false is accepted and means no bureau call
                    consent_version:
                      type: string
                    subject:
                      type: object
                      required:
                        - given_names
                        - first_surname
                        - second_surname
                        - birth_date
                        - tax_id
                        - address
                      properties:
                        given_names:
                          type: string
                        first_surname:
                          type: string
                        second_surname:
                          type: string
                        birth_date:
                          type: string
                          format: date
                        tax_id:
                          type: string
                        address:
                          type: object
                          required:
                            - street
                            - neighbourhood
                            - municipality
                            - city
                            - region
                            - postcode
                          properties:
                            street:
                              type: string
                            neighbourhood:
                              type: string
                            municipality:
                              type: string
                            city:
                              type: string
                            region:
                              type: string
                            postcode:
                              type: string
      responses:
        '202':
          description: >
            Decided once, inside this request, by the credit decision provider: `approved` with the policy's
            figures and an offer priced from them (`indicative: false`, the principal capped at what was
            asked); `declined` with the policy's reasons; or `referred` to a person. Every way the provider
            can fail is a referral that names it — `no_decision_provider` (none configured),
            `decision_unavailable`, `decision_unusable`, `priced_offer_unaffordable` — never an approval or a
            decline. An approval moves no money: the loan is paid out only when an operator proposes the
            disbursement and a second person approves it. Every decision names its provider, the policy's
            version and its provenance (`synthetic` for the example policy).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoanApplicationView'
              examples:
                approved:
                  summary: Approved by the example policy, priced, nothing moved
                  value:
                    id: 01926f3a-7b10-7c2e-9d41-5a6b7c8d9e0f
                    state: approved
                    awaiting_us: true
                    is_final: false
                    amount_minor: 10000000
                    currency: USD
                    term_months: 24
                    purpose: A used car
                    decision:
                      outcome: approved
                      reasons:
                        - code: within_example_policy
                          detail: within the example credit policy
                      provider: zen
                      policy_version: sha256:90c2377d6a57d781b19ce19d921a4927f0ca0ef3d35f021e744b232eb905e8b6
                      provenance: synthetic
                      figures:
                        limit_minor: 10000000
                        rate_percent: '18.5'
                        max_term_months: 36
                      decided_at: '2026-09-28T12:00:00Z'
                      bureau:
                        bureau: Círculo de Crédito (sandbox-stub)
                        outcome: scored
                        score: 712
                        score_name: FICO Score
                        reasons:
                          - D8
                          - K0
                          - F1
                        inquiry_ref: '930001'
                        consent:
                          given_at: '2026-09-28T12:00:00Z'
                          version: circulo-consulta-v1
                    operator_decision: null
                    offer:
                      principal_minor: 10000000
                      term_months: 24
                      instalment_minor: 501661
                      total_repayable_minor: 12039841
                      total_interest_minor: 2039841
                      annual_rate_percent: '18.5'
                      indicative: false
                    core_loan_ref: null
                    created_at: '2026-09-28T12:00:00Z'
                    your_moves:
                      - withdraw
                declined:
                  summary: Declined by the policy, with its reason
                  value:
                    state: declined
                    decision:
                      outcome: declined
                      reasons:
                        - code: obligations_too_high
                          detail: existing obligations are more than 40% of monthly income
                      provider: zen
                      policy_version: sha256:4f2a…
                      provenance: synthetic
                      decided_at: '2026-09-28T12:00:00Z'
                    your_moves: []
                    awaiting_us: false
                    is_final: true
                    id: 01a0e1c2-0000-7000-8000-00000000d0c1
                    amount_minor: 10000000
                    currency: USD
                    term_months: 24
                    purpose: A used car
                    created_at: '2026-09-28T12:00:00.000Z'
                    core_loan_ref: null
                    offer: null
                referred_no_provider:
                  summary: No provider configured — a person decides
                  value:
                    state: referred
                    decision:
                      outcome: referred
                      reasons:
                        - code: no_decision_provider
                          detail: No credit decision provider is configured, so a person decides.
                      provider: none
                      policy_version: null
                      provenance: unavailable
                      decided_at: '2026-09-28T12:00:00Z'
                    your_moves:
                      - withdraw
                    awaiting_us: true
                    is_final: false
                    id: 01a0e1c2-0000-7000-8000-000000000e01
                    amount_minor: 10000000
                    currency: USD
                    term_months: 24
                    purpose: A used car
                    created_at: '2026-09-28T12:00:00.000Z'
                    core_loan_ref: null
                    offer: null
                referred_unusable:
                  summary: The policy answered something that is not a decision
                  value:
                    state: referred
                    decision:
                      outcome: referred
                      reasons:
                        - code: decision_unusable
                          detail: We could not decide this automatically, so a person will look at it.
                      provider: zen
                      policy_version: sha256:9c1d…
                      provenance: live
                      decided_at: '2026-09-28T12:00:00Z'
                    your_moves:
                      - withdraw
                    awaiting_us: true
                    is_final: false
                    id: 01a0e1c2-0000-7000-8000-000000000e02
                    amount_minor: 10000000
                    currency: USD
                    term_months: 24
                    purpose: A used car
                    created_at: '2026-09-28T12:00:00.000Z'
                    core_loan_ref: null
                    offer: null
        '400':
          description: The request cannot be scheduled, and the answer says which part of it cannot
  /loans/{id}/agreement:
    get:
      tags:
        - lending
      summary: A person's loan agreement, to read and to sign
      description: >
        Spec 075. Rendered and kept with its SHA-256 the first time an approved loan's is asked for, naming
        the account it is paid into (the person's own account in the loan's currency). 409 until the loan is
        approved, or when the person holds no account in its currency.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The agreement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoanAgreementView'
        '404':
          description: Not the caller's loan
        '409':
          description: Not approved yet, or no account in the loan's currency
  /loans/{id}/agreement/sign:
    post:
      tags:
        - lending
      summary: Sign the loan agreement with the device's confirmation
      description: >
        Spec 075. The confirmation binds the amount and the document's SHA-256 (`authorize`), checked as a
        payment's is: strong, fresh, the caller's. The loan becomes `accepted` (your_moves' `accept` is this),
        the signed copy is kept, and only now can it be paid out.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Signed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoanAgreementView'
        '400':
          description: The confirmation does not bind this document
          or is not strong or fresh: null
        '404':
          description: Not the caller's loan
        '409':
          description: Signed already, or not an approved loan
  /loans/{id}/withdraw:
    post:
      tags:
        - lending
      summary: Withdraw one's own application
      description: >
        Theirs to do at any point before the money moves. It is the customer's own decision about their own
        application, so no second person is involved (Principle II).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The application, now withdrawn
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoanApplicationView'
        '404':
          description: Not the caller's application. Never 403
        '409':
          description: The application is past the point where it can be withdrawn
  /loans/quote:
    post:
      tags:
        - lending
      summary: What would this cost?
      description: >
        No application, no record and no credit footprint. A customer working out whether they can afford
        something has not applied for anything. Give an income and the answer includes whether it is
        affordable and the arithmetic behind that; omit it and the answer is a schedule and nothing more. The
        rate is indicative and says so (Principle V).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount_minor
                - currency
                - term_months
              properties:
                amount_minor:
                  type: integer
                  format: int64
                  minimum: 1
                currency:
                  type: string
                term_months:
                  type: integer
                  format: int32
                  minimum: 1
                  maximum: 480
                monthly_income_minor:
                  type: integer
                  format: int64
                  nullable: true
                monthly_obligations_minor:
                  type: integer
                  format: int64
                  nullable: true
      responses:
        '200':
          description: The instalment, the total and, where an income was given, the affordability
          content:
            application/json:
              example:
                instalment_minor: 213480
                total_repayable_minor: 5123520
                total_interest_minor: 623520
                annual_rate_percent: '12.9'
                indicative: true
                instalments: []
                affordability:
                  affordable: true
                  detail: The instalment is 28% of what is left after your other commitments
              schema:
                $ref: '#/components/schemas/LoanQuote'
        '400':
          description: The request cannot be scheduled
  /backoffice/documents/{id}/pdf:
    get:
      tags:
        - lending
        - back-office
      summary: Any document in the tenant, as read and signed or the signed copy
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: signed
          in: query
          required: false
          schema:
            type: boolean
      responses:
        '200':
          description: The PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such document
          or not signed yet: null
  /backoffice/credit-lines:
    get:
      tags:
        - lending
        - back-office
      summary: Every credit line the bank opened, and what is proposed for it
      description: >
        Spec 059. Open, closed and charged-off lines, the newest first, each with the core's figures, how many
        days its oldest payment is late on the bank's day, its autopay, the proposal waiting on it and the
        last ones, and why it cannot be charged off now.
      responses:
        '200':
          description: The lines
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeCreditLines'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/credit-lines/{id}/{action}:
    post:
      tags:
        - lending
        - back-office
      summary: Propose a line's new limit, closing it, or charging it off
      description: >
        Spec 059. **This decides nothing.** One operator proposes with a reason (credit_line_limit,
        credit_line_closing, credit_line_charge_off); a second approves, and it is carried out against where
        the line stands then. One proposal waits on a line at a time. A new limit is at least 100.00 USD, on
        an open line; closing is of an open line; a charge-off needs the oldest payment
        PLATFORM_CHARGE_OFF_DAYS late, checked now and again when approved.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: action
          in: path
          required: true
          schema:
            type: string
            enum:
              - limit
              - close
              - charge-off
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 8
                limit_minor:
                  type: integer
                  minimum: 10000
                  description: For a new limit
      responses:
        '201':
          description: Proposed, awaiting a second person
          content:
            application/json:
              schema:
                type: object
                required:
                  - approval_id
                  - state
                  - subject
                properties:
                  approval_id:
                    type: string
                    format: uuid
                  state:
                    type: string
                    enum:
                      - proposed
                  subject:
                    type: object
                    additionalProperties: true
        '400':
          description: No reason
          or a limit under the least or the same: null
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such line or action
        '409':
          description: Not open, not late enough, or a proposal already waits on it
        '503':
          description: The core did not answer
  /backoffice/loans:
    get:
      tags:
        - lending
        - back-office
      summary: Applications waiting on the bank, oldest first
      description: >
        Carries `waiting_longest_since` as its own field: it is the figure an SLA is measured against, and
        leaving the console to compute it from a list it might page through is how a queue quietly stops being
        measured.
      responses:
        '200':
          description: The referral queue, its size, and how long the oldest has waited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeLoanQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/loans/moves:
    get:
      tags:
        - lending
        - back-office
      summary: A person's term loans still on the platform's schedule
      description: >
        Spec 072, US6. Each drawn, open person's loan the core does not keep yet, with what it owes by the
        platform's schedule, how late it is, and what the last try to move it found. `ready` says whether a
        term loan product is configured in the core.
      responses:
        '200':
          description: '`ready` and `loans`'
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - lending
        - back-office
      summary: Propose moving the loans into the core
      description: >
        Spec 072, the platform’s rule. A credit officer proposes, naming the loans or all of them; a second
        person within their authority over what the loans owe together approves (`loan_move`). Each loan is
        replayed in the core — opened on its offer, paid out and repaid on its own days, both booked — and
        compared instalment by instalment, to the cent; equal, the core keeps it; not, it is taken back whole
        and stays as it was, with the difference. Once per loan.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                loan_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                type: object
        '400':
          description: No reason
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: Nothing to move, or a loan named that cannot be
        '503':
          description: No term loan product in the core
  /backoffice/credit-lines/{id}/opening:
    post:
      tags:
        - lending
        - back-office
      summary: Propose opening a company's signed line
      description: >
        Spec 072. Once the company's people signed its agreement and any guarantee asked is signed, a credit
        officer proposes; a second person within their authority over its limit opens it in the core
        (`credit_line_opening`), naming the guarantor there where the core knows them.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - note
              properties:
                note:
                  type: string
                  minLength: 10
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such company line
        '409':
          description: Not signed, or opened already
  /backoffice/settings/lending:
    get:
      tags:
        - settings
        - back-office
      summary: How long a company's offer stays open for its signatures
      description: Spec 072, the platform’s rule. 30 days while the bank has set nothing.
      responses:
        '200':
          description: '`offer_days` and whether the bank `set` them'
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - settings
        - back-office
      summary: Propose a new period for a company's offer
      description: >
        Spec 072, the platform’s rule. A credit officer proposes; a supervisor approves (`lending_setting`).
        Offers decided after it take it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - offer_days
                - reason
              properties:
                offer_days:
                  type: integer
                  minimum: 1
                  maximum: 365
                reason:
                  type: string
                  minLength: 8
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                type: object
        '400':
          description: A period out of range, or no reason
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: The same change already waits
  /backoffice/loans/decisions:
    post:
      tags:
        - lending
        - back-office
      summary: Propose approving or declining a referred application
      description: >
        **This decides nothing.** It raises a proposal a different person approves (Principle II). The
        referral queue is exactly where a single-signature approval would be abused: it is the pile of
        applications an automated decision already declined to approve. A reason is required and a token one
        is refused, because an adverse-action notice has to be produced from the reasons actually given.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - application_id
                - decision
                - reason
              properties:
                application_id:
                  type: string
                  format: uuid
                decision:
                  type: string
                  enum:
                    - approve
                    - decline
                reason:
                  type: string
                  minLength: 8
                  description: Trimmed, byte length (str::len) >= 8, else 400.
                amount_minor:
                  type: integer
                  description: >
                    Spec 072. With `rate_percent` and `term_months`, the terms an approval is offered at,
                    priced from the first of next month; required to approve a company's referral, which has
                    no priced offer. All three or none.
                rate_percent:
                  type: string
                  example: '16.5'
                term_months:
                  type: integer
                  minimum: 1
                  maximum: 120
                guarantee_required:
                  type: boolean
                  description: Spec 072, the platform’s rule. A guarantee asked of one of the company's people.
                guarantee_minor:
                  type: integer
                  description: For this amount, at most the loan's; the whole debt when absent.
      responses:
        '202':
          description: >
            Proposed. Nothing is decided until a second person agrees; when they do, the application moves and
            the decision is written as its `operator_decision`, leaving the policy's `decision` as it was
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeLoanProposal'
        '400':
          description: Not a decision, or a reason too thin to produce a notice from
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /onboarding:
    get:
      tags:
        - onboarding
      summary: Where the caller is up to
      description: >
        The state is derived from what is outstanding and today's date, never stored, so evidence that expires
        becomes outstanding again with no job running. Checks the bank performs on its own are not listed as
        steps for the applicant.
      responses:
        '200':
          description: What is outstanding, what has expired, and what happens next
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingJourney'
    post:
      tags:
        - onboarding
      summary: Begin, or say plainly that we cannot
      description: >
        Eligibility is checked **before** anything is asked. Refusing somebody after they have photographed a
        passport is worse than refusing them before, and it is the same refusal. A refusal is recorded with
        its reason so support can retrieve it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - country
                - age_years
              properties:
                country:
                  type: string
                  description: ISO-3166 alpha-2 (not validated)
                age_years:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 129
                expected_monthly_minor:
                  type: integer
                  format: int64
                  minimum: 0
                  default: 0
                own_account:
                  type: boolean
                  default: true
                  description: >
                    Spec 052. A dollar account of their own when the bank opens them, or (false) only a
                    profile that acts for a company they were invited to; they may open one later with `POST
                    /accounts/usd`
      responses:
        '200':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingJourney'
        '201':
          description: Started, with the first outstanding requirement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingJourney'
        '400':
          description: Not eligible, with the reason. Recorded, not merely returned
        '409':
          description: A journey is already open for this caller
  /onboarding/steps/{kind}:
    post:
      tags:
        - onboarding
      summary: Submit one step of the journey
      description: >
        Records the step as **submitted**, never as satisfied. Supplying a document and somebody checking it
        are two events and this route is only the first. A kind the bank performs itself is refused saying so
        - "that one is ours to do, not yours" - which is the platform’s rule enforced at the door rather than
        only in the list the applicant is shown.
      parameters:
        - name: kind
          in: path
          required: true
          schema:
            type: string
            enum:
              - identity
              - address
              - tax_residency
              - source_of_funds
              - terms_accepted
          description: >
            `screening` parses but is refused: it is the bank's own check, not the customer's step. `identity`
            is refused too now: it is submitted with the document, at /onboarding/identity
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                provider_ref:
                  type: string
                  nullable: true
                  description: >
                    A reference held by whoever verified it. The platform stores no identity document and no
                    document number
                tax_residency:
                  type: array
                  description: >
                    Spec 076. Required for `tax_residency`: each country the customer is tax resident in (ISO
                    3166-1 alpha-2, once each) with its tax identification number where they have one. The
                    countries are kept to read; the numbers are sealed under the platform's key.
                  items:
                    type: object
                    required:
                      - country
                    properties:
                      country:
                        type: string
                        minLength: 2
                        maxLength: 2
                      tin:
                        type: string
                        nullable: true
                        maxLength: 40
      responses:
        '200':
          description: The journey, with this step now submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingJourney'
        '400':
          description: Not a step of this journey, or one the bank performs itself
        '404':
          description: No journey open for this caller
  /onboarding/identity:
    post:
      tags:
        - onboarding
      summary: Submit identity — the document's machine-readable zone and the face check's result
      description: >
        Spec 034. The phone reads the zone on the device and sends only its lines; the photo never leaves the
        phone. The platform reads the lines again itself (ICAO 9303: every check digit and the composite),
        refuses one that does not read cleanly or has expired, and a face check that did not pass, each with
        the reason; flags a date of birth that disagrees with the age declared; keeps the details a person
        needs to decide sealed (the document number to its last four); and records identity as submitted. A
        person records the verdict.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - mrz
                - liveness
              properties:
                mrz:
                  type: array
                  items:
                    type: string
                  description: Two lines of 44 (a passport) or three of 30 (an identity card)
                liveness:
                  type: object
                  required:
                    - passed
                  properties:
                    passed:
                      type: boolean
                    reference:
                      type: string
                      nullable: true
            example:
              mrz:
                - P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<
                - L898902C36UTO7408122F2904157ZE184226B<<<<<18
              liveness:
                passed: true
                reference: face-turned-both-ways
      responses:
        '200':
          description: The journey, with identity now submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingJourney'
        '400':
          description: >-
            The zone did not read cleanly, the document has expired, or the face check did not pass; the
            answer says which
        '404':
          description: No journey open for this caller
        '409':
          description: Identity is not asked of this applicant, or is already satisfied
  /onboarding/screening:
    post:
      tags:
        - screening
        - onboarding
      summary: Screen the caller as part of their onboarding
      description: >
        The response says `clear` or `in_review` and that is the whole of it. There is no field here a list
        name could occupy, which is the point: the customer is never told they matched a watchlist, or which
        one, by any means. An unreachable list source produces an error and never an empty result.
      responses:
        '200':
          description: Clear, or in review. Nothing about why
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingScreeningResult'
        '404':
          description: No journey open for this caller
  /business:
    get:
      tags:
        - business
      summary: The caller's applications for a business account
      description: >
        Spec 039. Each with what the registry said and when, the mismatches with what was declared, how many
        owners are named, and its state (`draft`, `submitted`, `rejected`, `opened`). The screening's outcome
        and an operator's reason are not here (tipping-off). `memberships`: every opened company the caller
        acts for and their role, whoever applied. Spec 052: a company without an LEI carries
        `registration_number` and `registry_name`; `registry_source` says who described it (`gleif`,
        `sandbox`, `manual` for an operator's look-up by hand, or `none`) and `provenance` how far to believe
        it (`live`, `synthetic`, `unavailable`), with `manual_record` once an operator has looked.
      responses:
        '200':
          description: Newest first
          content:
            application/json:
              example:
                applications:
                  - id: 01a0fe37-baed-720a-922b-d24c9981b6ad
                    legal_name: Global LEI Foundation
                    country: CH
                    lei: 506700GE1G29325QX363
                    registry:
                      lei: 506700GE1G29325QX363
                      legal_name: Global Legal Entity Identifier Foundation
                      entity_status: ACTIVE
                      registration_status: ISSUED
                      jurisdiction: CH
                      registered_as: CHE200595965
                      source: gleif
                    registry_read_at: '2026-10-02T20:04:10Z'
                    mismatches:
                      - code: legal_name
                        detail: >-
                          declared "Global LEI Foundation", the registry says "Global Legal Entity Identifier
                          Foundation"
                    owners_named: 2
                    state: submitted
                    account_id: null
              schema:
                type: object
    post:
      tags:
        - business
      summary: Start an application for a company, looked up in the registry
      description: >
        Spec 039. The caller is a customer the bank already knows. The LEI's ISO 17442 check digits are read
        first; the registry (`PLATFORM_REGISTRY`: GLEIF, or the sandbox's) is asked, and its record kept
        beside the declaration with the mismatches (`legal_name`, `country`, `entity_inactive`,
        `lei_not_issued`, or `registry_not_asked` where none is configured). Spec 052: a company without an
        LEI gives `registration_number`, `registry_name` and `registered_address` instead; no registry is
        asked (`registry_not_asked`), and an operator looks it up by hand (`POST
        /backoffice/business/{id}/lookup`) before it can be opened.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - legal_name
                - country
              properties:
                legal_name:
                  type: string
                country:
                  type: string
                  description: ISO 3166 alpha-2
                lei:
                  type: string
                  description: The company's LEI; or the three that follow
                registration_number:
                  type: string
                  description: Its number in its registry
                  for a company without an LEI: null
                registry_name:
                  type: string
                  description: The registry that issued it
                  like Companies House: null
                registered_address:
                  type: string
            examples:
              lei:
                value:
                  legal_name: Oxa Sandbox Trading Ltd
                  country: US
                  lei: 5299OXABSANDBOX00111
              no_lei:
                value:
                  legal_name: Oxa Corner Shop Ltd
                  country: GB
                  registration_number: SC123456
                  registry_name: Companies House
                  registered_address: 1 High Street, Edinburgh EH1 1AA
      responses:
        '201':
          description: Started, in `draft`
          content:
            application/json:
              schema:
                type: object
        '400':
          description: >-
            No legal name, not a country, not an LEI, no entity with that LEI at the registry, or neither an
            LEI nor a whole registration
        '409':
          description: The caller has no account with the bank, or the company already has a live application
        '503':
          description: The registry could not be asked
  /business/{id}/owners:
    put:
      tags:
        - business
      summary: Name the beneficial owners and submit
      description: >
        Spec 039. Each owner owns 25% or more or controls the company, is an adult, and the shares add up to
        at most 100%; the caller declares they may act for the company. The company and every owner are
        screened (an alert goes to the screening queue) and the owners are sealed to the application. Refused
        once the bank has decided.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - owners
                - authorised
              properties:
                authorised:
                  type: boolean
                owners:
                  type: array
                  items:
                    type: object
                    required:
                      - full_name
                      - date_of_birth
                      - nationality
                      - ownership_percent
                    properties:
                      full_name:
                        type: string
                      date_of_birth:
                        type: string
                        format: date
                      nationality:
                        type: string
                      ownership_percent:
                        type: integer
                        minimum: 0
                        maximum: 100
                      controls:
                        type: boolean
            example:
              authorised: true
              owners:
                - full_name: Ana Rivera
                  date_of_birth: '1980-05-01'
                  nationality: US
                  ownership_percent: 100
      responses:
        '200':
          description: Submitted
          content:
            application/json:
              schema:
                type: object
        '400':
          description: Not authorised, or an owner that will not do (the answer says which)
        '404':
          description: Not the caller's application
        '409':
          description: The bank has decided on it
  /business/{id}/loans:
    get:
      tags:
        - business
      summary: The company's loans and its credit line
      description: >
        Spec 072. Every person of the company sees what it asked for, the policy's answer in its own words,
        the offer and until when it can be signed, and what the platform read of the company with where each
        figure came from. Never what an operator wrote.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: '`your_role`, `loans` (term loans, newest first) and `line`'
          content:
            application/json:
              schema:
                type: object
        '404':
          description: The caller does not act for that company
    post:
      tags:
        - business
      summary: Ask for a term loan or a credit line for the company
      description: >
        Spec 072, the platform’s rule–the platform’s rule. The company's admins only. The bank's credit policy
        decides on what the company declares — its turnover over twelve months and what it pays other lenders
        each month — and what the platform reads of it: its age from its registry's record, its days with the
        bank, and what came into its accounts here over 90 days (`unavailable` when the core does not answer,
        never zero). A policy with no rules for a company refers it (`policy_not_for_companies`). An approval,
        the policy's or a credit officer's on a referral, may ask for a guarantee: `decision.guarantee`, its
        amount and never the officer's reason. A term loan's offer, and an offered line, can be signed for the
        bank's offer period, 30 days by default. One application waiting per company; one line.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - product
                - amount_minor
                - term_months
                - account_id
                - turnover_12m_minor
                - obligations_monthly_minor
              properties:
                product:
                  type: string
                  enum:
                    - term
                    - line
                amount_minor:
                  type: integer
                  minimum: 10000
                term_months:
                  type: integer
                  minimum: 1
                  maximum: 120
                purpose:
                  type: string
                account_id:
                  type: string
                turnover_12m_minor:
                  type: integer
                  minimum: 0
                obligations_monthly_minor:
                  type: integer
                  minimum: 0
            example:
              product: term
              amount_minor: 2000000
              term_months: 12
              purpose: stock
              account_id: '88'
              turnover_12m_minor: 60000000
              obligations_monthly_minor: 0
      responses:
        '201':
          description: Asked and decided
          content:
            application/json:
              schema:
                type: object
        '400':
          description: An amount, a term or an account that will not do (the answer says which)
        '403':
          description: Not one of the company's admins
        '404':
          description: The caller does not act for that company, or the account is not the company's
        '409':
          description: The company's account is not open, or it already has a loan waiting or a line
  /business/{id}/loans/{loan}:
    get:
      tags:
        - business
      summary: One of the company's term loans
      description: Spec 072. As in the list.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The loan
          content:
            application/json:
              schema:
                type: object
        '404':
          description: The caller does not act for that company, or it has no such loan
  /business/{id}/loans/{loan}/withdraw:
    post:
      tags:
        - business
      summary: Withdraw what the company asked for, before the money moves
      description: Spec 072. The company's admins; a term loan or an offered or signed line.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Withdrawn
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not one of the company's admins
        '404':
          description: The caller does not act for that company, or it has no such loan
        '409':
          description: It can no longer be withdrawn
  /business/{id}/loans/{loan}/agreement:
    get:
      tags:
        - business
      summary: The company's loan agreement, rendered and kept the first time it is asked for
      description: >
        Spec 072, the platform’s rule. Any of the company's people. The agreement by its SHA-256, what a
        signature binds (`authorize`), who has signed (`signatures`, by position), who is still needed
        (`needs`: `first`, `second`, or none) and why a second is (`second_because`: `threshold` or
        `per_payment`), the guarantee asked and where it stands, and the last day it can be signed.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The agreement
          content:
            application/json:
              schema:
                type: object
        '404':
          description: The caller does not act for that company, or it has no such loan
        '409':
          description: There is no offer to sign
  /business/{id}/loans/{loan}/agreement/sign:
    post:
      tags:
        - business
      summary: Sign the company's loan agreement
      description: >
        Spec 072, the platform’s rule. An admin signs first; at or above the company's threshold, or above
        that admin's own limit per payment, a second admin or approver — never the first — signs too. Each is
        that person's own confirmation over the agreement's amount and SHA-256, as a payment's. Once signed
        whole, and any guarantee asked for is signed, the offer is accepted and waits for the bank to pay it
        out. Refused after the offer's last day.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Signed; the agreement as it now stands
          content:
            application/json:
              schema:
                type: object
        '400':
          description: The confirmation does not bind this agreement, or is not strong or fresh
        '403':
          description: Not an admin (first), or not an admin or approver (second)
        '404':
          description: The caller does not act for that company, or it has no such loan
        '409':
          description: Signed already by the caller, signed whole, expired, or not to be signed
  /business/{id}/loans/{loan}/guarantor:
    post:
      tags:
        - business
      summary: Name the guarantor among the company's people
      description: >
        Spec 072, the platform’s rule. An admin, when the policy or a credit officer asked for a guarantee.
        The person is one the bank has checked and opened; a person still being checked, or turned down, is
        said only to be unable to guarantee. One named or signed at a time.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - member
              properties:
                member:
                  type: string
                  format: uuid
      responses:
        '201':
          description: Named
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not one of the company's admins
        '404':
          description: The caller does not act for that company, or no such person or loan
        '409':
          description: No guarantee is asked, one is named already, or that person cannot guarantee
  /business/{id}/loans/{loan}/payments:
    post:
      tags:
        - business
      summary: Repay the company's term loan, or draw on or repay its line
      description: >
        Spec 072, US3 and US4, the platform’s rule, the platform’s rule. A payer or an admin asks; nothing
        moves until they confirm it. A repayment is at most what paying off today takes; a draw at most what
        is available of the line. `authorize` is what the confirmation binds.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - amount_minor
              properties:
                kind:
                  type: string
                  enum:
                    - draw
                    - repayment
                amount_minor:
                  type: integer
                  minimum: 1
                account_id:
                  type: string
      responses:
        '201':
          description: Asked, waiting for its payer's confirmation
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not a payer or an admin of the company
        '404':
          description: The caller does not act for that company, or no such loan or account
        '409':
          description: More than it owes or than is available, or not paid out or open
  /business/{id}/loans/{loan}/payments/{payment}/confirm:
    post:
      tags:
        - business
      summary: Confirm a repayment or draw
      description: >
        Spec 072. Its payer's own confirmation, as a payment's. Within their limits and below the company's
        threshold it goes; across one it waits for a second signature and says which rule (`cosign_reason`). A
        draw goes only if it still fits under the limit, under a lock on the line.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: payment
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Done, or waiting for a second signature
          content:
            application/json:
              schema:
                type: object
        '400':
          description: The confirmation does not bind it, or is not strong or fresh
        '404':
          description: Not the caller's
        '409':
          description: Not waiting for them, no longer fits, or the core refused it
  /business/{id}/loans/{loan}/payments/{payment}/cosign:
    post:
      tags:
        - business
      summary: Give the second signature
      description: Spec 072. An admin or approver of the company, never its payer; it goes under their authority.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: payment
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Done
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not an admin or approver
        '409':
          description: Its own payer, not waiting, signed already, or no longer fits
  /business/{id}/loans/{loan}/payments/{payment}/decline:
    post:
      tags:
        - business
      summary: Decline one waiting for a second signature
      description: Spec 072. An admin or approver, never its payer.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: loan
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: payment
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Declined
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not an admin or approver, or its own payer
        '409':
          description: Not waiting for a second signature
  /guarantees:
    get:
      tags:
        - business
      summary: What the caller guarantees or is asked to guarantee
      description: Spec 072, the platform’s rule. Theirs to read even after they leave the company.
      responses:
        '200':
          description: The guarantees, newest first
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
  /guarantees/{id}:
    get:
      tags:
        - business
      summary: One guarantee, with its pages
      description: Spec 072. Rendered and kept the first time its guarantor asks.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The guarantee
          content:
            application/json:
              schema:
                type: object
        '404':
          description: Not the caller's
  /guarantees/{id}/sign:
    post:
      tags:
        - business
      summary: Sign a guarantee
      description: >
        Spec 072, the platform’s rule. The guarantor's own confirmation over the amount guaranteed and the
        guarantee's SHA-256. The company's loan is accepted if that was all it waited for.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Signed
          content:
            application/json:
              schema:
                type: object
        '400':
          description: The confirmation does not bind this guarantee, or is not strong or fresh
        '404':
          description: Not the caller's
        '409':
          description: Not waiting for their signature
  /guarantees/{id}/decline:
    post:
      tags:
        - business
      summary: Decline a guarantee
      description: Spec 072. The company's admin may name another.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Declined
          content:
            application/json:
              schema:
                type: object
        '404':
          description: Not the caller's
        '409':
          description: Not waiting for their answer
  /business/join:
    post:
      tags:
        - business
      summary: Join a company with the one-time code an admin handed over
      description: >
        Spec 041. A code is twelve characters, read without dashes or case, kept by the platform only as its
        SHA-256, used once, within seven days. Spec 052: a person the bank does not know yet joins too, as
        `being_checked`: the code is spent at once, and they see and do nothing of the company until the bank
        has opened them (`joined`), or turned them down (`could_not_join`, never why).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - code
              properties:
                code:
                  type: string
            example:
              code: ZAWM-E9EQ-UDZV
      responses:
        '200':
          description: A member now, with this role
          content:
            application/json:
              example:
                business_id: 01a0fe37-baed-720a-922b-d24c9981b6ad
                name: Global Legal Entity Identifier Foundation
                role: payer
                state: joined
              schema:
                type: object
        '400':
          description: No such code, used, or expired (all the same)
        '409':
          description: The caller already acts for the company
  /business/{id}/people:
    get:
      tags:
        - business
      summary: The company's people, its second-signature threshold and the payments waiting for one
      description: >
        Spec 041. For anyone who acts for the company. Each person by the name the core holds for them, their
        role and whether it is the caller; the threshold in minor units and a raise or removal waiting for a
        second person; the payments waiting for a second signature, with what the second signatory confirms
        (`authorize`). Spec 052: each person's `state`, `joined`, `being_checked` (invited, the bank still
        checking them; removing them withdraws the invitation) or `could_not_join` (no reason is given).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The company's people
          content:
            application/json:
              example:
                business_id: 01a0fe37-baed-720a-922b-d24c9981b6ad
                your_role: admin
                people:
                  - id: 01a10169-0ae0-7072-b919-91121a836dda
                    name: Ana Rivera
                    role: payer
                    you: false
                    added_at: '2026-10-03T10:57:00Z'
                    state: joined
                cosign_above_minor: 2000
                pending_change: null
                awaiting_cosignature:
                  - id: 01a10169-1000-7000-8000-000000000001
                    amount_minor: 3000
                    currency: USD
                    payee: Anyone
                    reference: big one
                    authorized_at: '2026-10-03T10:58:00Z'
                    authorize:
                      amount: '30.00'
                      currency: USD
                      payee: Anyone
              schema:
                type: object
        '404':
          description: The caller does not act for that company
  /business/{id}/invitations:
    post:
      tags:
        - business
      summary: A one-time code for a person to join the company with, shown once
      description: Spec 041. Admins only.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                  enum:
                    - admin
                    - payer
                    - approver
                    - viewer
      responses:
        '201':
          description: The code, shown this once
          content:
            application/json:
              example:
                code: ZAWM-E9EQ-UDZV
                role: payer
                expires_at: '2026-10-10T10:57:00Z'
              schema:
                type: object
        '400':
          description: Not a role
        '403':
          description: Not an admin of the company
        '404':
          description: The caller does not act for that company
  /business/{id}/people/{member}:
    put:
      tags:
        - business
      summary: Change a person's role
      description: Spec 041. Admins only; the company keeps at least one admin.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: member
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                  enum:
                    - admin
                    - payer
                    - approver
                    - viewer
      responses:
        '200':
          description: Changed
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not an admin
        '409':
          description: No such person, or the company would have no admin
    delete:
      tags:
        - business
      summary: Remove a person
      description: Spec 041. Admins only; the last admin stays.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: member
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Removed
        '403':
          description: Not an admin
        '409':
          description: No such person, or the last admin
  /business/{id}/threshold:
    put:
      tags:
        - business
      summary: Set the amount from which a payment from the company's account needs a second signature
      description: >
        Spec 041. Admins only. Setting one or lowering it takes effect at once; raising or removing it
        (`null`) waits for a second admin or approver to confirm. Spec 052: a threshold is one currency's
        (`currency`, dollars when not said).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - above_minor
              properties:
                above_minor:
                  type: integer
                  nullable: true
                  minimum: 1
                currency:
                  type: string
                  default: USD
            example:
              above_minor: 100000
              currency: USD
      responses:
        '200':
          description: '`in_force`, `waits_for_a_second_person` or `unchanged`'
          content:
            application/json:
              example:
                effect: waits_for_a_second_person
              schema:
                type: object
        '400':
          description: Not more than nothing
        '403':
          description: Not an admin
  /business/{id}/threshold/confirm:
    post:
      tags:
        - business
      summary: Confirm a raise or removal of the threshold another person asked for
      description: Spec 041. An admin or an approver, not whoever asked for it. Spec 052, by currency.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                currency:
                  type: string
                  default: USD
      responses:
        '200':
          description: In force now
          content:
            application/json:
              example:
                currency: USD
                cosign_above_minor: 100000
              schema:
                type: object
        '403':
          description: Not an admin or an approver
        '409':
          description: Nothing waits, or the caller asked for it
  /business/{id}/accounts/euro:
    post:
      tags:
        - business
        - accounts
      summary: Open the company's euro account, or carry on opening it
      description: >
        Spec 052. Admins only. As `POST /accounts/euro`, with the company as the account's holder at the
        provider and the admin as its contact, confirmed by the admin; one per company. Its IBAN is shown with
        the company's accounts to the company's people.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EuroAccountOpenBody'
      responses:
        '200':
          description: The account as it now is
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EuroAccountView'
        '403':
          description: Not an admin
        '404':
          description: No such opened company the caller acts for
        '503':
          description: Euro accounts are not offered here
  /business/{id}/cards:
    get:
      tags:
        - business
        - cards
      summary: The company's cards, every holder's
      description: Spec 052. Admins only. Each card with its holder's membership and the card's own view.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The company's cards
          content:
            application/json:
              example:
                business_id: 01a10d4f-03dd-70dd-9c93-e83b3cfa4e7c
                cards:
                  - holder: 01a10d4f-0000-7000-8000-000000000002
                    yours: false
                    card:
                      id: 01a10d60-0000-7000-8000-000000000001
                      status: active
                      last_four: '4242'
                      currency: USD
                      business_id: 01a10d4f-03dd-70dd-9c93-e83b3cfa4e7c
              schema:
                type: object
        '403':
          description: Not an admin
    post:
      tags:
        - business
        - cards
      summary: Issue a card on the company's account to one of its people who pays
      description: >
        Spec 052. Admins only, for an admin or a payer, on one of the company's accounts, virtual. Issued
        through the card issuer as any card; its payments keep to its holder's limits and the company's
        threshold, and are declined above them. Removing the holder, or a role that no longer pays, cancels it
        at the issuer. An admin of the company freezes, unfreezes or cancels it with `POST
        /cards/{id}/actions`; activating it is its holder's.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - holder
                - account_id
                - cardholder_name
              properties:
                holder:
                  type: string
                  format: uuid
                  description: The person's membership id
                account_id:
                  type: string
                  description: One of the company's accounts
                cardholder_name:
                  type: string
                  maxLength: 26
                form:
                  type: string
                  enum:
                    - virtual
                  default: virtual
      responses:
        '201':
          description: Issued
          content:
            application/json:
              schema:
                type: object
        '400':
          description: Not virtual, no name, or not the company's account
        '403':
          description: Not an admin
        '409':
          description: The holder neither pays nor administers, or holds as many cards as allowed
        '503':
          description: No card issuer is connected
  /business/{id}/people/{member}/limits:
    get:
      tags:
        - business
      summary: What one person may pay alone, and what they have paid alone today
      description: >
        Spec 052. The person themselves, or an admin of the company. By currency: the limit per payment and
        per day (`null`, none), a loosening waiting for a second person, and what they paid alone from the
        company in the bank's day (`spent_today_minor`), a payment waiting for a second signature not counted
        until it is signed.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: member
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The person's limits
          content:
            application/json:
              example:
                member: 01a10cd8-0000-7000-8000-000000000002
                limits:
                  - currency: USD
                    per_payment_minor: 50000
                    per_day_minor: 100000
                    pending_change: null
                    spent_today_minor: 6000
                    left_today_minor: 94000
              schema:
                type: object
        '404':
          description: Not of the company, or another person's to anyone but an admin
    put:
      tags:
        - business
      summary: Set what one person may pay alone
      description: >
        Spec 052. Admins only, for an admin or a payer. Each limit is a ceiling: lower, or one where there was
        none, is in force at once; higher, or none, waits for a second admin or approver. Above either limit a
        payment waits for a second signature, as above the company's threshold, and says which.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: member
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                currency:
                  type: string
                  default: USD
                per_payment_minor:
                  type: integer
                  nullable: true
                  minimum: 1
                per_day_minor:
                  type: integer
                  nullable: true
                  minimum: 1
            example:
              currency: USD
              per_payment_minor: 50000
              per_day_minor: 100000
      responses:
        '200':
          description: 'Each limit''s effect: `in_force`, `waits_for_a_second_person` or `unchanged`'
          content:
            application/json:
              example:
                per_payment: in_force
                per_day: waits_for_a_second_person
                limits:
                  currency: USD
                  per_payment_minor: 50000
                  per_day_minor: 100000
              schema:
                type: object
        '400':
          description: Not more than nothing, or not a currency
        '403':
          description: Not an admin
        '409':
          description: The person neither pays nor administers
  /business/{id}/people/{member}/limits/confirm:
    post:
      tags:
        - business
      summary: Confirm a looser limit another person asked for
      description: Spec 052. An admin or an approver, not whoever asked for it.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: member
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                currency:
                  type: string
                  default: USD
      responses:
        '200':
          description: In force now
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not an admin or an approver
        '409':
          description: Nothing waits, or the caller asked for it
  /transfers/{id}/cosign:
    post:
      tags:
        - transfers
      summary: Give a company's payment its second signature
      description: >
        Spec 041. By an admin or approver of the company it is paid from, never its payer, with their own
        confirmation bound to the same amount and payee as `/confirm`'s. It moves under that authority.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Signed, and as far as it went
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferIntentView'
        '400':
          description: The confirmation is of something else, or not an authentication method
        '403':
          description: The caller's role does not give a second signature
        '404':
          description: Not a payment of a company the caller acts for
        '409':
          description: Not waiting for a second signature, or the caller made it
  /transfers/{id}/decline:
    post:
      tags:
        - transfers
      summary: Decline a company's payment's second signature
      description: Spec 041. As `/cosign`; the payment is refused and does not move.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Refused
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferIntentView'
        '403':
          description: The caller's role does not give a second signature
        '404':
          description: Not a payment of a company the caller acts for
        '409':
          description: Not waiting for a second signature, or the caller made it
  /backoffice/business:
    get:
      tags:
        - back-office
        - business
      summary: Every business application, for the operator
      description: >
        Spec 039. The registry's record and the mismatches, the owners opened, the screening for the company
        and each owner, the state, and `blocked`: why it cannot be opened now (the entity not active, the LEI
        not issued, an open screening alert, a confirmed match, no owner).
      responses:
        '200':
          description: Undecided first
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/business/{id}/credit:
    get:
      tags:
        - lending
        - back-office
      summary: A company's checks at the business credit bureau, newest first
      description: >
        Spec 074. With what the bank's record suggests searching with — the legal name, the registered address
        read into street, city, state and zip as well as free text allows, and the registration number where
        it is an EIN — and whether a business bureau is configured.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The checks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeBusinessCredit'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such company
  /backoffice/business/{id}/credit/search:
    post:
      tags:
        - lending
        - back-office
      summary: Find the company in the business credit bureau's files
      description: >
        Spec 074. Experian's Business Information search: a name (≤40), a city and a two-letter state are
        required; the street, five-digit zip, EIN and phone sharpen the match. No match is an empty list.
        Nothing is kept.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BusinessBureauSubject'
      responses:
        '200':
          description: The matches, surest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - matches
                properties:
                  matches:
                    type: array
                    items:
                      $ref: '#/components/schemas/BusinessBureauMatch'
        '400':
          description: The bureau refused the request (its words in detail)
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such company
        '503':
          description: No business bureau configured, or the bureau could not be asked
  /backoffice/business/{id}/credit/checks:
    post:
      tags:
        - lending
        - back-office
      summary: Score the match picked, and keep the answer on the company
      description: >
        Spec 074. Experian's Intelliscore Plus (paying) and Financial Stability Risk (lasting), each with its
        risk class, percentile, recommended credit limit and factors in the bureau's words. Kept with who
        asked and the bureau's reference; a company's loan is decided with its latest check of the last 30
        days.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - bureau_id
              properties:
                bureau_id:
                  type: string
                  description: The bureau's id for the company (Experian's BIN, nine digits)
      responses:
        '201':
          description: The check, kept
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BusinessCreditCheck'
        '400':
          description: The bureau refused the request (its words in detail)
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such company
        '503':
          description: No business bureau configured, or the bureau could not be asked
  /backoffice/business/{id}/open:
    post:
      tags:
        - back-office
        - business
      summary: Propose opening it, for a second operator
      description: >
        Spec 039. A `business_opening` approval; once a second operator approves it, the core opens the
        company as an entity client under its registered name and a dollar account, and the applicant sees it
        among their accounts. Checked again when it is carried out.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '201':
          description: Proposed
          content:
            application/json:
              example:
                approval_id: 01a0fe38-6f08-72ba-9d79-202f95fabb4d
                state: opening
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such application in this tenant
        '409':
          description: Not submitted, already proposed, or blocked, and why
  /backoffice/business/{id}/reject:
    post:
      tags:
        - back-office
        - business
      summary: Turn it down, with why
      description: Spec 039. The reason is the bank's record; the applicant reads only that it was not opened.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 8
      responses:
        '200':
          description: Rejected
          content:
            application/json:
              schema:
                type: object
        '400':
          description: No reason
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: Already opening or opened
  /backoffice/business/{id}/lookup:
    post:
      tags:
        - back-office
        - business
      summary: Record what the registry said, read by hand, for a company without an LEI
      description: >
        Spec 052. The operator looked the company up at its registry and records what they read: the name it
        holds, its status (`active`, `inactive`, `not_found`), its registration date and address, and where
        they looked. Kept with who and when; the mismatches are worked out again from it. Opening is refused
        until it says `active`, and the operator who recorded it cannot approve the opening.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - status
                - source
              properties:
                legal_name:
                  type: string
                  description: As the registry holds it; may be empty when not found
                status:
                  type: string
                  enum:
                    - active
                    - inactive
                    - not_found
                registered_on:
                  type: string
                  format: date
                address:
                  type: string
                source:
                  type: string
                  description: Where they looked
                  the registry or its page: null
            example:
              legal_name: OXA CORNER SHOP LIMITED
              status: active
              registered_on: '2019-04-02'
              address: 1 High Street, Edinburgh EH1 1AA
              source: https://find-and-update.company-information.service.gov.uk/company/SC123456
      responses:
        '200':
          description: Recorded
          content:
            application/json:
              schema:
                type: object
        '400':
          description: No source, no name for a company that was found, or a registration date in the future
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such application in the tenant
        '409':
          description: The company has an LEI, or the bank has decided
  /backoffice/incoming:
    get:
      tags:
        - back-office
      summary: Money that arrived through a customer's named account, newest first
      description: >
        Spec 038. Each with its sender, what became of its credit (`credit_state`, `credited`), and of a
        return (`return_state`, `return_reason`, `returned`). `returnable` is true where its credit was
        rejected, it was not credited, no return is proposed, and its rail can be sent back here (an ACH
        credit or an international wire through Column).
      responses:
        '200':
          description: The last hundred
          content:
            application/json:
              example:
                incoming:
                  - provider: column
                    notice_id: evnt_ach_1
                    rail: ach
                    received_at: '2026-10-01T23:21:00Z'
                    amount_minor: 2550
                    currency: USD
                    sender_name: ACME PAYROLL
                    sender_account: null
                    sender_reference: SALARY
                    credit_state: rejected
                    credited: false
                    return_state: null
                    return_reason: null
                    returned: false
                    returnable: true
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/incoming/return:
    post:
      tags:
        - back-office
      summary: Propose sending back money that arrived and was not credited, for a second operator
      description: >
        Spec 038. Proposes an `incoming_return`; once a second operator approves it the provider is asked to
        return it, once (ACH `R03`, `R02`, `R23` or `R16`; an international wire's `return_reason`; a domestic
        wire reversed with Column's reversal `reason`, the platform’s specification). Refused while its credit
        is undecided or approved, once it is credited or a return is proposed, for a euro credit, and for an
        ACH return past the second banking day after it arrived (except `refused_by_customer`). Money with a
        return proposed is never credited.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - provider
                - notice_id
                - reason
              properties:
                provider:
                  type: string
                notice_id:
                  type: string
                reason:
                  type: string
                  enum:
                    - no_such_account
                    - account_closed
                    - refused_by_customer
                    - compliance
            example:
              provider: column
              notice_id: evnt_ach_1
              reason: account_closed
      responses:
        '201':
          description: Proposed; it waits for a second operator under /approvals
          content:
            application/json:
              example:
                approval_id: 01a0f9c5-0000-7000-8000-000000000003
                reason: account_closed
              schema:
                type: object
        '400':
          description: Not a reason
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No money arrived under that notice in this tenant
        '409':
          description: Not returnable now, and why
  /backoffice/onboarding:
    get:
      tags:
        - onboarding
        - back-office
      summary: Everybody with something outstanding, oldest first
      responses:
        '200':
          description: The queue, oldest wait first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeOnboardingQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/onboarding/verdicts:
    post:
      tags:
        - onboarding
        - back-office
      summary: Record what a check found
      description: >
        Acts directly rather than proposing, and that is the considered exception to Principle II: a verdict
        is writing down what a check found, not granting anybody anything. **Opening the account is the
        granting step**, it is elsewhere, and that is where maker-checker belongs.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - onboarding_id
                - kind
                - satisfied
              properties:
                onboarding_id:
                  type: string
                  format: uuid
                kind:
                  $ref: '#/components/schemas/BackofficeOnboardingRequirementKind'
                satisfied:
                  type: boolean
                failure_code:
                  type: string
                  nullable: true
                  description: >
                    Effectively required when satisfied is false: the failure (code AND detail) is only stored
                    when BOTH are present; with detail but no code, the requirement is marked failed with a
                    NULL failure_detail.
                failure_detail:
                  type: string
                  nullable: true
                  minLength: 8
                  description: Required when satisfied is false; trimmed byte length >= 8, else 400.
      responses:
        '200':
          description: The journey as the verdict leaves it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeOnboardingView'
        '400':
          description: Not a requirement of this journey
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such journey in this tenant
  /backoffice/screening:
    get:
      tags:
        - screening
        - back-office
      summary: Alerts waiting for a person, strongest match first
      responses:
        '200':
          description: Open alerts, with the list and the entry version each was raised against
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeRiskScreeningQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/screening/adjudications:
    post:
      tags:
        - screening
        - back-office
      summary: Propose an adjudication
      description: >
        **This changes nothing.** It raises a proposal a different person approves. Clearing an alert is what
        lets a customer through, and an officer who could clear their own alert alone is the control not
        existing. The `revision` binds the decision to the version of the entry the officer actually saw, so a
        clearance lapses when the publisher changes it and returns for review carrying the earlier decision.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - key
                - revision
                - verdict
                - note
              properties:
                key:
                  type: string
                  description: '`subject|list|entry_id` of an OPEN alert, else 404.'
                revision:
                  type: string
                  description: The entry revision the officer saw. Must equal the alert's current revision, else 400.
                verdict:
                  type: string
                  enum:
                    - cleared
                    - confirmed
                  description: Anything else is 400.
                note:
                  type: string
                  minLength: 10
                  description: Trimmed, char count >= 10 (screening::MINIMUM_NOTE), else 400. Stored trimmed.
                proposal_id:
                  type: string
                  format: uuid
                  nullable: true
                  description: >
                    The automated proposal the officer had in front of them, kept on the approval so the file
                    shows what was suggested next to what was decided. Must be the tenant's (else 404) and
                    about this key and revision (else 409).
      responses:
        '202':
          description: Proposed. The alert stays open until a second person agrees
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: Not a verdict, or a note too thin to stand as a reason
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: >
            The key is not among the (first 500) open alerts; it may already have been decided. Or the named
            proposal does not exist for this tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: The named proposal is about a different alert or an earlier revision of the entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/screening/alert:
    get:
      tags:
        - screening
        - back-office
      summary: One alert, with everything ever decided about it
      parameters:
        - name: key
          in: query
          required: true
          schema:
            type: string
          description: >
            `subject|list|entry_id`. A query parameter rather than a path segment because the key contains the
            separator
      responses:
        '200':
          description: The alert and its decision history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeRiskScreeningAlertDetail'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/screening/settings:
    get:
      tags:
        - screening
        - back-office
      summary: Whether a provider's service screens beside the bank's own lists
      description: >
        Spec 073. `service_on` (false while the bank has set nothing), `provider` (the deployment's, or null),
        `provider_configured`, `min_decided` (1000) and `min_confirmed` (50) for training a model, `set_by`,
        `set_at`, and whether a change already waits (`change_waiting`).
      responses:
        '200':
          description: The settings in force
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - screening
        - back-office
      summary: Propose turning the provider's service on or off, or the training minimums
      description: >
        Spec 073, the platform’s rule. A compliance officer proposes; another approves (`screening_setting`).
        Turning the service on sends every subject screened from then on to the provider, beside the bank's
        own lists and never instead of them.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - service_on
                - min_decided
                - min_confirmed
              properties:
                service_on:
                  type: boolean
                min_decided:
                  type: integer
                  minimum: 1
                min_confirmed:
                  type: integer
                  minimum: 1
      responses:
        '202':
          description: Proposed; nothing changes until another compliance officer approves
          content:
            application/json:
              schema:
                type: object
        '400':
          description: |
            The minimum confirmed matches must be more than zero and no more than the minimum decided.
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: |
            No screening service is configured for this bank; or the same change already waits.
  /backoffice/screening/runs:
    get:
      tags:
        - screening
        - back-office
      summary: The subjects whose latest run waits on the provider's service, or on a missing list
      description: >
        Spec 073, the platform’s rule, and the platform’s specification, FR-7705a. A subject here is told
        `in_review`: the provider could not answer, or a declared list has no version in use, and "we could
        not check" is never a clear result. The platform screens each again once the provider answers or the
        list loads; two compliance officers may close one on the lists that did load instead. Ask for exactly
        one of `service_state=unavailable` and `lists_state=waiting`.
      parameters:
        - name: service_state
          in: query
          required: false
          schema:
            type: string
            enum:
              - unavailable
        - name: lists_state
          in: query
          required: false
          schema:
            type: string
            enum:
              - waiting
      responses:
        '200':
          description: >
            `{count, runs: [{id, subject_reference, screened_at, outcome, service_provider, service_error,
            missing_lists, lists_state}]}`, oldest first
          content:
            application/json:
              schema:
                type: object
        '400':
          description: Neither filter, or both
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/alerts:
    get:
      tags:
        - alerts
        - back-office
      summary: The alerts of the caller's roles
      description: >
        Spec 077, the platform’s rule, the platform’s rule. Only the roles the caller holds; an auditor sees
        every role's, read only. Ordered by weight, then severity, then the latest occurrence. AlertsWork.
      parameters:
        - name: state
          in: query
          required: false
          schema:
            type: string
            enum:
              - open
              - acknowledged
              - resolved
              - cleared
        - name: severity
          in: query
          required: false
          schema:
            type: string
            enum:
              - info
              - low
              - medium
              - high
              - critical
        - name: role
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: The alerts, at most 500
          content:
            application/json:
              schema:
                type: object
                required:
                  - count
                  - alerts
                properties:
                  count:
                    type: integer
                    minimum: 0
                  alerts:
                    type: array
                    items:
                      $ref: '#/components/schemas/BackofficeAlert'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/alerts/{id}:
    get:
      tags:
        - alerts
        - back-office
      summary: One alert, its event, its occurrences and its deliveries
      description: >
        Spec 077. The event shows only the fields the rule catalogue declares. Another role's alert is "No
        such alert.", the same as one that does not exist.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The alert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeAlertDetail'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such alert.
  /backoffice/alerts/{id}/acknowledge:
    post:
      tags:
        - alerts
        - back-office
      summary: Acknowledge an alert
      description: Spec 077, the platform’s rule. One person, audited (`alert.acknowledged`).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The alert as it now stands
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeAlert'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such alert.
        '409':
          description: This alert is already resolved; or this alert was cleared by an event.
  /backoffice/alerts/{id}/resolve:
    post:
      tags:
        - alerts
        - back-office
      summary: Resolve an alert with a note
      description: >-
        Spec 077, the platform’s rule. One person, a note of at least 10 characters, audited
        (`alert.resolved`).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - note
              properties:
                note:
                  type: string
                  minLength: 10
      responses:
        '200':
          description: The alert as it now stands
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeAlert'
        '400':
          description: A resolution needs a note of at least 10 characters.
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such alert.
        '409':
          description: This alert is already resolved; or this alert was cleared by an event.
  /backoffice/alerts/rules:
    get:
      tags:
        - alerts
        - back-office
      summary: The alert rules in force, and the role mailboxes
      description: >
        Spec 077, the platform’s rule, the platform’s rule. Each rule in force with its version, state, and
        what it has done (`fired`, `not_evaluated`, `cleared`), and whether a change waits. With
        `format=yaml`, the rules in force in the catalogue's own format, as a document.
      parameters:
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - yaml
      responses:
        '200':
          description: The rules and mailboxes; or, with format=yaml, application/yaml
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeAlertRules'
            application/yaml:
              schema:
                type: string
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - alerts
        - back-office
      summary: Propose adding, changing, switching off or on, or removing a rule
      description: >
        Spec 077, the platform’s rule, the platform’s rule. Checked here, and again when carried out; proposed
        by a person of the rule's role or a supervisor; carried out when another person of that role, or a
        supervisor, approves (`alert_rule`). AlertRulesManage.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - change
                - rule
                - reason
              properties:
                change:
                  type: string
                  enum:
                    - add
                    - change
                    - 'off'
                    - 'on'
                    - remove
                rule:
                  $ref: '#/components/schemas/AlertRule'
                reason:
                  type: string
                  minLength: 10
      responses:
        '202':
          description: Proposed; nothing changes until another person of the rule's role, or a supervisor, approves
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: >
            The platform's sentence: an event it does not emit, a field the event does not carry, a comparison
            that does not suit the field, no such role, a message placeholder that is not the event's,
            grouping by customer on an event that names none, a rule that would raise and clear on the same
            event.
        '403':
          description: Your role does not allow this.
        '409':
          description: The same change already waits for approval.
  /backoffice/alerts/rules/{rule_id}/versions:
    get:
      tags:
        - alerts
        - back-office
      summary: Every version of one rule
      parameters:
        - name: rule_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The versions, oldest first, each with the approval that made it
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: There is no rule with that id.
  /backoffice/alerts/mailboxes:
    post:
      tags:
        - alerts
        - back-office
      summary: Propose where a role's alerts are emailed
      description: >
        Spec 077, research R10. A team's mailbox, never a person's address; under the same approval as a rule
        (`alert_rule`). AlertRulesManage.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
                - address
                - reason
              properties:
                role:
                  type: string
                address:
                  type: string
                reason:
                  type: string
                  minLength: 10
      responses:
        '202':
          description: Proposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyProposedNoted'
        '400':
          description: That is not an email address; or there is no role named so.
        '403':
          description: Your role does not allow this.
        '409':
          description: The same change already waits for approval.
  /backoffice/screening/lists:
    get:
      tags:
        - screening
        - back-office
      summary: Every declared list and how it stands
      description: >
        Spec 077. The JSON lists and the OFAC sets, read or not: `state` is `current`, `partial` (an optional
        file the manifest named is missing), `stale` (the last read failed; the previous version is in use) or
        `failed` (no version in use: every run waits, FR-7705a). RiskReview; an auditor reads.
      responses:
        '200':
          description: The lists, by name
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BackofficeScreeningList'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '503':
          description: The watchlists are not loaded
  /backoffice/screening/runs/{run_id}/local-only:
    post:
      tags:
        - screening
        - back-office
      summary: Propose closing a run that waits on the provider, on the bank's own lists alone
      description: >
        Spec 073, US2. A compliance officer proposes with a reason; another approves (`screening_local_only`).
        The run becomes `closed_local_only` with the outcome its local matches come to now, and onboarding is
        settled as a fresh clear run would settle it.
      parameters:
        - name: run_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 10
      responses:
        '202':
          description: Proposed; nothing changes until another compliance officer approves
          content:
            application/json:
              schema:
                type: object
        '400':
          description: Say why, in at least 10 characters.
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such run
        '409':
          description: >-
            This run is not waiting for anything (the provider or a missing list); or the same change already
            waits.
  /backoffice/screening/models:
    get:
      tags:
        - screening
        - back-office
      summary: Every screening model the bank has trained, newest first
      description: >
        Spec 073. Each model without its file: `id`, `version`, `sha256`, `state` (`candidate`, `refused`,
        `promoted`, `withdrawn`, `superseded`), `trained_by`, `trained_at`, the period of decisions it learnt
        from, `counts` (decided, confirmed, cleared, synthetic, the bank's share of the weight), `heldout`
        (the recent decisions it is tested on), `report`, `answering_version`, `trained_on`, `weights`,
        `proposals_answered` (how many of the bank's proposals it made) and its promotion and withdrawal
        approvals. Beside the list, `answering` (`{version, provenance, id}`: the bank's promoted model,
        `live`; or `screening-lr-1`, `synthetic`; null with a `note` when no AI plane is configured and the
        rules answer) and `decided_since_answering_trained`.
      responses:
        '200':
          description: '`{answering, note, decided_since_answering_trained, models: [...]}`'
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - screening
        - back-office
      summary: Train a candidate on the bank's decided alerts
      description: >
        Spec 073, US3. Acts directly, as running a check does: a candidate answers nothing until two
        compliance officers promote it. The bank's decisions leave for the AI plane as nine numbers and a
        verdict each, blended there with synthetic alerts, the bank's carrying two thirds of the weight. The
        most recent decisions, at least 20 of them confirmed, are held out to test it.
      responses:
        '201':
          description: The candidate, as the list shows it
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: >
            Not enough decided alerts to train on (with the counts held and needed), or not enough recent
            confirmed matches to test a model on; or the plane refused what it was sent.
        '503':
          description: The AI plane is not configured, or did not answer; nothing was trained.
  /backoffice/screening/models/{id}:
    get:
      tags:
        - screening
        - back-office
      summary: One screening model
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The model, as the list shows it
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such model
  /backoffice/screening/models/{id}/promotion:
    post:
      tags:
        - screening
        - back-office
      summary: Propose making a candidate the model that answers the bank's alerts
      description: >
        Spec 073, US4. A compliance officer proposes; another approves (`screening_model_promotion`). Only a
        candidate whose harness report is promotable, and whose file is the one pinned, may be proposed, and
        both are checked again when it is carried out. The model answering before is superseded and answers
        again if this one is withdrawn.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '202':
          description: Proposed; nothing changes until another compliance officer approves
          content:
            application/json:
              schema:
                type: object
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such model
        '409':
          description: >
            Only a candidate can be promoted; this model did not pass the harness (and why); its file is not
            the one pinned; or the same proposal already waits.
  /backoffice/screening/models/{id}/withdrawal:
    post:
      tags:
        - screening
        - back-office
      summary: Propose withdrawing the model that answers
      description: >
        Spec 073, US4. A compliance officer proposes with a reason; another approves
        (`screening_model_withdrawal`). The model it replaced answers again, or the one in the AI plane's
        image when there was none.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 10
      responses:
        '202':
          description: Proposed; nothing changes until another compliance officer approves
          content:
            application/json:
              schema:
                type: object
        '400':
          description: Say why, in at least 10 characters.
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such model
        '409':
          description: Only the answering model can be withdrawn; or the same proposal already waits.
  /backoffice/screening/rescreen:
    post:
      tags:
        - screening
        - back-office
      summary: Screen somebody again, by hand
      description: >
        Acts directly rather than proposing, the same exception the onboarding verdict route makes: running a
        check is not granting anybody anything. What comes out of it needs two people; running it does not.
        The recurring job does the same work unattended.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - subject_reference
                - full_name
              properties:
                subject_reference:
                  type: string
                  description: Not validated; becomes the prefix of every match key.
                full_name:
                  type: string
                  pattern: \S
                  description: Must contain a non-whitespace character, else 400.
                also_known_as:
                  type: array
                  default: []
                  items:
                    type: string
                date_of_birth:
                  type: string
                  format: date
                  nullable: true
                country:
                  type: string
                  nullable: true
                organisation:
                  type: boolean
                  default: false
                  description: true screens the subject as an organisation, else a person.
      responses:
        '200':
          description: Screened, with the list versions used and the moment it ran
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeRiskScreeningRun'
        '400':
          description: Not enough of a subject to screen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '503':
          description: Watchlists not loaded, no watchlists configured, or the list source failed. Never a clear result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /payees:
    get:
      tags:
        - transfers
      summary: The caller's payees
      description: >
        Each carries whether they have ever been paid before, because that is what the first-payment warning
        at approval is derived from.
      responses:
        '200':
          description: Payees, each marked first payment or not
          content:
            application/json:
              example:
                payees:
                  - id: c7a3b201-5e46-4d18-b9c2-7f0a1b2c3d4e
                    display_name: Ana Rivera
                    account_ref: '012180001234567895'
                    first_payment: true
                    destination: spei
              schema:
                $ref: '#/components/schemas/PayeeList'
    post:
      tags:
        - transfers
      summary: Add a payee
      responses:
        '201':
          description: The payee, marked as never paid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayeeView'
        '400':
          description: >
            A name or account missing, or an 18-digit account that is not a valid CLABE — a wrong check digit
            or a bank SPEI does not reach — refused now, while the customer is looking at the number, with the
            reason. The same for a US bank account (`aba:<routing>:<account>:<checking|savings>`, the
            platform’s specification) whose routing number is not nine digits or fails the ABA check digit,
            whose account is not 4 to 17 digits, or whose type is neither. A shape the platform cannot route
            is kept and answered `destination: unknown`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayeeNewBody'
  /transfers:
    get:
      tags:
        - transfers
      summary: The caller's own transfers, newest first
      responses:
        '200':
          description: >
            Transfers, each carrying `money_moved`. It is `true` only on a `posted` transfer - the accounting
            core confirmed the movement and `core_ref` names it. A transfer to an account the core holds is
            posted after authorisation. `posting` means the core was asked and has not been heard back from;
            it is not moved and not refused, and the field does not guess. A payee at another Mexican bank —
            `destination: spei`, a CLABE — is `sent` once the core has debited the customer to the bank's
            settlement account and the rail accepted the order (`core_ref` and `rail_ref` both named,
            `money_moved: true`), `settled` when the rail reports delivery, and `returned` when the rail sent
            it back and a second person approved the credit back. `destination: unknown` is a reference the
            platform cannot route yet: it stays `authorized` with `money_moved: false`
          content:
            application/json:
              example:
                transfers:
                  - id: f0e9d8c7-b6a5-4321-9876-543210fedcba
                    state: authorized
                    from_account: 3f1b9d20-7c44-4a1e-9e0e-2b5d8c1a4f77
                    payee:
                      id: c7a3b201-5e46-4d18-b9c2-7f0a1b2c3d4e
                      display_name: Ana Rivera
                    amount_minor: 40000
                    currency: MXN
                    reference: Dinner
                    created_at: '2026-09-20T19:11:04Z'
                    authorize:
                      amount: '400.00'
                      currency: MXN
                      payee: Ana Rivera
                      expires_in_minutes: 10
                    warnings:
                      - code: first_payment
                        detail: You have not paid Ana Rivera before
                    authorized_at: '2026-09-20T19:11:31Z'
                    auth_method: device_biometric
                    refused_reason: null
                    core_ref: null
                    rail_ref: null
                    destination: sepa
                    money_moved: false
                    goes_out_on: '2026-09-21'
                    after_cutoff: true
                    cutoff: '14:00'
                    holiday: null
                    instant: true
                    cosign_reason: null
                  - id: 0a1b2c3d-4e5f-4061-8a7b-9c0d1e2f3a4b
                    state: posted
                    from_account: '1'
                    payee:
                      id: d8b4c312-6f57-4e29-8ad3-801b2c3d4e5f
                      display_name: Savings
                    amount_minor: 100
                    currency: MXN
                    reference: Rent share
                    created_at: '2026-09-24T09:02:10Z'
                    authorize:
                      amount: '1.00'
                      currency: MXN
                      payee: Savings
                      expires_in_minutes: 10
                    warnings: []
                    authorized_at: '2026-09-24T09:02:40Z'
                    auth_method: device_biometric
                    refused_reason: null
                    core_ref: '57'
                    rail_ref: null
                    destination: core
                    money_moved: true
                    goes_out_on: null
                    after_cutoff: null
                    cutoff: null
                    holiday: null
                    instant: false
                    cosign_reason: null
              schema:
                $ref: '#/components/schemas/TransferList'
    post:
      tags:
        - transfers
      summary: Say what you want to do. Nothing happens yet
      description: >
        Creates an **intent** the customer is shown. What executes later is derived from the stored intent and
        never from anything sent with the approval. The intent expires, and it outlives an authentication on
        purpose, so a customer is never asked to authenticate again for a payment the platform has already
        forgotten. A first payment to a payee never paid before comes back as a warning and is not a refusal.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferNewBody'
      responses:
        '201':
          description: The intent as the customer must be shown it, with any warnings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferIntentView'
        '400':
          description: Not something that can be paid
        '404':
          description: Not the caller's payee or account. Never 403
  /transfers/{id}/confirm:
    post:
      tags:
        - transfers
      summary: Authorise it
      description: >
        The customer is the authoriser (Principle II). The authorisation is refused unless it is all three of:
        two or more categories of authentication factor, given inside the freshness window, and bound to the
        exact amount and payee being executed - and the refusal says which of the three failed. The signed
        fields are compared against the **stored intent**; they are not the source of what executes. After
        authorisation the platform asks the accounting core to move the money, with the intent's id as the
        reference and idempotency key, so asking twice moves it once. The response says what the core did:
        `posted` with `money_moved: true`; `refused` if the core said no (the money did not move);
        `authorized` with `money_moved: false` if the payee is not an account the core holds; or `posting` if
        the core could not be heard back from, which is resolved by looking the movement up, never by moving
        it again.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: >
            Authorised, and `money_moved` says whether the core moved it. A held transfer says a person will
            decide and nothing is posted; risk scoring being unavailable never turns this into a refusal
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferIntentView'
        '400':
          description: >
            Not an authentication method, a single factor where two categories are required, or an
            authentication outside its window. The answer says which
        '404':
          description: Not the caller's transfer
        '409':
          description: >
            Already confirmed, or the intent has expired and the details must be checked again. Confirming
            twice settles once and the second attempt is told so
  /funding:
    get:
      tags:
        - crypto
      summary: The caller's own fundings
      responses:
        '200':
          description: Fundings and the stage each has reached
          content:
            application/json:
              example:
                - id: a1b2c3d4-e5f6-4708-9a0b-1c2d3e4f5061
                  stage: awaiting_deposit
                  detail: Waiting for your transfer to arrive
                  pending: true
                  final: false
                  network: polygon
                  token: USDC
                  token_decimals: 6
                  deposit_address: null
                  deposit_memo: null
                  address_provenance: unavailable
                  amount_observed: null
                  confirmations: 0
                  confirmations_required: 128
                  tx_hash: null
                  settles_in: MXN
                  fee_minor: null
                  credited_minor: null
                  failure_detail: null
                  created_at: '2026-09-21T10:00:00Z'
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FundingView'
    post:
      tags:
        - crypto
      summary: Start a funding
      description: >
        Spec 062: where a ramp is bound for the network and token (PLATFORM_RAMP=evm), the address is the
        node's — one per customer and network, never another customer's — and the funding is watched from the
        block it opened at: seen, confirmed, converted at the ramp's rate less its fee, and its credit
        proposed for a second person, who moves the dollars in the core. Without one, the attempt has **no
        address**, honestly absent (Principle V): a deposit address is usable or absent, and one that cannot
        receive funds is never presented as one that can.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - network
                - token
              properties:
                network:
                  type: string
                  description: Any string; not validated against a list
                token:
                  type: string
                token_decimals:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 24
                  default: 6
                settles_in:
                  type: string
                  default: USD
                to_account:
                  type: string
                  description: >-
                    Spec 062: the customer's own account the dollars go to; their first in settles_in when
                    absent
      responses:
        '202':
          description: >
            The attempt, with provenance `unavailable` where an address would be. Selling is not offered at
            all: there is no off-ramp
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FundingStarted'
        '400':
          description: Not a network or token this tenant funds in
  /funding/networks:
    get:
      tags:
        - crypto
      summary: The chains and tokens the bank's ramp takes
      description: Spec 062. Empty when no ramp is bound.
      responses:
        '200':
          description: The networks
          content:
            application/json:
              schema:
                type: object
                required:
                  - networks
                properties:
                  networks:
                    type: array
                    items:
                      type: object
                      required:
                        - id
                        - label
                        - token
                        - token_decimals
                        - confirmations_required
                        - provenance
                      properties:
                        id:
                          type: string
                        label:
                          type: string
                        token:
                          type: string
                        token_decimals:
                          type: integer
                        confirmations_required:
                          type: integer
                        provenance:
                          type: string
                          enum:
                            - live
                            - synthetic
              example:
                networks:
                  - id: anvil
                    label: anvil (OxaBank sandbox chain)
                    token: USDC
                    token_decimals: 6
                    confirmations_required: 3
                    provenance: synthetic
  /funding/conversion:
    post:
      tags:
        - crypto
      summary: What would this deposit come to?
      description: >
        No funding, no address and no record. A customer working out whether it is worth it has not started
        anything. The rate carries its provenance like every other figure.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount
                - token
              properties:
                amount:
                  type: string
                  pattern: ^\s*\+?[0-9]+\s*$
                  description: >
                    A whole number in the token's smallest unit (1000000 = 1 USDC at 6 decimals), parsed as
                    i128 after trimming. Must be > 0. NOT a decimal.
                token:
                  type: string
                token_decimals:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 24
                  default: 6
                settles_in:
                  type: string
                  default: USD
                settlement_decimals:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 24
                  default: 2
      responses:
        '200':
          description: What it would come to, with where the rate came from
          content:
            application/json:
              example:
                gross_minor: 1000000
                fee_minor: 8500
                net_minor: 991500
                fee_exceeded_deposit: false
                settles_in: MXN
                peg: 1 USDC = 1 USD
                indicative: true
                rate:
                  numerator: '1834'
                  denominator: '100'
              schema:
                $ref: '#/components/schemas/FundingConversion'
        '400':
          description: Not a convertible pair, or an amount that is not a decimal
  /backoffice/funding:
    get:
      tags:
        - crypto
        - back-office
      summary: Fundings still moving, oldest first
      responses:
        '200':
          description: The open queue
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeFundingQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/funding/{id}/fail:
    post:
      tags:
        - crypto
        - back-office
      summary: Mark a stuck funding failed
      description: >
        An operator's, and it acts directly rather than proposing. Unlike a limit change or a credit decision
        it **removes** a claim rather than granting one, and the money is untouched either way; making it wait
        for a second person would leave a customer with no answer while two people coordinate about a record.
        What an operator explicitly cannot do here is credit a funding - that moves money and lives at
        `/backoffice/funding/{id}/credit` under maker-checker.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - detail
              properties:
                detail:
                  type: string
                  minLength: 8
                  description: Trimmed, byte length >= 8, else 400. Stored trimmed as failure_detail.
      responses:
        '200':
          description: Marked failed, with the detail recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FundingView'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such funding in this tenant
        '409':
          description: The funding is not at a stage that can be failed
  /investments:
    get:
      tags:
        - investments
      summary: What the caller holds, how far it has drifted, and what a rebalance would do
      description: >
        Every figure states where it came from. Tax lots, dividends and corporate actions are not accounted
        for and the response does not pretend otherwise.
      responses:
        '200':
          description: Holdings, drift against the model, and the provenance of each figure
          content:
            application/json:
              example:
                id: d5e6f708-1a2b-4c3d-8e9f-0a1b2c3d4e5f
                model: Balanced
                risk_profile: balanced
                assessed_on: '2026-06-01'
                suitability:
                  permits: true
                  detail: The model matches the profile on file
                  verdict: suitable
                total_minor: 87200
                positions:
                  - instrument: WORLD-EQ
                    quantity: '12.4000'
                    quantity_scale: 4
                    price_minor: 4100
                drift:
                  - instrument: WORLD-EQ
                    target_bps: 6000
                    actual_bps: 5830
                    drift_bps: -170
                    target_minor: 52320
                    actual_minor: 50836
                proposed_rebalance:
                  within_band: true
                  orders: []
                  cash_after_minor: 36364
                provenance: unavailable
              schema:
                $ref: '#/components/schemas/InvestmentAccountView'
    post:
      tags:
        - investments
      summary: Open an investment account on a model
      description: >
        Suitability is checked against the stated risk profile before the account opens. The model is the
        platform's; the execution is a broker's and the cash leg is the accounting core's.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - model
                - risk_profile
              properties:
                model:
                  type: string
                risk_profile:
                  type: string
      responses:
        '201':
          description: The account, on the model it was opened against
        '400':
          description: The model is not suitable for the stated profile, and the answer says why
        '409':
          description: An account already exists for this caller on this model
  /investments/contributions:
    get:
      tags:
        - investments
      summary: Money the caller added to investing, newest first
      responses:
        '200':
          description: The contributions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvestmentContributions'
        '404':
          description: No investment account
    post:
      tags:
        - investments
      summary: Add money to investing from one of the caller's dollar accounts
      description: >
        Spec 076. On the customer's device confirmation, bound to the amount and to
        `InvestmentAccountView.contributions.payee`; a device key signs over the kind
        `investment_contribution` and the investment account's id. The core moves the money from their account
        to the bank's investing cash account under the reference `investment:<contribution id>`, which is what
        their statement shows. 503 while the bank takes no money into investing.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/TransferConfirmBody'
                - type: object
                  required:
                    - from_account
                    - amount_minor
                  properties:
                    from_account:
                      type: string
                      description: One of the caller's own USD accounts
                    amount_minor:
                      type: integer
                      format: int64
                      minimum: 1
      responses:
        '201':
          description: Moved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvestmentContribution'
        '400':
          description: Not the caller's dollar account, nothing to add, or the confirmation does not bind it
        '404':
          description: No investment account
        '503':
          description: The bank takes no money into investing yet, or the core did not move it
  /investments/fills:
    get:
      tags:
        - investments
      summary: What the broker bought and sold for the caller, and at what
      description: >
        Spec 076. Each filled order: instrument, side, quantity (scaled as positions are), value and when. The
        positions' `cost_minor` is these fills at average cost. Empty, with `broker_bound: false`, until a
        broker adapter is bound and fills an order.
      responses:
        '200':
          description: The fills
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvestmentFills'
        '404':
          description: No investment account
  /investments/models:
    get:
      tags:
        - investments
      summary: What is on offer
      responses:
        '200':
          description: Models, each with the profile it suits
          content:
            application/json:
              example:
                models:
                  - name: Balanced
                    risk_profile: balanced
                    drift_band_bps: 500
                    targets: []
              schema:
                $ref: '#/components/schemas/InvestmentModelList'
  /backoffice/investments:
    get:
      tags:
        - investments
        - back-office
      summary: Investment accounts in this tenant
      responses:
        '200':
          description: Accounts, with drift against their models
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyInvestmentQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/investments/rebalances:
    post:
      tags:
        - investments
        - back-office
      summary: Propose a rebalance
      description: >
        **This places no orders.** A different person approves it (Principle II). An operator who could
        rebalance an account alone could churn it, and churning is the oldest way to take a client's money
        legally - the control here is not bureaucracy, it is the actual protection.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - account_id
                - reason
              properties:
                account_id:
                  type: string
                  format: uuid
                reason:
                  type: string
                  minLength: 8
                  description: Trimmed BYTE length >= 8, else 400. Stored untrimmed on the proposal.
      responses:
        '202':
          description: Proposed. No order is placed until a second person agrees
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyRebalanceProposed'
        '400':
          description: reason shorter than 8 trimmed BYTES.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such account in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: >
            The account is on a model no longer offered; the planner refused; or the account is inside its
            drift band ("trading it would be churn").
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/exchange:
    get:
      tags:
        - exchange
      summary: Recent exchange quotes, for somebody answering a customer about one
      description: >
        The same figures the customer saw, and the core's reference for each leg. An exchange is authorised by
        the customer, so there is nothing here to approve — with one exception: a quote `posting` with
        `out_core_ref` and no `in_core_ref` is half done, the customer's money having left and the other
        currency not arrived, and it is completed through `POST /backoffice/exchange/completions`.

        The subject is included because an operator is answering a named customer; every other customer-facing
        response in this platform omits it.
      responses:
        '200':
          description: Quotes across customers, newest first
          content:
            application/json:
              example:
                quotes:
                  - id: 01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
                    subject: customer-1
                    sell_minor: 100000
                    sell_currency: USD
                    buy_minor: 86800
                    buy_currency: EUR
                    mid: '0.87237'
                    rate: '0.86800815'
                    spread_bps: 50
                    spread_minor: 437
                    rate_source: European Central Bank reference rate
                    rate_as_of: '2026-09-18'
                    state: authorized
                    created_at: '2026-09-24T10:02:00Z'
                    money_moved: false
              schema:
                $ref: '#/components/schemas/BackofficeMoneyExchangeQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /notifications:
    get:
      tags:
        - notifications
      summary: What the bank did on the caller's behalf, newest first
      description: >
        Built from the domain events the platform recorded, filtered by subject **in the query**. An event a
        customer is not entitled to see never reaches this endpoint: the notifications module answers with
        nothing for it, and nothing is what this returns. Filtering in the client is not filtering, and
        neither is stripping fields at the edge — one forgotten field there is a customer told they matched a
        watchlist.

        Each notice carries a `target`, so the app opens the thing it is about rather than parsing a sentence
        to guess.
      responses:
        '200':
          description: Notices, newest first
          content:
            application/json:
              example:
                notices:
                  - id: 01992d20-1a2b-7c3d-8e4f-5a6b7c8d9e02
                    title: Payment authorised
                    line: You approved it and we accepted it.
                    at: '2026-09-24T10:14:00Z'
                    tone: positive
                    target:
                      kind: transfer
                      id: 01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
                  - id: 01992d1f-9f8e-7d6c-8b5a-4c3d2e1f0a99
                    title: We opened your case
                    line: We will tell you as it moves.
                    at: '2026-09-24T09:12:00Z'
                    tone: null
                    target:
                      kind: dispute
                      id: 01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b11
              schema:
                $ref: '#/components/schemas/NoticeList'
  /backoffice/notifications:
    get:
      tags:
        - notifications
      summary: What a named customer was told, for somebody answering them about it
      description: >
        The same notices the customer sees and no more. An operator asking "what did we tell them?" needs the
        customer's view rather than a richer one — a back office that saw more than the customer would answer
        questions the customer never asked.
      parameters:
        - name: subject
          in: query
          required: true
          schema:
            type: string
          description: >-
            A customer's token subject (the outbox `subject` column). Matched exactly; unknown subjects give
            notices [].
      responses:
        '200':
          description: That customer's notices, newest first
          content:
            application/json:
              example:
                subject: customer-1
                notices:
                  - id: 01992d20-1a2b-7c3d-8e4f-5a6b7c8d9e02
                    title: Payment authorised
                    line: You approved it and we accepted it.
                    at: '2026-09-24T10:14:00Z'
                    tone: positive
                    target:
                      kind: transfer
                      id: 01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
              schema:
                $ref: '#/components/schemas/BackofficeNoticeList'
        '400':
          description: '`subject` missing (axum Query rejection, plain text), or several tenant memberships (Problem).'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '500':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: No database configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /exchange/quotes:
    get:
      tags:
        - exchange
      summary: The caller's exchange quotes, newest first
      description: >
        Each one carries the rate it was quoted at, frozen as the two integers the source published it as.
        Re-deriving a rate at read time would mean the record of what somebody agreed to changing after they
        agreed to it.
      responses:
        '200':
          description: Quotes with the rate applied, the spread as money, and what was credited
          content:
            application/json:
              example:
                quotes:
                  - id: 01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
                    from_account: acct-usd-1
                    to_account: acct-eur-1
                    sell_minor: 100000
                    sell_currency: USD
                    buy_minor: 86800
                    buy_currency: EUR
                    mid: '0.87237'
                    rate: '0.86800815'
                    spread_bps: 50
                    spread_minor: 437
                    remainder: 89880000
                    remainder_denominator: 1000000000
                    rate_source: European Central Bank reference rate
                    rate_as_of: '2026-09-18'
                    state: authorized
                    refused_reason: null
                    created_at: '2026-09-24T10:02:00Z'
                    authorize:
                      amount: '1000.00'
                      currency: USD
                      payee: Your EUR account
                    out_core_ref: null
                    in_core_ref: null
                    money_moved: false
              schema:
                $ref: '#/components/schemas/ExchangeQuoteList'
    post:
      tags:
        - exchange
      summary: Quote a conversion between two of the caller's own accounts
      description: >
        Nothing is converted. The quote is stored and what a confirmation executes is this row, so the figures
        a customer agreed to are the figures that run.

        Both rates are returned. `mid` is what the source published and `rate` is what the customer gets;
        `spread_minor` is the difference in money, which is the only form in which the two are comparable. A
        margin folded into a single quoted rate is a fee charged without being named.

        `503` when no rate provider is configured, which is a supported way to run (Principle IX). The
        platform never substitutes a rate of its own.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeQuoteBody'
            example:
              from_account: acct-usd-1
              to_account: acct-eur-1
              sell_minor: 100000
      responses:
        '201':
          description: A quote, stored, awaiting the customer's confirmation
          content:
            application/json:
              example:
                id: 01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
                from_account: acct-usd-1
                to_account: acct-eur-1
                sell_minor: 100000
                sell_currency: USD
                buy_minor: 86800
                buy_currency: EUR
                mid: '0.87237'
                rate: '0.86800815'
                spread_bps: 50
                spread_minor: 437
                remainder: 89880000
                remainder_denominator: 1000000000
                rate_source: European Central Bank reference rate
                rate_as_of: '2026-09-18'
                state: awaiting_confirmation
                refused_reason: null
                created_at: '2026-09-24T10:02:00Z'
                authorize:
                  amount: '1000.00'
                  currency: USD
                  payee: Your EUR account
                out_core_ref: null
                in_core_ref: null
                money_moved: false
              schema:
                $ref: '#/components/schemas/ExchangeQuoteView'
        '409':
          description: >
            The rate is too old to quote on, and the refusal says how old. A customer told only that we cannot
            quote has been told they cannot and not why.
          content:
            application/json:
              example:
                type: about:blank
                title: Conflict
                status: 409
                detail: this rate was published 17 days ago and we will not quote on one older than 4
        '503':
          description: No rate provider is configured, so the platform cannot quote and says so.
  /exchange/quotes/{id}/cosign:
    post:
      tags:
        - exchange
        - business
      summary: Give a company's exchange its second signature
      description: >
        Spec 052. An exchange from a company's account crossing its threshold or its maker's limits, in the
        sell currency, waits after its maker's confirmation (`awaiting_cosignature`, with `cosign_reason`). An
        admin or approver of the company, never its maker, confirms the same figures with their own
        confirmation; the rate's freshness is checked again, and a stale one expires (409) rather than moves.
        It moves under the signatory's authority.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
      responses:
        '200':
          description: Signed; the quote as it now is
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not an admin or approver of the company
        '404':
          description: Not a company's exchange the caller acts for
        '409':
          description: Not waiting, made by the caller, or its rate now too old (expired)
  /exchange/quotes/{id}/decline:
    post:
      tags:
        - exchange
        - business
      summary: Decline a company's exchange waiting for a second signature
      description: Spec 052. An admin or approver of the company, never its maker. Nothing moves; the quote is `refused`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Declined
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Not an admin or approver of the company
        '409':
          description: Not waiting, or made by the caller
  /exchange/quotes/{id}/confirm:
    post:
      tags:
        - exchange
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      summary: Confirm a stored quote
      description: >
        The request carries the customer's authority and nothing the platform reads for the figures. Every
        number comes from the stored quote; the signed fields are compared with the quote's `authorize` object
        — the amount leaving, its currency and "Your <buy> account" — and a mismatch, a single factor or a
        stale authentication is refused saying which, as for a transfer.

        The core does not convert, so a confirmed quote moves in two legs within one currency each, through
        the bank's own account in each currency: `out` from the customer's sell account, then `in` to their
        buy account. `posted` with `money_moved: true` has both; `posting` with only `out_core_ref` is half
        done — the customer's money has left and the other currency has not arrived — and is shown as that;
        `refused` means the first leg was refused and nothing moved; `authorized` with `money_moved: false`
        means the bank holds no account in one of the two currencies.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmBody'
            example:
              method: device_biometric
              authenticated_at: '2026-09-24T10:02:30Z'
              evidence_ref: no-identity-provider:01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
              signed_amount: '1000.00'
              signed_currency: USD
              signed_payee: Your EUR account
      responses:
        '200':
          description: The quote, settled, and whether both legs moved
          content:
            application/json:
              example:
                id: 01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01
                from_account: acct-usd-1
                to_account: acct-eur-1
                sell_minor: 100000
                sell_currency: USD
                buy_minor: 86800
                buy_currency: EUR
                mid: '0.87237'
                rate: '0.86800815'
                spread_bps: 50
                spread_minor: 437
                remainder: 89880000
                remainder_denominator: 1000000000
                rate_source: European Central Bank reference rate
                rate_as_of: '2026-09-18'
                state: posted
                refused_reason: null
                created_at: '2026-09-24T10:02:00Z'
                authorize:
                  amount: '1000.00'
                  currency: USD
                  payee: Your EUR account
                out_core_ref: '11'
                in_core_ref: '12'
                money_moved: true
              schema:
                $ref: '#/components/schemas/ExchangeQuoteView'
        '400':
          description: >
            Not an authentication method, a single factor, an authentication outside its window, or signed
            figures that are not this quote's. The answer says which
        '409':
          description: Already settled, or too old to act on. One quote, one answer.
  /disputes:
    get:
      tags:
        - disputes
      summary: The caller's disputes, newest first, and the reasons a new one can have
      description: >
        Every sentence a customer reads is composed by the disputes module and sent here; the app renders it
        and composes none. `provisional_credit_proposed` says proposed because it is: a provisional credit is
        an approval a second person has not necessarily signed yet.
      responses:
        '200':
          description: Cases with their stage, what happens next and the moves the customer may make
          content:
            application/json:
              example:
                disputes:
                  - id: 01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b11
                    payment_kind: transfer
                    payment_ref: 01992c70-1a2b-7c3d-8e4f-5a6b7c8d9e0f
                    amount_minor: 12980
                    currency: USD
                    reason: not_received
                    reason_prompt: I paid and it never arrived
                    stage: evidence_required
                    next_step: 'We still need: What you ordered, and what arrived.'
                    deadline: null
                    outstanding:
                      - kind: order_detail
                        ask: What you ordered, and what arrived
                    provisional_credit_proposed: false
                    outcome: null
                    raised_at: '2026-09-24T09:12:00Z'
                    your_moves:
                      - supply_evidence
                      - withdraw
                reasons:
                  - reason: not_received
                    prompt: I paid and it never arrived
                    within_days: 120
                    needs:
                      - kind: order_detail
                        ask: What you ordered, and what arrived
                      - kind: merchant_contacted
                        ask: That you asked the merchant first
                    provisional_credit: false
              schema:
                $ref: '#/components/schemas/DisputeList'
    post:
      tags:
        - disputes
      summary: Dispute one of the caller's payments
      description: >
        The payment is looked up in the platform's own records and scoped to the caller: a card settlement on
        one of their cards, or a transfer of theirs that was authorised. Its amount and date come from there
        and never from this request. A reason outside the closed list, or raised after its reason's limit, is
        refused with the limit named. One payment has at most one open case: a second attempt answers 409 and
        carries the case that already exists.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - payment_kind
                - payment_ref
                - reason
              properties:
                payment_kind:
                  type: string
                  enum:
                    - card
                    - transfer
                payment_ref:
                  type: string
                  description: A card authorisation id
                  or a transfer id: null
                reason:
                  type: string
                  enum:
                    - not_recognised
                    - not_received
                    - not_as_described
                    - cancelled
                    - duplicate
                    - wrong_amount
      responses:
        '201':
          description: The case, and for the reasons that earn one, a provisional credit proposed
          content:
            application/json:
              example:
                id: 01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b10
                payment_kind: card
                payment_ref: auth-7Q2K9
                amount_minor: 685
                currency: USD
                reason: not_recognised
                reason_prompt: I do not recognise this payment
                stage: raised
                next_step: We have what we need. We will come back to you as this moves.
                deadline: null
                outstanding: []
                provisional_credit_proposed: true
                outcome: null
                raised_at: '2026-09-24T09:12:00Z'
                your_moves:
                  - withdraw
              schema:
                $ref: '#/components/schemas/DisputeCaseView'
        '400':
          description: Not a reason on the list, or raised after its limit, which the detail names
        '404':
          description: Not one of the caller's payments. Never 403
        '409':
          description: A case is already open on this payment, and the body carries it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisputeAlreadyOpen'
  /disputes/{id}/evidence:
    post:
      tags:
        - disputes
      summary: Supply something the case asked for
      description: >
        Only a kind the case's reason asks for is accepted. Supplying the last thing outstanding moves the
        case on in the same transaction, so a customer is never asked again for what they gave.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - reference
              properties:
                kind:
                  type: string
                  enum:
                    - order_detail
                    - cancellation_proof
                    - agreed_amount
                    - merchant_contacted
                reference:
                  type: string
                  minLength: 1
                  maxLength: 2000
                  pattern: \S
                  description: Trimmed before checking; must be non-blank and at most 2000 characters after trimming
      responses:
        '200':
          description: The case as it now stands
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisputeCaseView'
        '400':
          description: The case does not ask for that
        '404':
          description: Not the caller's case
        '409':
          description: The case is not asking for evidence at the moment
  /disputes/{id}/withdraw:
    post:
      tags:
        - disputes
      summary: Take a case back
      description: >
        Allowed from any open stage. Where money went back provisionally, its reversal is proposed in the same
        transaction, because withdrawing says the claim no longer stands.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The case, withdrawn
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisputeCaseView'
        '404':
          description: Not the caller's case
        '409':
          description: The case is already closed
  /backoffice/disputes:
    get:
      tags:
        - disputes
        - back-office
      summary: Open cases, oldest first, each with the moves the module allows
      description: >
        The moves are the disputes module's table, so this queue cannot offer a move the platform then
        refuses. Evidence is shown as the customer wrote it.
      responses:
        '200':
          description: The queue
          content:
            application/json:
              example:
                count: 1
                disputes:
                  - id: 01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b10
                    customer: ana
                    payment_kind: card
                    payment_ref: auth-7Q2K9
                    amount_minor: 685
                    currency: USD
                    reason: not_recognised
                    stage: raised
                    next_step: We have what we need. We will come back to you as this moves.
                    evidence: []
                    provisional_approval_id: 01992c7e-5b1b-7c00-9b11-2d3e4f5a6b7c
                    settlement_approval_id: null
                    moves:
                      - send_to_merchant
                      - decide
              schema:
                $ref: '#/components/schemas/BackofficeMoneyDisputeQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/disputes/{id}/moves:
    post:
      tags:
        - disputes
        - back-office
      summary: Send a case to the merchant, or decide it
      description: >
        Deciding is the operator's act and is recorded directly. What the decision does to money is not: an
        upheld case's refund, or a declined case's reversal of a provisional credit, is an approval of kind
        `dispute_refund` raised in the same transaction as the decision, which a different person approves.
        Its executor reads the amount from the case, refuses an approval the case did not record, and books
        money returned to the customer as money in, never as the payment seam's refund, which takes money out.
        Where the bank's funding account is configured (`PLATFORM_DISPUTE_FUNDING_ACCOUNT`), the money moves
        between it and the account the disputed payment left from, and the execution record carries
        `money_moved: true` and the core's `core_ref`; otherwise it is a ledger entry and says `money_moved:
        false`. A posted transfer can be disputed as well as an authorised one.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - move
              properties:
                move:
                  type: string
                  enum:
                    - send_to_merchant
                    - decide
                  description: >
                    Unknown label is 400. The customer labels supply_evidence and withdraw parse but are
                    refused with 409.
                outcome:
                  type: string
                  enum:
                    - upheld
                    - declined
                  description: Required when move is decide (else 400); ignored for send_to_merchant.
      responses:
        '200':
          description: The case as it now stands, with any approval it raised
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyDisputeCase'
        '400':
          description: Not a move, or a decision without an outcome
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a case in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: The module does not allow that move from this stage, or somebody moved it first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /support:
    get:
      tags:
        - support
      summary: The caller's own conversations
      responses:
        '200':
          description: Conversations, each showing whose turn it is
          content:
            application/json:
              example:
                - id: 9f8e7d6c-5b4a-4938-2716-0a5b4c3d2e1f
                  topic: A payment I do not recognise
                  turn: waiting_on_us
                  waiting_on_us: true
                  resolved: false
                  created_at: '2026-09-22T09:41:00Z'
                  messages:
                    - author: customer
                      channel: written
                      body: There is a payment on my card I did not make
                      at: '2026-09-22T09:41:00Z'
                      from_assistant: false
                  callback:
                    state: requested
                    attempts: 0
                    still_owed: true
                    topic: A payment I do not recognise
                    requested_at: '2026-09-22T09:44:00Z'
                  callback_note: we call you on the number we already hold; we will never ask you to give one out
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SupportConversation'
    post:
      tags:
        - support
      summary: Write in
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                topic:
                  type: string
                  nullable: true
                message:
                  type: string
                  nullable: true
      responses:
        '201':
          description: The conversation, now waiting on the bank
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportConversation'
  /support/{id}/callback:
    post:
      tags:
        - support
      summary: Ask us to call
      description: >
        Written first, and voice is a callback the bank makes. A call is written back into the same thread so
        an agent never makes the customer explain it twice.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                topic:
                  type: string
                  nullable: true
                  description: Refused (400) if it looks like card data
      responses:
        '200':
          description: A callback is owed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportConversation'
        '404':
          description: Not the caller's conversation
    delete:
      tags:
        - support
      summary: Never mind, no call needed
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The callback is no longer owed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportConversation'
        '404':
          description: Not the caller's conversation
  /support/{id}/messages:
    post:
      tags:
        - support
      summary: Say something
      description: >
        Reopens a resolved thread, because a reply is a continuation. A message carrying what looks like card
        data is refused outright: no card number reaches platform storage or logs by any door, and customers
        paste them (Principle VIII).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  minLength: 1
                  pattern: \S
                  description: Must be non-blank; stored trimmed; refused (400) if it looks like card data
      responses:
        '200':
          description: The conversation, now waiting on the bank
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportConversation'
        '400':
          description: Empty, or carrying something that looks like card data
        '404':
          description: Not the caller's conversation
  /backoffice/support:
    get:
      tags:
        - support
        - back-office
      summary: The queue, by who has been waiting longest on us
      description: >
        Whose turn it is is derived from the messages rather than stored, so the filtering happens in the
        handler. A `WHERE turn = 'us'` would need a stored turn, which is the thing this avoids. `calls_owed`
        is carried separately because a callback owed is a different debt from a reply owed.
      responses:
        '200':
          description: Conversations waiting on the bank, longest first, and the calls owed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeSupportQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/support/{id}/draft:
    post:
      tags:
        - support
        - back-office
      summary: A reply for the operator to edit and send, from the assistant
      description: >
        Spec 061. From the packet the assistant handed over with the conversation and its transcript: the
        model's draft where PLATFORM_ASSISTANT_MODEL_URL names one, the rules' where it does not. **This sends
        nothing**: the operator edits it and replies as themselves.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The draft, and what wrote it
          content:
            application/json:
              schema:
                type: object
                required:
                  - draft
                  - by
                properties:
                  draft:
                    type: string
                  by:
                    type: string
                    description: rules, or model <name>
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such conversation
  /assistant:
    post:
      tags:
        - support
      summary: Start a conversation with OxaBank's assistant
      description: >
        Spec 061. Voiager's machine and policy table, on the customer's own accounts and cards: it reads,
        freezes a card once the customer says yes, offers a statement, and hands everything else in its table
        to a person as a support conversation with the packet and the transcript. Marked as an assistant,
        never disguised.
      responses:
        '201':
          description: The assistant's opening line
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantConversation'
  /assistant/{id}:
    get:
      tags:
        - support
      summary: A conversation with the assistant, the caller's own
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The conversation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantConversation'
        '404':
          description: Not the caller's
  /assistant/{id}/turns:
    post:
      tags:
        - support
      summary: Say something to the assistant
      description: >
        Spec 061. Read by the rules, or where they find nothing by the optional model as one word from a
        closed list. An act (freezing a card) happens only after a yes to that exact question, through the
        same route the app uses, as the customer. Nothing card-shaped is kept.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
                  maxLength: 1000
      responses:
        '200':
          description: The conversation, with this turn's reply
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantConversation'
              example:
                id: 0199d000-0000-7000-8000-0000000000f1
                phase: confirm
                finished: false
                answered_by: rules
                support_id: null
                may:
                  - Read your balances, your recent activity and last month's statement
                never:
                  - Move money
                messages:
                  - who: assistant
                    text: Hi Anna, I'm OxaBank's assistant. What can I do for you?
                    at: '2026-10-05T21:00:00Z'
                    from_assistant: true
                  - who: customer
                    text: I lost my card
                    at: '2026-10-05T21:00:05Z'
                    from_assistant: false
                  - who: assistant
                    text: >-
                      I can freeze your card ending 4242 now, so nothing more goes through on it. You can
                      unfreeze it later in Cards. Shall I?
                    at: '2026-10-05T21:00:05Z'
                    from_assistant: true
                reply:
                  lines:
                    - >-
                      I can freeze your card ending 4242 now, so nothing more goes through on it. You can
                      unfreeze it later in Cards. Shall I?
                  spoken:
                    - >-
                      I can freeze your card ending in four two four two now, so nothing more goes through on
                      it. You can unfreeze it later in Cards. Shall I?
                  offers: []
                  suggestions:
                    - Yes, please
                    - No, thanks
                  finished: false
                  handed_over: false
                voice:
                  voice: af_heart
                  speed: 0.95
                  acknowledgements:
                    - Sure, one moment.
                    - Okay, let me check.
                  not_heard: Sorry, I didn't catch that. Could you say it again?
                created_at: '2026-10-05T21:00:00Z'
        '400':
          description: Empty
          too long: null
          or card-shaped: null
        '404':
          description: Not the caller's
  /backoffice/support/calls:
    post:
      tags:
        - support
        - back-office
      summary: Move a callback along
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: routes/support.rs `CallProgressBody`.
              required:
                - conversation_id
                - outcome
              properties:
                conversation_id:
                  type: string
                  format: uuid
                outcome:
                  $ref: '#/components/schemas/BackofficeCallbackOutcome'
                summary:
                  type: string
                  nullable: true
                  description: >
                    What was said, written into the thread as the agent's voice message. Required for outcome
                    completed: at least 8 bytes after trimming, and no card number (400). Ignored for
                    scheduled and no_answer. Stored trimmed.
      responses:
        '200':
          description: The conversation, with the call's outcome written into the thread
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportConversation'
        '400':
          description: >
            Problem. `outcome` is not scheduled|no_answer|completed; outcome completed with a summary under 8
            bytes after trimming (or none); a completed summary that looks like a card number; or several
            tenant memberships. Malformed JSON is also 400 (axum, plain text).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such conversation in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: >
            The callback's current state does not allow the move, e.g. scheduling one that is
            completed/abandoned/already scheduled, no_answer on one merely requested, completing one that is
            completed/abandoned. Problem, detail "a callback that is <state> cannot be <action>".
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '415':
          description: Content-Type is not application/json (axum, plain text).
        '422':
          description: >-
            Missing conversation_id/outcome, conversation_id not a UUID, or wrong JSON types (axum, plain
            text).
        '500':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: No database configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/support/replies:
    post:
      tags:
        - support
        - back-office
      summary: Reply to a customer
      description: >
        `from_assistant` is **told, not detected**. An assistant's reply does not discharge the bank's turn,
        and a queue that counted it would look healthy while nobody was being helped - the most common way
        support metrics are gamed. A conversation is answered only when a person sends one. Agents paste card
        numbers too, so a reply carrying what looks like card data is refused here as well.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - conversation_id
                - body
              properties:
                conversation_id:
                  type: string
                  format: uuid
                body:
                  type: string
                  minLength: 1
                  description: Must be non-empty after trim; refused (400) if it looks like card data. Stored trimmed.
                from_assistant:
                  type: boolean
                  default: false
                channel:
                  type: string
                  nullable: true
                  enum:
                    - chat
                    - voice
                    - null
                  default: chat
                  description: Server accepts any string; anything other than "voice" is stored as "chat".
      responses:
        '200':
          description: The conversation. Whose turn it is now depends on who actually replied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportConversation'
        '400':
          description: Empty, or carrying something that looks like card data
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such conversation in this tenant
  /backoffice/support/resolve:
    post:
      tags:
        - support
        - back-office
      summary: Mark a conversation resolved, or reopen it
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - conversation_id
              properties:
                conversation_id:
                  type: string
                  format: uuid
                resolved:
                  type: boolean
                  default: true
      responses:
        '200':
          description: The conversation. A customer's next message reopens it regardless
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupportConversation'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such conversation in this tenant
  /backoffice/days/{day}:
    get:
      tags:
        - back-office
        - reconciliation
      summary: What closing a business day would meet, and how its close went
      description: >
        Spec 033. The day is a calendar day in the bank's zone (`PLATFORM_BANK_TIMEZONE`, UTC by default).
        Lists the payments still `posting` that were authorised in it, each bound rail's cut-off on it and
        whether it has passed, the reconciliation runs that covered it with their exceptions, the core jobs a
        close runs, and the close: `open`, `proposed`, `closed` (with the core's report job by job and how
        many balances were kept) or `failed` (with why). Spec 035: each rail's `holiday` on the day by its
        calendar (`PLATFORM_CALENDARS`, `PLATFORM_HOLIDAYS`), or null; and `close.proposed_by` is `schedule`
        where the platform proposed the day itself (`PLATFORM_DAY_CLOSE_AT`), its proposal carrying the day's
        checks. Spec 049: `business_date`, the core's open business day where it keeps one (null where it
        books on the calendar), and `not_the_open_day`, why this day cannot be closed now (days are closed in
        order, each the core's open one), or null; a closed day's `close.report` holds its `transactions` (the
        day's movements in the core for every account a balance was kept for, `by_kind` with kind, rail,
        currency, count, in_minor and out_minor, and the `exceptions` the platform did not make or cannot
        name), what was `carried_over` (transfers, exchanges half done, pulls, money arriving, approvals that
        move money, card holds) and its `loans` (how many, in arrears and by how much, the interest the day
        accrued, the newly overdue).
      parameters:
        - name: day
          in: path
          required: true
          schema:
            type: string
            format: date
      responses:
        '200':
          description: The day's checks and its close
          content:
            application/json:
              example:
                day: '2026-09-30'
                zone: UTC
                over: true
                posting: []
                cutoffs:
                  - rail: sepa
                    cutoff: '14:00'
                    zone: Europe/London
                    at: '2026-09-30T13:00:00Z'
                    passed: true
                    holiday: null
                reconciliation:
                  - run_id: 01a0f7b1-0000-7000-8000-000000000001
                    account_ref: '5'
                    ran_by: schedule
                    clean: 4
                    exceptions: 0
                jobs:
                  - SA_PINT
                  - ACC_AATR
                  - LA_ARAG
                  - LA_OPEN
                  - LA_UNPA
                  - ACC_RBAL
                  - TBL_UDET
                  - LA_ECOB
                business_date: '2026-09-30'
                not_the_open_day: null
                close:
                  state: open
              schema:
                type: object
        '400':
          description: Not a day written YYYY-MM-DD
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/days/{day}/close:
    post:
      tags:
        - back-office
      summary: Propose closing a business day, for a second operator to approve
      description: >
        Spec 033. Proposes a `day_close` approval. Once a second operator approves it, the core's close of
        business runs (each job reported), every customer account's balance at that moment is kept with the
        day, and the day is closed — or `failed` with why, after which it may be proposed again. A day not yet
        over in the bank's zone, or already proposed or closed, is refused. Spec 049: where the core keeps a
        business date, so is a day that is not its open one; closing advances it by a day before the jobs run
        (Loan COB among them), and the close keeps the day's transactions, what carried over, and every loan's
        position.
      parameters:
        - name: day
          in: path
          required: true
          schema:
            type: string
            format: date
      responses:
        '201':
          description: Proposed; it waits for a second operator under /approvals
          content:
            application/json:
              example:
                day: '2026-09-30'
                approval_id: 01a0f7b1-0000-7000-8000-000000000002
                close:
                  state: proposed
              schema:
                type: object
        '400':
          description: Not a day written YYYY-MM-DD
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: The day is not over yet, or is already proposed or closed
  /backoffice/statements:
    get:
      tags:
        - reconciliation
        - back-office
      summary: The bank statements the platform holds
      description: >
        The tenant's statements, the most recently ingested first: each one's heading, the file it came from
        and how many lines it held. Under the back office, so only the bank's operators reach it; the contract
        had it at `/statements`, where a customer's session would have been all that was asked. Amounts are
        decimal strings.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        '200':
          description: The statements, newest first
          content:
            application/json:
              example:
                statements:
                  - id: 01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e20
                    account_ref: USD-OPERATING
                    statement_ref: STMT-2026-09-29
                    opening_balance: '125000.0000'
                    closing_balance: '126250.0000'
                    currency: USD
                    from: '2026-09-29'
                    to: '2026-09-29'
                    source: camt053-2026-09-29.xml
                    ingested_at: '2026-09-30T06:10:00Z'
                    entry_count: 3
              schema:
                type: object
                required:
                  - statements
                properties:
                  statements:
                    type: array
                    items:
                      $ref: '#/components/schemas/BankStatement'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
    post:
      tags:
        - reconciliation
        - back-office
      summary: Hand the platform a statement the bank sent
      description: >
        The bank's side of reconciliation. The body is the file as the bank delivered it — CAMT.053 today —
        and it is parsed by `BankStatementAdapter`, never by a parser of the route's own. Each statement in it
        is stored once per account and statement reference: the same file again is `already_held` and stores
        nothing. Every line keeps the bank's status, and reconciliation reads a line that is not booked as
        provisional rather than as a break. `reconciles` says whether the booked lines add up from the opening
        to the closing balance; a file that does not is stored and flagged, because it is what the bank said.
      parameters:
        - name: source
          in: query
          required: true
          schema:
            type: string
            minLength: 1
          description: >-
            The file's name; trimmed, and blank after trimming is 400. Stored as source_file on every
            statement.
      requestBody:
        required: true
        description: >
          The statement file's bytes, as the bank delivered it. The handler takes axum::body::Bytes and never
          reads Content-Type, so ANY content type is accepted (application/xml, text/xml,
          application/octet-stream, or none); what matters is that the bytes are UTF-8 CAMT.053 XML (the only
          BankStatementAdapter wired in state.rs is platform_adapter_camt053::Camt053). Limit 2 MiB.
        content:
          application/xml:
            schema:
              type: string
              format: binary
          text/xml:
            schema:
              type: string
              format: binary
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '201':
          description: What each statement in the file came to
          content:
            application/json:
              example:
                source: camt053-2026-09-15.xml
                statements:
                  - statement_ref: STMT-2026-258
                    account_ref: DE89370400440532013000
                    currency: EUR
                    period_from: '2026-09-15'
                    period_to: '2026-09-15'
                    outcome: stored
                    statement_id: 01a0d5d0-1a2b-7c3d-8e4f-5a6b7c8d9e03
                    entries: 3
                    pending_entries: 1
                    reconciles: true
              schema:
                $ref: '#/components/schemas/BackofficeStatementUpload'
        '400':
          description: Not a statement this platform reads; the parser's reason is given and nothing is stored
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '413':
          description: Body over axum's default 2 MiB limit (no DefaultBodyLimit is configured). Plain text.
        '500':
          description: A store failure part way through a multi-statement file; statements before it stay stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: No database configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/reconciliation/runs:
    get:
      tags:
        - reconciliation
        - back-office
      summary: Recent runs, newest first
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int64
            default: 50
            minimum: 1
            maximum: 200
          description: Clamped to 1..200 rather than refused. The contract has no default; the code's is 50.
      responses:
        '200':
          description: Runs, each with what every source offered and what disagreed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationRunList'
        '400':
          description: >-
            `limit` is not an integer (axum Query rejection, plain text), or several tenant memberships
            (Problem).
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '500':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
    post:
      tags:
        - reconciliation
        - back-office
      summary: Reconcile one account over a period
      description: >
        Acts directly rather than proposing: running a comparison changes nothing, and what comes out of it is
        where a person and a second signature belong. A period in which one source offered nothing while the
        others offered something is **refused as a run** rather than reported as breaks, because that is a
        missing statement far more often than a quiet day. A run that could not read one source completely
        says so, and does not present unread records as disagreements.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReconciliationRunRequest'
      responses:
        '200':
          description: The run, with each disagreement classified rather than merely counted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationRun'
        '400':
          description: Not a period that can be reconciled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '409':
          description: A source offered nothing while the others offered something. Refused as a run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: No database, no core adapter, or the core failed while paging
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/reconciliation/runs/{id}:
    get:
      tags:
        - reconciliation
        - back-office
      summary: One run and its breaks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The run, the counts each source offered, and every break with its kind
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationRun'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such run in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '500':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: Documented from the code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /backoffice/reconciliation/exceptions:
    get:
      tags:
        - reconciliation
        - back-office
      summary: Breaks still open, newest run first
      description: >
        A disagreement that will resolve itself with time stays visible while it is unresolved: the books do
        not balance until it does.
      responses:
        '200':
          description: Open breaks, each saying what acting on it would actually do
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationQueue'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
  /backoffice/reconciliation/resolve:
    post:
      tags:
        - reconciliation
        - back-office
      summary: Close a break, or propose the entry that will close it
      description: >
        `explained` and `written_off` act directly: they are an operator writing down what they found, the
        same exception the onboarding verdict route makes and for the same reason. `adjusting_entry` does not
        - it posts money to the ledger, so it raises a proposal, and the break **stays open until a second
        person approves it and the entry actually lands**. Only a payment the engine and the bank both saw and
        the bank never booked is resolvable by posting; every other kind is refused that route.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReconciliationResolveRequest'
      responses:
        '200':
          description: Closed, for the two resolutions an operator may complete alone
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationResolved'
        '202':
          description: Proposed. The break stays open until the entry is actually posted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationAdjustmentProposed'
        '400':
          description: Not a resolution, a break this resolution does not apply to, or a token note
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: No such break in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: >
            MISSING FROM THE CONTRACT. Break already closed; an entry already proposed for it; the break has
            no record/amount to post from; or a concurrent resolve/attach won.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
  /sessions:
    get:
      tags:
        - sessions
      summary: Where the caller is signed in
      description: >
        The caller's live sessions, most recently used first, filtered by tenant and subject in the query.
        `current` is true on exactly one, the session this request came from. `last_used_at` is right to
        within five minutes. The device identifier is never returned.
      responses:
        '200':
          description: The caller's live sessions
          content:
            application/json:
              example:
                sessions:
                  - id: 01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01
                    name: null
                    model: Google Pixel 8
                    os: Android 14
                    app_version: 1.0.0
                    began_at: '2026-09-25T09:00:00Z'
                    last_used_at: '2026-09-25T09:14:00Z'
                    expires_at: '2026-09-26T09:00:00Z'
                    current: true
                  - id: 01992d7a-5e6f-7a8b-9c0d-1e2f3a4b5c02
                    name: Work laptop
                    model: Chrome
                    os: Windows
                    app_version: 1.0.0
                    began_at: '2026-09-24T18:30:00Z'
                    last_used_at: '2026-09-24T19:05:00Z'
                    expires_at: '2026-09-25T18:30:00Z'
                    current: false
              schema:
                type: object
                required:
                  - sessions
                properties:
                  sessions:
                    type: array
                    items:
                      $ref: '#/components/schemas/SessionView'
        '401':
          description: '`signed-out`: no live session holds this token'
        '503':
          description: No store
    post:
      tags:
        - sessions
      summary: Begin this token's session on this device
      description: >
        The one request a token makes before its session exists, so it is not refused for having none, and it
        verifies its own token. Beginning ends the live session this device had for this customer, as
        `replaced`. What the device says it is gets recorded for a person to recognise and is trusted for
        nothing. The device identifier is never returned, here or anywhere: it is the device's, and nobody
        else needs it. A token whose session has ended begins nothing and is answered `signed-out`, the same
        whether its session ended, expired or was replaced. Spec 043: a person the provider verified who is a
        member of no tenant first joins `PLATFORM_SIGNUP_TENANT` as a customer, once, audited. A token
        carrying the provider's session (`sid`) of a live session from this device continues it: 200, the same
        session, the new token holding it and the old one no longer. A provider session whose platform session
        was signed out or ended by its person or an operator begins nothing and is answered `signed-out`; its
        person signs in again.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - device_id
              properties:
                device_id:
                  type: string
                  pattern: ^[A-Za-z0-9_-]{16,64}$
                model:
                  type: string
                  maxLength: 64
                  description: Trimmed. No control character
                os:
                  type: string
                  maxLength: 64
                  description: Trimmed. No control character
                app_version:
                  type: string
                  maxLength: 64
                  description: Trimmed. No control character
            example:
              device_id: 9f2c4e1ab7d0435e8c6f1a2b3c4d5e6f
              model: Google Pixel 8
              os: Android 14
              app_version: 1.0.0
      responses:
        '200':
          description: >
            This token's session already exists and is live. The same body, so that a retried begin is not a
            failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionView'
        '201':
          description: A session begun
          content:
            application/json:
              example:
                id: 01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01
                name: null
                model: Google Pixel 8
                os: Android 14
                app_version: 1.0.0
                began_at: '2026-09-25T09:00:00Z'
                last_used_at: '2026-09-25T09:00:00Z'
                expires_at: '2026-09-26T09:00:00Z'
                current: true
              schema:
                $ref: '#/components/schemas/SessionView'
        '400':
          description: >
            A description outside the bounds above; the detail says which. Or a subject in several tenants
            that named none, as on every route
        '401':
          description: >
            The token is not valid; or `signed-out`, because this token's session has ended. One answer
            whatever the reason, because telling a caller which would tell somebody holding a copied token
            what happened to its owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://oxabank.dev/problems/signed-out
                title: Signed out
                status: 401
                detail: You are signed out on this device. Sign in again.
                correlation_id: 01992e41-8b2c-7d3e-9f4a-5b6c7d8e9f01
        '403':
          description: The subject belongs to no tenant
        '409':
          description: Another sign-in from this device won a race with this one. Beginning again succeeds
        '503':
          description: No store, or the identity provider cannot be reached
  /sessions/{id}/end:
    post:
      tags:
        - sessions
      summary: End one of the caller's sessions
      description: >
        Ending the session this request came from is signing out, and answers `signed_out`; this response is
        delivered, and the token is refused from its next request. Ending another of the caller's sessions
        answers `ended_by_customer`. One that has already ended answers 200 with how it ended, so that a retry
        after a lost response is not a failure.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: How the session ended
          content:
            application/json:
              example:
                id: 01992d7a-5e6f-7a8b-9c0d-1e2f3a4b5c02
                ended_at: '2026-09-25T09:15:00Z'
                ending: ended_by_customer
              schema:
                $ref: '#/components/schemas/SessionEndedView'
        '401':
          description: '`signed-out`: the session this request came from has already ended'
        '404':
          description: |
            Not one of the caller's sessions. Somebody else's and one that never existed get the same answer
  /sessions/{id}/name:
    post:
      tags:
        - sessions
      summary: Name a device, through one of its sessions
      description: >
        The name is the device's, reached through one of its sessions, because the device identifier is never
        returned. The session has to be one of the caller's and live. Trimmed, then at most 64 characters,
        none of them a control character, in any script; an empty name removes it. Every device of the
        customer's shows it, and the device keeps it when it signs in again. Every change appends
        `session.named` to the audit, saying whether a name was set or removed and never what it was; saving
        the name a device already has changes nothing and appends nothing. The platform keeps the name sealed
        and opens it only to answer this customer. No operator reads it.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Trimmed, then at most 64 characters, no control character. Empty removes the name
            example:
              name: Work laptop
      responses:
        '200':
          description: The session as the list shows it, with the name it now has
          content:
            application/json:
              example:
                id: 01992d7a-5e6f-7a8b-9c0d-1e2f3a4b5c02
                name: Work laptop
                model: Chrome
                os: Windows
                app_version: 1.0.0
                began_at: '2026-09-24T18:30:00Z'
                last_used_at: '2026-09-24T19:05:00Z'
                expires_at: '2026-09-25T18:30:00Z'
                current: false
              schema:
                $ref: '#/components/schemas/SessionView'
        '400':
          description: Longer than 64 characters, or a control character in it; the detail says which
        '401':
          description: '`signed-out`: no live session holds this token'
        '404':
          description: >
            Not one of the caller's live sessions. Somebody else's, one that ended and one that never existed
            get the same answer
        '503':
          description: No store
  /backoffice/sessions:
    get:
      tags:
        - sessions
        - backoffice
      summary: A customer's live sessions, for an operator
      description: >
        Operators only: anybody else is refused with 403 before a row is read, and the body is the forbidden
        problem, saying nothing about any customer. The sessions are by what each device says it is, without
        `current`, since the operator is on none of them, and without `name`, which is the customer's alone. A
        subject with no live sessions, or unknown to the tenant, is 200 with an empty list: telling the two
        apart would answer "is this person a customer here?".
      parameters:
        - name: subject
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The customer's live sessions
          content:
            application/json:
              example:
                subject: customer-1
                sessions:
                  - id: 01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01
                    model: Google Pixel 8
                    os: Android 14
                    app_version: 1.0.0
                    began_at: '2026-09-25T09:00:00Z'
                    last_used_at: '2026-09-25T09:14:00Z'
                    expires_at: '2026-09-26T09:00:00Z'
              schema:
                type: object
                required:
                  - subject
                  - sessions
                properties:
                  subject:
                    type: string
                  sessions:
                    type: array
                    items:
                      type: object
                      required:
                        - id
                        - model
                        - os
                        - app_version
                        - began_at
                        - last_used_at
                        - expires_at
                      properties:
                        id:
                          type: string
                          format: uuid
                        model:
                          type: string
                        os:
                          type: string
                        app_version:
                          type: string
                        began_at:
                          type: string
                          format: date-time
                        last_used_at:
                          type: string
                          format: date-time
                        expires_at:
                          type: string
                          format: date-time
        '400':
          description: No `subject`
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '503':
          description: No store
  /backoffice/sessions/{id}/end:
    post:
      tags:
        - sessions
        - backoffice
      summary: End one of a customer's sessions, for them
      description: >
        One operator, no second person. The audit names the operator as the actor and the customer in its
        detail. An already ended session answers 200 with how it ended; that session's token is refused from
        its next request.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: How the session ended
          content:
            application/json:
              example:
                id: 01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01
                ended_at: '2026-09-25T09:20:00Z'
                ending: ended_by_operator
              schema:
                $ref: '#/components/schemas/SessionEndedView'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a session in the operator's tenant
  /sessions/end-others:
    post:
      tags:
        - sessions
      summary: Sign out everywhere else
      description: >
        Ends every live session of the caller's but the one this request came from, each as
        `ended_by_customer` and each with its own audit entry naming the session that asked. A session whose
        token had already lapsed is written down as `expired` at its expiry and is not counted. Nothing else
        to end answers `ended: 0`.
      responses:
        '200':
          description: How many sessions this ended
          content:
            application/json:
              example:
                ended: 2
              schema:
                type: object
                required:
                  - ended
                properties:
                  ended:
                    type: integer
                    minimum: 0
        '401':
          description: '`signed-out`: no live session holds this token'
        '503':
          description: No store
  /backoffice/loans/{id}/repayments/{repayment_id}/reverse:
    post:
      tags:
        - lending
        - back-office
      summary: Propose reversing a repayment
      description: >
        A mistaken repayment is corrected by a **reversing row**, never by editing or deleting one: an arrears
        position that can be changed retroactively is not evidence of anything. Both rows survive - the
        original stays, marked reversed by the row pointing at it, and the reversal names its author. Proposes
        rather than acts, because reversing a repayment puts a customer back into arrears, and that is a
        notice, a fee and possibly a credit file entry. The reversing row carries the **target's**
        `received_on` and no `external_ref` of its own.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: repayment_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - note
              properties:
                note:
                  type: string
                  minLength: 10
                  description: Trimmed, char count >= 10, else 400.
      responses:
        '202':
          description: Proposed. Nothing is reversed until a second person agrees
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackofficeMoneyReversalProposed'
        '400':
          description: No reason given, or one too thin to stand as a record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          $ref: '#/components/responses/OperatorsOnly'
        '404':
          description: Not a repayment of this loan in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: Already reversed, or the row named is itself a reversal
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
components:
  securitySchemes:
    oidc:
      type: openIdConnect
      openIdConnectUrl: https://id.your-bank.example/.well-known/openid-configuration
  parameters:
    PathId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Offset:
      name: offset
      in: query
      schema:
        type: integer
        minimum: 0
        default: 0
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: Reserved. The platform does not read it yet; repeating a write repeats it.
  responses:
    OperatorsOnly:
      description: >
        Not an operator of the tenant: a member who is not marked one, or somebody who belongs to no tenant.
        Decided before the route reads anything; the same body on every operator route whatever was asked,
        with nothing of any customer, card, approval, entry or event in it.
      content:
        application/json:
          example:
            type: https://oxabank.dev/problems/forbidden
            title: Forbidden
            status: 403
            detail: This is for the bank's operators.
            correlation_id: 01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e99
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Provenance:
      type: string
      enum:
        - live
        - synthetic
        - unavailable
      description: >
        Required on every verification, screening or decision result. `unavailable` means the capability does
        not exist yet and is never replaced by an invented outcome.
    Problem:
      type: object
      description: RFC 9457 problem detail. Carries no internal or vendor detail.
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        correlation_id:
          type: string
          format: uuid
    Tenant:
      type: object
      required:
        - id
        - slug
        - status
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        display_name:
          type: string
        status:
          type: string
          enum:
            - active
            - suspended
    ApprovalState:
      type: string
      enum:
        - proposed
        - approved
        - rejected
        - executed
        - failed
    NewApproval:
      type: object
      required:
        - subject_type
        - subject
      properties:
        subject_type:
          type: string
          example: journal_entry
        subject:
          type: object
        proposal_id:
          type: string
          format: uuid
          nullable: true
    Approval:
      description: >
        Corrected: the server has no richer approval view. Every approvals route returns ApprovalView; the
        fields tenant_id, subject_type, proposed_at, decided_at, rejection_reason and proposal_id are never
        sent. Kept as an alias so existing $refs (POST /approvals 201, POST /rules 201) stay valid.
      allOf:
        - $ref: '#/components/schemas/ApprovalView'
    AuditEntry:
      type: object
      description: routes/audit.rs `AuditView`. No tenant_id and no detail are returned.
      required:
        - id
        - correlation_id
        - actor
        - action
        - subject_type
        - subject_id
        - occurred_at
        - hash
      properties:
        id:
          type: integer
          format: int64
          description: Chain position; ascending
        correlation_id:
          type: string
          format: uuid
        actor:
          type: string
        action:
          type: string
        subject_type:
          type: string
        subject_id:
          type: string
        occurred_at:
          type: string
          format: date-time
        hash:
          type: string
          pattern: ^[0-9a-f]{64}$
          description: SHA-256 chain hash, 64 lowercase hex characters
    ChainVerification:
      type: object
      description: routes/audit.rs `ChainVerification`. All four keys are always present.
      required:
        - intact
        - entries_checked
        - first_divergence_at
        - reason
      properties:
        intact:
          type: boolean
        entries_checked:
          type: integer
          minimum: 0
          description: >
            When intact, the chain length. When broken, the zero-based INDEX of the divergent entry (i.e. how
            many entries verified before it), not the chain length.
        first_divergence_at:
          type: integer
          format: int64
          nullable: true
          description: The AuditEntry.id of the first divergent entry. Null when intact.
        reason:
          nullable: true
          description: Null when intact.
          allOf:
            - $ref: '#/components/schemas/AuditVerifyReason'
    Rule:
      type: object
      properties:
        id:
          type: string
        version:
          type: integer
        scope:
          type: string
        condition:
          type: object
        action:
          type: object
        author:
          type: string
        approved_by:
          type: string
          nullable: true
        effective_from:
          type: string
          format: date-time
        status:
          type: string
          enum:
            - draft
            - active
            - superseded
    Proposal:
      type: object
      description: platform-api/src/routes/proposals.rs `view`. A suggestion, never an instruction.
      required:
        - id
        - subject_type
        - subject_ref
        - classification
        - explanation
        - suggested_action
        - confidence
        - evidence
        - provenance
        - provider
        - provider_version
        - prompt_hash
        - created_at
      properties:
        id:
          type: string
          format: uuid
        subject_type:
          type: string
          description: What was judged — `screening_match`
        subject_ref:
          type: string
          description: Which one — for a screening match, `key@revision`
        classification:
          type: string
          description: For a screening match, `likely_false_positive`, `likely_true_match` or `needs_review`
        explanation:
          type: string
        suggested_action:
          type: object
          nullable: true
          description: 'For a screening match, `{verdict: cleared|confirmed}`; null when the proposer suggests nothing'
        confidence:
          type: number
          minimum: 0
          maximum: 1
        evidence:
          type: array
          items:
            type: object
            required:
              - code
              - detail
            properties:
              code:
                type: string
              detail:
                type: string
        provenance:
          $ref: '#/components/schemas/Provenance'
        provider:
          type: string
          description: '`rule-based` for the deterministic proposer'
        provider_version:
          type: string
        prompt_hash:
          type: string
          description: SHA-256 of the canonical enquiry
        created_at:
          type: string
          format: date-time
    BankStatement:
      type: object
      properties:
        id:
          type: string
          format: uuid
        account_ref:
          type: string
        statement_ref:
          type: string
        opening_balance:
          type: string
        closing_balance:
          type: string
        currency:
          type: string
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        entry_count:
          type: integer
        source:
          type: string
          description: The file the statement arrived in
        ingested_at:
          type: string
          format: date-time
    ReplayRequest:
      type: object
      description: >
        routes/events.rs `ReplayBody`. `from` is serde-untagged: a JSON string is read as a name (only
        "earliest" and "latest" are accepted; any other string is 400), a JSON non-negative integer as a
        position (u64; a negative or fractional number fails deserialisation, 422). `topic` outside the enum
        is 400.
      required:
        - topic
        - from
      properties:
        topic:
          $ref: '#/components/schemas/BackofficeReplayTopic'
        from:
          oneOf:
            - type: string
              enum:
                - earliest
                - latest
            - type: integer
              format: int64
              minimum: 0
        limit:
          type: integer
          format: int64
          minimum: 0
          default: 1000
          nullable: true
          description: >
            How many of the tenant's published rows (all topics) to look at in this call, not how many to
            send. Absent or null means 1000; the value is clamped to 1..10000.
    ApprovalView:
      type: object
      description: >
        What every approvals route actually returns (routes/approvals.rs `ApprovalView`): GET /approvals
        items, the 201 of POST /approvals and the 200 of POST /approvals/{id}/decision. Deliberately narrow:
        no tenant_id, proposed_at, decided_at, rejection_reason or proposal_id. `kind` is the subject_type, so
        a screen knows whose role decides it.
      required:
        - id
        - kind
        - state
        - proposed_by
        - decided_by
        - correlation_id
        - subject
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          description: 'What it is: ApprovalKind''s label (teller_movement, loan_decision, role_change…)'
        state:
          $ref: '#/components/schemas/ApprovalState'
        proposed_by:
          type: string
          description: The proposer's verified token subject
        decided_by:
          type: string
          nullable: true
          description: The decider's verified token subject. Null while `proposed`.
        correlation_id:
          type: string
          format: uuid
        subject:
          description: >
            The proposal's subject exactly as proposed (a serde_json::Value stored as jsonb). Any JSON value;
            in practice an object. Its shape depends on the approval kind.
    AuditVerifyReason:
      type: string
      description: >
        The Rust Debug name of `platform_audit::verify::Divergence`, i.e. PascalCase, not snake_case.
        ContentAltered - the stored hash no longer matches the entry (edited). LinkBroken - prev_hash does not
        match the previous entry (inserted, removed or reordered). MissingGenesis - the chain's first entry
        has a prev_hash.
      enum:
        - ContentAltered
        - LinkBroken
        - MissingGenesis
    ReconciliationSide:
      type: string
      enum:
        - engine
        - core
        - bank
    ReconciliationExceptionKind:
      type: string
      description: >
        `format!("{:?}", kind).to_lowercase()` of recon::ExceptionKind - Debug name lowercased with NO
        underscores (partialrefund, missingincore, missingatbank), not the serde snake_case form. Only
        `missingincore` can be resolved with `adjusting_entry`.
      enum:
        - timing
        - fee
        - partialrefund
        - duplicate
        - missingincore
        - missingatbank
        - unknown
    ReconciliationResolution:
      type: string
      description: >
        CHECK constraint in the schema. `adjusting_entry` is only ever written when the adjusting approval
        executes (reconciliation_store::close_for_approval), never by POST.../resolve.
      enum:
        - explained
        - written_off
        - adjusting_entry
    ReconciliationRecord:
      type: object
      description: routes/reconciliation.rs `record_json`, one side's record as the run compared it.
      required:
        - side
        - id
        - amount
        - currency
        - date
        - references
        - provisional
      properties:
        side:
          $ref: '#/components/schemas/ReconciliationSide'
        id:
          type: string
          description: The source's own identifier
        amount:
          type: string
          description: rust_decimal::Decimal as a string, always positive (e.g. "250.00")
          example: '250.00'
        currency:
          type: string
          example: USD
          description: May be "" for a core entry with no lines
        date:
          type: string
          format: date
        references:
          type: array
          description: >
            Vec<(String, String)> serialised as an array of 2-element [name, value] arrays, e.g.
            [["correlation_id","..."],["end_to_end_id","..."]].
          items:
            type: array
            minItems: 2
            maxItems: 2
            items:
              type: string
        provisional:
          type: boolean
    ReconciliationMatch:
      type: object
      required:
        - matched_on
        - engine
        - core
        - bank
      properties:
        matched_on:
          type: string
          description: The keys that matched, joined with ", then ", or "nothing"
        engine:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ReconciliationRecord'
        core:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ReconciliationRecord'
        bank:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ReconciliationRecord'
    ReconciliationExceptionRecords:
      type: object
      description: routes/reconciliation.rs `exception_payload`, stored as jsonb at run time.
      required:
        - matched
        - unmatched
      properties:
        matched:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ReconciliationMatch'
        unmatched:
          type: array
          items:
            $ref: '#/components/schemas/ReconciliationRecord'
    ReconciliationException:
      type: object
      description: routes/reconciliation.rs `exception_view`. One break.
      required:
        - run_id
        - seq
        - kind
        - sides
        - detail
        - delta
        - currency
        - records
        - resolution
        - resolved_by
        - resolved_at
        - resolved_note
        - approval_id
      properties:
        run_id:
          type: string
          format: uuid
        seq:
          type: integer
          format: int32
        kind:
          $ref: '#/components/schemas/ReconciliationExceptionKind'
        sides:
          type: array
          items:
            $ref: '#/components/schemas/ReconciliationSide'
        detail:
          type: string
        delta:
          type: string
          nullable: true
          description: Decimal as a string; null when a side is missing entirely
        currency:
          type: string
          nullable: true
        records:
          $ref: '#/components/schemas/ReconciliationExceptionRecords'
        resolution:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ReconciliationResolution'
        resolved_by:
          type: string
          nullable: true
        resolved_at:
          type: string
          format: date-time
          nullable: true
        resolved_note:
          type: string
          nullable: true
        approval_id:
          type: string
          format: uuid
          nullable: true
          description: Set with a null resolution means an adjusting entry is proposed and awaiting a second person
    ReconciliationRun:
      type: object
      description: >
        routes/reconciliation.rs `run_view`. `exceptions` is populated by POST /runs and GET /runs/{id}; GET
        /runs always sends it as an empty array. The stored `rules` are never returned.
      required:
        - id
        - account_ref
        - period
        - ran_at
        - ran_by
        - counts
        - truncated
        - exceptions
      properties:
        id:
          type: string
          format: uuid
        account_ref:
          type: string
        period:
          type: object
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date
            to:
              type: string
              format: date
        ran_at:
          type: string
          format: date-time
        ran_by:
          type: string
          description: The operator's token subject, or "reconciliation-schedule" for a scheduled run
        counts:
          type: object
          required:
            - engine
            - core
            - bank
            - clean
            - exceptions
          properties:
            engine:
              type: integer
              format: int32
            core:
              type: integer
              format: int32
            bank:
              type: integer
              format: int32
            clean:
              type: integer
              format: int32
            exceptions:
              type: integer
              format: int32
        truncated:
          type: boolean
          description: True when the core's ledger hit the 50-page x 200-entry read ceiling
        exceptions:
          type: array
          items:
            $ref: '#/components/schemas/ReconciliationException'
    ReconciliationRunList:
      type: object
      description: >-
        GET /backoffice/reconciliation/runs (not in this group's op list; included because the contract has no
        schema for it).
      required:
        - count
        - runs
      properties:
        count:
          type: integer
        runs:
          type: array
          items:
            $ref: '#/components/schemas/ReconciliationRun'
    ReconciliationQueue:
      type: object
      description: >-
        GET /backoffice/reconciliation/exceptions. Open breaks only (resolution IS NULL), newest run first
        then seq, at most 200.
      required:
        - count
        - timing
        - awaiting_approval
        - exceptions
      properties:
        count:
          type: integer
          description: Number of exceptions in this response (capped at 200), not a total
        timing:
          type: integer
          description: How many of them are kind=timing
        awaiting_approval:
          type: integer
          description: How many have an approval_id
        exceptions:
          type: array
          items:
            $ref: '#/components/schemas/ReconciliationException'
    ReconciliationRunRequest:
      type: object
      required:
        - account_ref
        - from
        - to
      properties:
        account_ref:
          type: string
        from:
          type: string
          format: date
        to:
          type: string
          format: date
          description: Must not be before `from`; (to - from) may be at most 62 days, else 400.
    ReconciliationResolveRequest:
      type: object
      required:
        - run_id
        - seq
        - resolution
        - note
      properties:
        run_id:
          type: string
          format: uuid
        seq:
          type: integer
          format: int32
        resolution:
          type: string
          enum:
            - explained
            - written_off
            - adjusting_entry
        note:
          type: string
          minLength: 10
          description: >
            At least 10 characters AFTER trimming (counted as Unicode chars), else 400. Checked before
            anything else, including before `resolution` is validated. Stored trimmed.
    ReconciliationResolved:
      type: object
      description: 200 of POST /backoffice/reconciliation/resolve (explained | written_off).
      required:
        - run_id
        - seq
        - resolution
      properties:
        run_id:
          type: string
          format: uuid
        seq:
          type: integer
          format: int32
        resolution:
          type: string
          enum:
            - explained
            - written_off
    ReconciliationAdjustmentProposed:
      type: object
      description: 202 of POST /backoffice/reconciliation/resolve (adjusting_entry).
      required:
        - approval_id
        - state
        - note
      properties:
        approval_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
        note:
          type: string
          description: 'Fixed text: "the break stays open until the entry is posted. Nothing has moved yet."'
    BackofficeOnboardingQueue:
      type: object
      description: >
        GET /backoffice/onboarding. Onboardings in the tenant with at least one requirement not `satisfied`
        and no ineligible_reason, oldest first, at most 200.
      required:
        - count
        - waiting_longest_since
        - onboardings
      properties:
        count:
          type: integer
          minimum: 0
        waiting_longest_since:
          type: string
          format: date-time
          nullable: true
          description: created_at of the oldest onboarding in the list; null when the queue is empty.
        onboardings:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeOnboardingView'
    BackofficeOnboardingView:
      type: object
      description: >
        routes/onboarding.rs `view()` (json!), non-ineligible branch. This is the SAME function GET
        /v1/onboarding, POST /v1/onboarding and POST /v1/onboarding/steps/{kind} return. The back office never
        sees the ineligible branch (the queue filters ineligible_reason IS NULL, and a verdict on an
        ineligible onboarding 404s because it has no requirement rows), so here `final` is always present and
        `state` is never `ineligible`. If the customer group defines an OnboardingView, reconcile to it (it
        must make `final` optional and add `ineligible`).
      required:
        - id
        - state
        - final
        - detail
        - next_step
        - requirements
      properties:
        identity_evidence:
          type: object
          nullable: true
          description: >
            Spec 034. Present in the back office's queue only: what the applicant's document said, as the
            platform read its machine-readable zone again, for the person deciding identity. No image is held;
            the number is its last four. `details` is null if the seal does not open.
          required:
            - document_type
            - details
            - checks_held
            - liveness_passed
            - mismatches
            - recorded_at
          properties:
            document_type:
              type: string
              enum:
                - passport
                - identity_card
            details:
              type: object
              nullable: true
              properties:
                document_code:
                  type: string
                issuing_state:
                  type: string
                number_last_four:
                  type: string
                surname:
                  type: string
                given_names:
                  type: string
                nationality:
                  type: string
                birth:
                  type: string
                  format: date
                sex:
                  type: string
                  nullable: true
                expiry:
                  type: string
                  format: date
                age_on_document:
                  type: integer
                declared_age:
                  type: integer
                declared_country:
                  type: string
            checks_held:
              type: boolean
              description: Every check digit held; a zone that did not is never kept
            liveness_passed:
              type: boolean
            mismatches:
              type: array
              items:
                type: string
                enum:
                  - birth
            recorded_at:
              type: string
              format: date-time
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - awaiting_customer
            - awaiting_us
            - ready
            - refused
        final:
          type: boolean
        detail:
          type: string
          description: >
            Prose. "N thing(s) left for you" | "nothing for you to do; we are checking N thing(s)" |
            "everything checks out" | "<kind>: <detail>;..." for refused.
        next_step:
          type: object
          nullable: true
          required:
            - kind
            - prompt
          properties:
            kind:
              $ref: '#/components/schemas/BackofficeOnboardingRequirementKind'
            prompt:
              type: string
        requirements:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeOnboardingRequirement'
    BackofficeOnboardingRequirement:
      type: object
      required:
        - kind
        - status
        - blocking
        - expires_on
        - lapsed
        - failure_detail
        - provider_ref
      properties:
        kind:
          $ref: '#/components/schemas/BackofficeOnboardingRequirementKind'
        status:
          type: string
          enum:
            - outstanding
            - submitted
            - satisfied
            - failed
          description: Raw column value; no DB CHECK, but the code only ever writes these four.
        blocking:
          type: boolean
        expires_on:
          type: string
          format: date
          nullable: true
        lapsed:
          type: boolean
          description: true when status is satisfied and today > expires_on.
        failure_detail:
          type: string
          nullable: true
        provider_ref:
          type: string
          nullable: true
    BackofficeOnboardingRequirementKind:
      type: string
      enum:
        - identity
        - address
        - screening
        - tax_residency
        - source_of_funds
        - terms_accepted
    BackofficeLoanQueue:
      type: object
      description: >
        GET /backoffice/loans. Applications in state submitted, referred or approved, oldest first, at most
        200.
      required:
        - count
        - waiting_longest_since
        - applications
      properties:
        count:
          type: integer
          minimum: 0
        waiting_longest_since:
          type: string
          format: date-time
          nullable: true
          description: created_at of the oldest application listed; null when empty.
        applications:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeLoanApplicationView'
    BackofficeLoanApplicationView:
      description: >
        lending.rs view(): the application whole. `decision` keeps any `operator_note`, and
        `operator_decision` names who proposed and who approved.
      allOf:
        - $ref: '#/components/schemas/LoanApplicationView'
        - type: object
          properties:
            operator_decision:
              allOf:
                - $ref: '#/components/schemas/LoanOperatorDecision'
              nullable: true
    BackofficeLoanProposal:
      type: object
      description: 202 body of POST /backoffice/loans/decisions.
      required:
        - approval_id
        - state
        - note
      properties:
        approval_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
        note:
          type: string
    BackofficeLoanDisbursementProposal:
      type: object
      description: 202 body of POST /backoffice/loans/disbursements.
      required:
        - approval_id
        - state
        - money_moved
        - note
      properties:
        approval_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
        money_moved:
          type: boolean
          enum:
            - false
          description: Always false at proposal time. Whether approval will move money is only in `note`.
        note:
          type: string
    BackofficeArrearsQueue:
      type: object
      description: >
        GET /backoffice/loans/arrears. Drawn loans (up to 500 scanned, by disbursed_on) whose derived position
        has arrears_minor > 0, sorted by days_past_due descending.
      required:
        - count
        - loans
      properties:
        count:
          type: integer
          minimum: 0
        loans:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeArrearsLoan'
    BackofficeArrearsLoan:
      type: object
      required:
        - id
        - customer
        - currency
        - arrears_minor
        - days_past_due
      properties:
        id:
          type: string
          format: uuid
          description: The loan application id.
        customer:
          type: string
          description: The borrower's OIDC subject.
        currency:
          type: string
        arrears_minor:
          type: integer
          format: int64
          minimum: 1
        days_past_due:
          type: integer
          format: int64
          minimum: 0
    BackofficeSupportQueue:
      type: object
      description: >
        GET /backoffice/support. Scans the oldest 200 unresolved conversations; `conversations` holds those
        whose derived turn is `us`, `calls_owed` those whose callback is still owed. One conversation can
        appear in both lists.
      required:
        - waiting_on_us
        - oldest_since
        - conversations
        - calls_owed
      properties:
        waiting_on_us:
          type: integer
          minimum: 0
          description: Length of `conversations` (an integer count, not a boolean).
        oldest_since:
          type: string
          format: date-time
          nullable: true
          description: >
            created_at of the oldest UNRESOLVED conversation scanned, whether or not it is waiting on us; null
            when there are none.
        conversations:
          type: array
          items:
            $ref: '#/components/schemas/SupportConversation'
        calls_owed:
          type: array
          items:
            $ref: '#/components/schemas/SupportConversation'
    BackofficeFraudQueue:
      type: object
      description: >
        GET /backoffice/fraud. Assessments with verdict IS NULL and outcome in (allow_and_review, hold),
        oldest decided_at first, at most 200.
      required:
        - count
        - oldest
        - health
        - assessments
      properties:
        count:
          type: integer
          minimum: 0
        oldest:
          type: string
          format: date-time
          nullable: true
          description: decided_at of the oldest listed assessment; null when empty.
        health:
          $ref: '#/components/schemas/BackofficeFraudHealth'
        assessments:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeFraudAssessment'
    BackofficeFraudHealth:
      type: object
      required:
        - assessed
        - decided_without_an_answer
        - reviewed
        - fraudulent
        - false_positive_rate
      properties:
        assessed:
          type: integer
          format: int64
          minimum: 0
        decided_without_an_answer:
          type: integer
          format: int64
          minimum: 0
        reviewed:
          type: integer
          format: int64
          minimum: 0
        fraudulent:
          type: integer
          format: int64
          minimum: 0
        false_positive_rate:
          type: number
          format: double
          minimum: 0
          maximum: 1
          nullable: true
          description: (reviewed - fraudulent) / reviewed; null (key present) when nothing has been reviewed.
    BackofficeFraudAssessment:
      type: object
      description: >
        routes/fraud.rs `view(&StoredAssessment)`. Also the 200 body of GET /backoffice/fraud/{id} (not in
        this group's list, but the same function). In the queue, outcome is only allow_and_review|hold and
        every review field is null.
      required:
        - id
        - correlation_id
        - action
        - subject_ref
        - amount_minor
        - currency
        - score
        - band
        - answered
        - outcome
        - reasons
        - decided_at
        - can_still_be_stopped
        - verdict
        - consequence
        - reviewed_by
        - reviewed_at
        - review_note
      properties:
        id:
          type: string
          format: uuid
        correlation_id:
          type: string
          format: uuid
        action:
          type: string
          enum:
            - card_authorization
            - transfer
            - payout
            - refund
          description: fraud::Action labels; the column has no CHECK.
        subject_ref:
          type: string
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
        score:
          type: number
          format: double
          minimum: 0
          maximum: 1
          nullable: true
        band:
          type: string
          nullable: true
          enum:
            - allow
            - review
            - block
            - null
        answered:
          type: boolean
        outcome:
          type: string
          enum:
            - allow
            - allow_and_review
            - hold
            - block
        reasons:
          type: array
          items:
            type: object
            required:
              - code
              - detail
            properties:
              code:
                type: string
              detail:
                type: string
        decided_at:
          type: string
          format: date-time
        can_still_be_stopped:
          type: boolean
          description: outcome == hold
        verdict:
          type: string
          nullable: true
          enum:
            - legitimate
            - fraudulent
            - null
        consequence:
          type: string
          nullable: true
          enum:
            - release
            - refuse
            - nothing
            - record_loss
            - null
        reviewed_by:
          type: string
          nullable: true
        reviewed_at:
          type: string
          format: date-time
          nullable: true
        review_note:
          type: string
          nullable: true
    BackofficeFraudVerdictResult:
      type: object
      description: 200 body of POST /backoffice/fraud/{id}.
      required:
        - id
        - verdict
        - consequence
        - note
      properties:
        id:
          type: string
          format: uuid
        verdict:
          type: string
          enum:
            - legitimate
            - fraudulent
        consequence:
          type: string
          enum:
            - release
            - refuse
            - nothing
            - record_loss
          description: >
            hold+legitimate=release, hold+fraudulent=refuse, allow_and_review+fraudulent=record_loss,
            allow_and_review+legitimate=nothing.
        note:
          type: string
          description: One fixed sentence per consequence (prose, not an enum to match on).
    BackofficeFundingQueue:
      type: object
      description: >
        GET /backoffice/funding. Fundings in stage awaiting_deposit, confirming or converting, oldest first,
        at most 200.
      required:
        - count
        - oldest_since
        - funding
      properties:
        count:
          type: integer
          minimum: 0
        oldest_since:
          type: string
          format: date-time
          nullable: true
          description: created_at of the oldest listed funding; null when empty.
        funding:
          type: array
          description: Key is singular `funding`, not `fundings`.
          items:
            $ref: '#/components/schemas/FundingView'
    AccountView:
      type: object
      description: routes/accounts.rs `AccountView`. Figures relayed from the core.
      required:
        - id
        - name
        - kind
        - currency
        - balance_minor
        - provenance
      properties:
        id:
          type: string
          description: The core's account id (e.g. "4"); not a uuid in general
        name:
          type: string
          description: Never empty
        kind:
          type: string
          description: Free-form, the core's word (core adapter writes "savings")
        currency:
          type: string
        balance_minor:
          type: integer
          format: int64
          nullable: true
          description: >
            Signed minor units. `null` (key present) where the core could not answer, or the balance is not
            exactly representable in minor units. Never absent.
        provenance:
          type: string
          enum:
            - live
        iban:
          type: string
          description: A euro account the customer opened. Absent for every other account
        bic:
          type: string
          description: Its bank's BIC, beside the IBAN
        account_number:
          type: string
          description: |
            A dollar account's US account number, once Column issued it. Absent for every other account
        routing_number:
          type: string
          description: Its routing number, beside the account number
        business:
          type: string
          description: >
            The company a business account belongs to, which the caller acts for (specs 039, 041). Absent for
            the caller's own accounts
        business_role:
          type: string
          enum:
            - admin
            - payer
            - approver
            - viewer
          description: What the caller does for that company; absent likewise
    ClosingBalanceList:
      type: object
      description: routes/accounts.rs `closing_balances`
      required:
        - days
      properties:
        days:
          type: array
          items:
            type: object
            required:
              - day
              - closing_minor
              - currency
            properties:
              day:
                type: string
                format: date
              closing_minor:
                type: integer
                format: int64
                nullable: true
              currency:
                type: string
    EuroAccountOpenBody:
      type: object
      description: routes/named_accounts.rs `OpenBody`
      required:
        - first_name
        - last_name
        - email
        - phone
        - address
        - method
        - authenticated_at
        - evidence_ref
        - signed_amount
        - signed_currency
        - signed_payee
      properties:
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        email:
          type: string
          maxLength: 254
        phone:
          type: string
          description: 6 to 15 digits, with spaces, +, - and brackets
        address:
          type: object
          required:
            - line_1
            - city
            - country_code
          properties:
            line_1:
              type: string
              maxLength: 70
            line_2:
              type: string
              nullable: true
              maxLength: 70
            city:
              type: string
              maxLength: 70
            state:
              type: string
              nullable: true
            postal_code:
              type: string
              nullable: true
              maxLength: 16
            country_code:
              type: string
              pattern: ^[A-Za-z]{2}$
              description: ISO 3166-1 alpha-2
        method:
          type: string
          enum:
            - passkey
            - device_biometric
            - device_key
            - app_pin
            - sms_otp
            - email_otp
        authenticated_at:
          type: string
          format: date-time
        evidence_ref:
          type: string
          minLength: 1
        signed_amount:
          type: string
          description: '"0.00"'
        signed_currency:
          type: string
          description: '"EUR"'
        signed_payee:
          type: string
          description: '"A new EUR account"'
        signature:
          type: string
          description: >-
            With method device_key, the device key's signature over oxabank-confirm/1, the kind, the id, the
            amount, the currency and the payee, one per line; base64, DER or r and s; the kind is euro_account
            and the id -. With method passkey, the WebAuthn assertion as four base64url parts joined by dots:
            the credential id, the authenticator data, the client data JSON and the signature; its challenge
            is SHA-256 of that same message.
    FxWire:
      type: object
      description: routes/fx_wires.rs `view`, the platform’s specification
      required:
        - id
        - state
        - said
        - from_account
        - payee
        - sell_minor
        - sell_currency
        - buy_minor
        - buy_currency
        - rate
        - spread_bps
        - spread_minor
        - rate_source
        - rate_as_of
        - reference
        - authorize
        - money_moved
        - return_reason
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - awaiting_authorization
            - refused
            - posting
            - sent
            - settled
            - returned
            - refunded
        said:
          type: string
        from_account:
          type: string
        payee:
          type: object
          required:
            - id
            - display_name
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type: string
        sell_minor:
          type: integer
        sell_currency:
          type: string
        buy_minor:
          type: integer
        buy_currency:
          type: string
        rate:
          type: string
          description: What a dollar buys
          after the spread: null
        spread_bps:
          type: integer
        spread_minor:
          type: integer
          description: The spread as money
          in the payee's currency: null
        rate_source:
          type: string
        rate_as_of:
          type: string
          format: date
        reference:
          type: string
          nullable: true
        authorize:
          type: object
          required:
            - amount
            - currency
            - payee
            - expires_in_minutes
          properties:
            amount:
              type: string
            currency:
              type: string
            payee:
              type: string
            expires_in_minutes:
              type: integer
        money_moved:
          type: boolean
        return_reason:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    LinkedAccount:
      type: object
      description: routes/pulls.rs `link_view`, the platform’s specification. Never the whole account number.
      required:
        - id
        - holder_name
        - routing_number
        - last4
        - account_type
        - label
        - verified
        - created_at
      properties:
        id:
          type: string
          format: uuid
        holder_name:
          type: string
        routing_number:
          type: string
        last4:
          type: string
        account_type:
          type: string
          enum:
            - checking
            - savings
        label:
          type: string
          description: From <holder> ••<last4>, as the customer signs it
        verified:
          type: boolean
          description: 'Spec 054: the customer said its two small deposits'
        verification:
          description: The latest verification, in the list only
          nullable: true
          allOf:
            - $ref: '#/components/schemas/MicroDeposit'
        created_at:
          type: string
          format: date-time
    MicroDeposit:
      type: object
      description: routes/micro_deposits.rs `view`, the platform’s specification. Never the amounts.
      required:
        - id
        - linked_account_id
        - state
        - said
        - tries_left
        - authorize
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        linked_account_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - awaiting_authorization
            - refused
            - posting
            - sent
            - verified
            - failed
            - returned
        said:
          type: string
          description: Where it is
          in the platform's words: null
        tries_left:
          type: integer
          nullable: true
          description: While sent
        authorize:
          type: object
          nullable: true
          required:
            - amount
            - currency
            - payee
            - expires_in_minutes
          properties:
            amount:
              type: string
            currency:
              type: string
            payee:
              type: string
            expires_in_minutes:
              type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreditLine:
      type: object
      description: >-
        routes/credit_lines.rs `view`, the platform’s specification. The figures are the core's, or null while
        it is not open or not answering.
      required:
        - id
        - state
        - said
        - currency
        - asked_minor
        - limit_minor
        - drawn_minor
        - available_minor
        - payoff_minor
        - overdue_minor
        - next_due
        - days_past_due
        - closed_by
        - closed_at
        - annual_rate
        - term_months
        - account_id
        - provenance
        - movements
        - created_at
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
            - open
            - declined
            - refused
            - closed
            - charged_off
          description: 'Spec 059: closed and charged_off are never drawn; what they owe is still paid back'
        said:
          type: string
          description: Where it is
          in the platform's words: null
        currency:
          type: string
        asked_minor:
          type: integer
        limit_minor:
          type: integer
        drawn_minor:
          type: integer
          nullable: true
        available_minor:
          type: integer
          nullable: true
        payoff_minor:
          type: integer
          nullable: true
          description: What paying it all off today takes
        overdue_minor:
          type: integer
          nullable: true
        next_due:
          type: object
          nullable: true
          required:
            - 'on'
            - amount_minor
          properties:
            'on':
              type: string
              format: date
            amount_minor:
              type: integer
        days_past_due:
          type: integer
          nullable: true
          description: >-
            Spec 059: the bank's day less the oldest unpaid instalment's day once that day has passed, else 0;
            read from the schedule, not the core's overdue figure; null without the core's figures
        closed_by:
          type: string
          nullable: true
          enum:
            - customer
            - bank
            - null
        closed_at:
          type: string
          format: date-time
          nullable: true
        annual_rate:
          type: string
          description: A year's rate, "24" for 24%
        term_months:
          type: integer
        account_id:
          type: string
        provenance:
          type: string
          enum:
            - live
            - platform
        movements:
          type: array
          items:
            $ref: '#/components/schemas/LineMovement'
        agreement:
          description: >-
            Spec 060: the line's agreement once asked for, signed or not; a line is drawn only once it is
            signed
          nullable: true
          allOf:
            - $ref: '#/components/schemas/LineAgreement'
        autopay:
          description: 'Spec 058: the line''s autopay, asked for, on or the latest stopped; open and closed lines only'
          nullable: true
          allOf:
            - $ref: '#/components/schemas/LineAutopay'
        created_at:
          type: string
          format: date-time
    LineAutopay:
      type: object
      description: routes/line_autopay.rs `view`, the platform’s specification
      required:
        - id
        - state
        - said
        - from_account
        - cap_minor
        - authorize
        - runs
        - created_at
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - awaiting_authorization
            - active
            - stopped
        said:
          type: string
        from_account:
          type: string
        cap_minor:
          type: integer
          description: The most taken on a due day
        authorize:
          type: object
          nullable: true
          required:
            - amount
            - currency
            - payee
            - expires_in_minutes
          properties:
            amount:
              type: string
            currency:
              type: string
            payee:
              type: string
            expires_in_minutes:
              type: integer
        runs:
          type: array
          items:
            type: object
            required:
              - due_on
              - amount_minor
              - state
              - said
            properties:
              due_on:
                type: string
                format: date
              amount_minor:
                type: integer
              state:
                type: string
                enum:
                  - asked
                  - done
                  - refused
              said:
                type: string
        created_at:
          type: string
          format: date-time
    LineMovement:
      type: object
      description: routes/credit_lines.rs `movement_view`, the platform’s specification
      required:
        - id
        - kind
        - amount_minor
        - currency
        - account_id
        - state
        - said
        - authorize
        - money_moved
        - auth_method
        - created_at
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - draw
            - repayment
        amount_minor:
          type: integer
        currency:
          type: string
        account_id:
          type: string
        state:
          type: string
          enum:
            - awaiting_authorization
            - refused
            - done
        said:
          type: string
        authorize:
          type: object
          nullable: true
          required:
            - amount
            - currency
            - payee
            - expires_in_minutes
          properties:
            amount:
              type: string
            currency:
              type: string
            payee:
              type: string
            expires_in_minutes:
              type: integer
        money_moved:
          type: boolean
        auth_method:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    LineAgreement:
      type: object
      description: routes/documents.rs `agreement_view`, the platform’s specification
      required:
        - id
        - state
        - sha256
        - signed_at
        - auth_method
        - authorize
        - pdf
        - signed_pdf
        - created_at
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - unsigned
            - signed
        sha256:
          type: string
          description: The SHA-256 of the PDF as read and signed
        signed_at:
          type: string
          format: date-time
          nullable: true
        auth_method:
          type: string
          nullable: true
        authorize:
          type: object
          nullable: true
          description: While unsigned, what the customer confirms
          required:
            - amount
            - currency
            - payee
          properties:
            amount:
              type: string
            currency:
              type: string
            payee:
              type: string
        pdf:
          type: string
          description: Where the PDF as read is
        signed_pdf:
          type: string
          nullable: true
          description: Where the signed copy is
          once signed: null
        created_at:
          type: string
          format: date-time
    LineChange:
      type: object
      description: routes/credit_lines.rs `change_view`, the platform’s specification. Where it stands is its approval's.
      required:
        - approval_id
        - kind
        - from_minor
        - to_minor
        - reason
        - proposed_by
        - state
        - decided_by
        - created_at
      properties:
        approval_id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - limit
            - close
            - charge_off
        from_minor:
          type: integer
          description: The limit when it was proposed
        to_minor:
          type: integer
          nullable: true
          description: A new limit's
        reason:
          type: string
        proposed_by:
          type: string
        state:
          type: string
          enum:
            - proposed
            - approved
            - rejected
            - executed
            - failed
        decided_by:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    BackofficeCreditLine:
      description: routes/credit_lines.rs `backoffice_list`, the platform’s specification
      allOf:
        - $ref: '#/components/schemas/CreditLine'
        - type: object
          required:
            - customer
            - closed_reason
            - autopay_state
            - waiting
            - changes
            - charge_off_refusal
          properties:
            customer:
              type: string
              description: The customer's subject
            closed_reason:
              type: string
              nullable: true
            autopay_state:
              type: string
              nullable: true
              enum:
                - awaiting_authorization
                - active
                - stopped
                - null
            waiting:
              nullable: true
              description: The one proposal waiting on the line
              allOf:
                - $ref: '#/components/schemas/LineChange'
            changes:
              type: array
              items:
                $ref: '#/components/schemas/LineChange'
            charge_off_refusal:
              type: string
              nullable: true
              description: Why it cannot be charged off now
              or null when it can: null
    BackofficeCreditLines:
      type: object
      required:
        - lines
        - today
        - charge_off_days
      properties:
        lines:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeCreditLine'
        today:
          type: string
          format: date
          description: The bank's day the lateness is measured on
        charge_off_days:
          type: integer
          description: PLATFORM_CHARGE_OFF_DAYS, 180 by default
    RecurringPull:
      type: object
      description: routes/recurring_pulls.rs `view`, the platform’s specification
      required:
        - id
        - state
        - said
        - linked_account_id
        - from
        - to_account
        - amount_minor
        - currency
        - every
        - first_on
        - next_on
        - taken
        - mandate
        - authorize
        - auth_method
        - created_at
        - stopped_at
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - awaiting_authorization
            - refused
            - active
            - stopped
        said:
          type: string
          description: Where it is
          in the platform's words: null
        linked_account_id:
          type: string
          format: uuid
        from:
          type: string
          nullable: true
        to_account:
          type: string
        amount_minor:
          type: integer
        currency:
          type: string
        every:
          type: string
          enum:
            - week
            - month
        first_on:
          type: string
          format: date
        next_on:
          type: string
          format: date
          nullable: true
          description: While active
        taken:
          type: integer
          description: How many due days have been started
        mandate:
          type: string
          description: The recurring authorisation the customer agrees to
        authorize:
          type: object
          nullable: true
          required:
            - amount
            - currency
            - payee
            - expires_in_minutes
          properties:
            amount:
              type: string
            currency:
              type: string
            payee:
              type: string
            expires_in_minutes:
              type: integer
        auth_method:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        stopped_at:
          type: string
          format: date-time
          nullable: true
    Pull:
      type: object
      description: routes/pulls.rs `pull_view`, the platform’s specification
      required:
        - id
        - state
        - said
        - linked_account_id
        - from
        - to_account
        - amount_minor
        - currency
        - mandate
        - authorize
        - auth_method
        - money_moved
        - return_reason
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - awaiting_authorization
            - refused
            - sent
            - settled
            - credited
            - returned
            - reversed
        said:
          type: string
          description: Where it is
          in the platform's words: null
        linked_account_id:
          type: string
          format: uuid
        from:
          type: string
          nullable: true
        to_account:
          type: string
        amount_minor:
          type: integer
        currency:
          type: string
        mandate:
          type: string
          description: The authorisation text the customer agrees to
        authorize:
          type: object
          nullable: true
          required:
            - amount
            - currency
            - payee
            - expires_in_minutes
          properties:
            amount:
              type: string
            currency:
              type: string
            payee:
              type: string
            expires_in_minutes:
              type: integer
        auth_method:
          type: string
          nullable: true
        money_moved:
          type: boolean
        return_reason:
          type: string
          nullable: true
        recurring_id:
          type: string
          format: uuid
          nullable: true
          description: 'Spec 055: the standing authorisation it was started under'
        due_on:
          type: string
          format: date
          nullable: true
          description: The day it was due
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Passkey:
      type: object
      description: routes/passkeys.rs `view`, the platform’s specification
      required:
        - id
        - name
        - credential_id
        - registered_at
        - last_used_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        credential_id:
          type: string
          description: base64url
          as the browser's allowCredentials takes it: null
        registered_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
          nullable: true
    EuroAccountView:
      type: object
      description: |
        routes/named_accounts.rs `view`: a euro account and its IBAN, or a dollar account's US account details
      required:
        - id
        - currency
        - iban
        - bic
        - account_number
        - routing_number
        - state
        - failure
      properties:
        id:
          type: string
          nullable: true
          description: The core's account id
          once opened: null
        currency:
          type: string
          enum:
            - EUR
            - USD
        iban:
          type: string
          nullable: true
        bic:
          type: string
          nullable: true
        account_number:
          type: string
          nullable: true
        routing_number:
          type: string
          nullable: true
        state:
          type: string
          enum:
            - open
            - opening
        failure:
          type: string
          nullable: true
          description: Why the last attempt stopped
    StatementEntryView:
      type: object
      description: routes/accounts.rs `EntryView`.
      required:
        - id
        - posted_on
        - amount_minor
        - currency
        - reference
        - provenance
      properties:
        id:
          type: string
          description: The core's id for the movement
        posted_on:
          type: string
          format: date
          description: The day the core booked it. The core records no time, so none is given
        amount_minor:
          type: integer
          format: int64
          description: Signed from this account's point of view; negative is money out
        currency:
          type: string
          description: Always the account's currency
        reference:
          type: string
          nullable: true
          description: >
            The platform's reference the movement was made under — a transfer's or an approval's id, sometimes
            prefixed (`transfer-release:`). Null for a movement the platform did not make. The core holds no
            correlation id, so none is claimed
        counterparty:
          type: string
          nullable: true
          description: >
            Spec 076. Who the money went to or came from, where the platform made the movement: the payee of a
            payment out (or between the caller's own accounts), the payer of one in by their name with the
            bank. Null is "not known here", never a name made up
        provenance:
          type: string
          enum:
            - live
    CardLimitsView:
      type: object
      description: routes/cards.rs `LimitsView`. Minor units; `null` means no limit.
      required:
        - per_transaction
        - daily
        - monthly
        - remaining_today
        - remaining_this_month
      properties:
        per_transaction:
          type: integer
          format: int64
          nullable: true
          minimum: 0
        daily:
          type: integer
          format: int64
          nullable: true
          minimum: 0
        monthly:
          type: integer
          format: int64
          nullable: true
          minimum: 0
        remaining_today:
          type: integer
          format: int64
          nullable: true
          minimum: 0
          description: null exactly when `daily` is null; holds and settled spend included, floored at 0
        remaining_this_month:
          type: integer
          format: int64
          nullable: true
          minimum: 0
          description: null exactly when `monthly` is null
    CardView:
      type: object
      description: routes/cards.rs `CardView0` (the module's own `CardView` is not what is served).
      required:
        - id
        - status
        - status_detail
        - last_four
        - network
        - expires_on
        - residency
        - currency
        - actions
        - limits
        - provenance
        - form
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          description: Derived for today; `expired` is never stored, only derived from `expires_on`.
          enum:
            - requested
            - issued
            - active
            - frozen
            - expired
            - cancelled
        status_detail:
          type: string
          description: CardStatus::why_declined(), one fixed sentence per status.
          enum:
            - this card has not been produced yet
            - this card has not been activated yet
            - this card is active
            - this card is frozen
            - this card has expired
            - this card has been cancelled
        last_four:
          type: string
          pattern: ^[0-9]{4}$
        network:
          type: string
          description: Issuer's word, e.g. "visa"
        expires_on:
          type: string
          format: date
          description: Last good day
        residency:
          type: string
          enum:
            - delegated
            - vaulted
        currency:
          type: string
        actions:
          type: array
          description: >-
            Legal actions now, in stable order (issued: activate,cancel; active: freeze,cancel; frozen:
            unfreeze,cancel; expired: cancel; requested/cancelled: none)
          items:
            $ref: '#/components/schemas/CardAction'
        limits:
          $ref: '#/components/schemas/CardLimitsView'
        provenance:
          type: string
          enum:
            - live
            - synthetic
        business_id:
          type: string
          format: uuid
          description: |
            The company whose account it spends from, for a company card; absent on a customer's own
        form:
          type: string
          enum:
            - virtual
            - physical
          description: Spec 076. As it was issued; a card issued before is virtual
    CardAction:
      type: string
      enum:
        - activate
        - freeze
        - unfreeze
        - cancel
    CardLimitsBody:
      type: object
      description: >
        routes/cards.rs `LimitsBody` (double Option). A key that is absent leaves that limit unchanged; a key
        sent as `null` removes the limit, which is refused (409) as a raise whenever a limit existed. Negative
        values are 400.
      properties:
        per_transaction:
          type: integer
          format: int64
          nullable: true
          minimum: 0
        daily:
          type: integer
          format: int64
          nullable: true
          minimum: 0
        monthly:
          type: integer
          format: int64
          nullable: true
          minimum: 0
    PayeeView:
      type: object
      description: routes/transfers.rs `payee_view` (json!).
      required:
        - id
        - display_name
        - account_ref
        - destination
        - first_payment
      properties:
        id:
          type: string
          format: uuid
        display_name:
          type: string
        account_ref:
          type: string
        destination:
          type: string
          enum:
            - core
            - spei
            - ach
            - swift
            - sepa
            - unknown
          description: >
            core = digits only, shorter than 18; spei = a valid CLABE; ach = a US bank account, written
            aba:<routing>:<account>:<checking|savings>; swift = an account abroad, written swift:<BIC>:<IBAN
            or account>, with an address; sepa = a valid IBAN of a SEPA country given alone, paid in euros
            from the customer's euro account; unknown = anything else
        first_payment:
          type: boolean
          description: True while no transfer to them has been authorized
        address:
          type: object
          nullable: true
          description: The account holder's address, for a payee abroad; null for every other
          required:
            - line_1
            - city
            - country_code
          properties:
            line_1:
              type: string
              maxLength: 70
            line_2:
              type: string
              nullable: true
            city:
              type: string
              maxLength: 70
            state:
              type: string
              nullable: true
            postal_code:
              type: string
              nullable: true
            country_code:
              type: string
              pattern: ^[A-Z]{2}$
              description: ISO 3166-1 alpha-2
    PayeeList:
      type: object
      required:
        - payees
      properties:
        payees:
          type: array
          description: Ordered by display_name
          items:
            $ref: '#/components/schemas/PayeeView'
    PayeeNewBody:
      type: object
      description: routes/transfers.rs `NewPayeeBody`.
      required:
        - display_name
        - account_ref
      properties:
        display_name:
          type: string
          minLength: 1
          pattern: \S
          description: Refused if blank after trim
        account_ref:
          type: string
          minLength: 1
          pattern: \S
          description: >
            Refused if blank after trim. If it is exactly 18 digits (whitespace ignored) it must be a valid
            CLABE (check digit and a SPEI bank code) or it is 400; any other shape is kept and classified
            `core` or `unknown`.
        address:
          type: object
          description: Required for a swift:<BIC>:<account> reference and refused on any other
          required:
            - line_1
            - city
            - country_code
          properties:
            line_1:
              type: string
              maxLength: 70
            line_2:
              type: string
              nullable: true
            city:
              type: string
              maxLength: 70
            state:
              type: string
              nullable: true
            postal_code:
              type: string
              nullable: true
            country_code:
              type: string
              pattern: ^[A-Z]{2}$
              description: ISO 3166-1 alpha-2
    TransferPayeeRef:
      type: object
      required:
        - id
        - display_name
      properties:
        id:
          type: string
          format: uuid
        display_name:
          type: string
          description: The payee's name as stored on the intent
    TransferAuthorizeBinding:
      type: object
      description: Exactly what must be displayed and signed; the confirm's signed_* fields must equal these.
      required:
        - amount
        - currency
        - payee
        - expires_in_minutes
      properties:
        amount:
          type: string
          description: Decimal major units with the currency's exponent, e.g. "400.00"
        currency:
          type: string
        payee:
          type: string
          description: The payee display name
        expires_in_minutes:
          type: integer
          enum:
            - 10
          description: Constant INTENT_LIFETIME_MINUTES
    TransferWarning:
      type: object
      required:
        - code
        - detail
      properties:
        code:
          type: string
          enum:
            - first_payment
        detail:
          type: string
    TransferIntentView:
      type: object
      description: routes/transfers.rs `intent_view` (json!). Every key is always present.
      required:
        - id
        - state
        - from_account
        - payee
        - amount_minor
        - currency
        - reference
        - created_at
        - authorize
        - warnings
        - authorized_at
        - auth_method
        - refused_reason
        - core_ref
        - destination
        - money_moved
        - rail_ref
        - instant
        - goes_out_on
        - after_cutoff
        - cutoff
        - holiday
        - business_id
        - cosigned
      properties:
        business_id:
          type: string
          format: uuid
          nullable: true
          description: The company it is paid from, for a payment from a business account
        cosigned:
          type: boolean
          description: >
            A second person of the company signed it; its signatory's identity is the company's record, not in
            this view
        cosign_reason:
          type: string
          nullable: true
          enum:
            - threshold
            - per_payment
            - per_day
            - null
          description: >
            Why it waits, or waited, for a second signature: the company's threshold, or the payer's own limit
            per payment or for the day
        instant:
          type: boolean
          description: >
            Spec 050. True for a euro payment to an IBAN up to €100,000, which the FX provider sends as SEPA
            Instant — in seconds, at any hour — where the payee's bank takes instant payments; where it does
            not, the payment goes as SEPA by the cut-off and calendar the other fields say
        goes_out_on:
          type: string
          format: date
          nullable: true
          description: >
            The day the rail takes it, by the rail's cut-off in its own zone: that day before the cut-off on
            Monday to Friday, else the next of those. `null` within OxaBank or on a rail with no cut-off
            configured
        after_cutoff:
          type: boolean
          nullable: true
          description: True when it goes out later than the day it was authorised (or prepared)
        cutoff:
          type: string
          nullable: true
          description: The rail's cut-off as the customer is told it, "16:30", in the rail's zone
        holiday:
          type: object
          nullable: true
          description: >
            The first day the rail's calendar keeps that held it over, and its name. `null` where none did,
            within OxaBank, or on a rail with no cut-off
          required:
            - day
            - name
          properties:
            day:
              type: string
              format: date
            name:
              type: string
              example: Thanksgiving Day
        id:
          type: string
          format: uuid
        state:
          type: string
          description: >
            DB check constraint transfer_intents_state_check. `awaiting_cosignature`: a company's payment at
            or above its threshold, or above one of its payer's limits, confirmed by its payer and waiting for
            a second person of the company
          enum:
            - awaiting_authorization
            - awaiting_cosignature
            - authorized
            - held
            - refused
            - expired
            - posting
            - posted
            - sent
            - settled
            - returned
        from_account:
          type: string
        payee:
          $ref: '#/components/schemas/TransferPayeeRef'
        amount_minor:
          type: integer
          format: int64
          minimum: 1
        currency:
          type: string
        reference:
          type: string
          nullable: true
          maxLength: 35
        created_at:
          type: string
          format: date-time
        authorize:
          $ref: '#/components/schemas/TransferAuthorizeBinding'
        warnings:
          type: array
          description: Populated only on the POST /transfers response. GET /transfers and confirm always send [].
          items:
            $ref: '#/components/schemas/TransferWarning'
        authorized_at:
          type: string
          format: date-time
          nullable: true
        auth_method:
          type: string
          nullable: true
          enum:
            - passkey
            - device_biometric
            - device_key
            - app_pin
            - sms_otp
            - email_otp
        refused_reason:
          type: string
          nullable: true
        core_ref:
          type: string
          nullable: true
        destination:
          type: string
          enum:
            - core
            - spei
            - ach
            - swift
            - sepa
            - unknown
        money_moved:
          type: boolean
          description: true iff state is posted, sent or settled
        rail_ref:
          type: string
          nullable: true
    TransferList:
      type: object
      required:
        - transfers
      properties:
        transfers:
          type: array
          description: At most 50
          items:
            $ref: '#/components/schemas/TransferIntentView'
    TransferNewBody:
      type: object
      description: routes/transfers.rs `NewTransferBody` plus the checks in modules/transfers validate().
      required:
        - from_account
        - payee_id
        - amount_minor
        - currency
      properties:
        from_account:
          type: string
          description: One of the caller's core account ids; otherwise 404
        payee_id:
          type: string
          format: uuid
          description: One of the caller's payees; otherwise 404
        amount_minor:
          type: integer
          format: int64
          minimum: 1
        currency:
          type: string
          pattern: ^[A-Z]{3}$
          description: Must equal the from_account's currency (400 otherwise)
        reference:
          type: string
          nullable: true
          maxLength: 35
          description: Counted in characters
    TransferConfirmBody:
      type: object
      description: >
        routes/transfers.rs `ConfirmBody`; also the body of POST /exchange/quotes/{id}/confirm (same Rust
        type). The five methods all parse; sms_otp and email_otp are then refused 400 as one factor.
        authenticated_at must be within 300s in the past (future or older is 400). evidence_ref must be
        non-blank. signed_* must equal the stored `authorize` object.
      required:
        - method
        - authenticated_at
        - evidence_ref
        - signed_amount
        - signed_currency
        - signed_payee
      properties:
        method:
          type: string
          enum:
            - passkey
            - device_biometric
            - device_key
            - app_pin
            - sms_otp
            - email_otp
        authenticated_at:
          type: string
          format: date-time
        evidence_ref:
          type: string
          minLength: 1
          pattern: \S
        signed_amount:
          type: string
        signed_currency:
          type: string
        signed_payee:
          type: string
        signature:
          type: string
          description: >-
            With method device_key, the device key's signature over oxabank-confirm/1, the kind, the id, the
            amount, the currency and the payee, one per line; base64, DER or r and s. The kind is transfer,
            cosign or exchange, and the id the transfer's or the quote's. With method passkey, the WebAuthn
            assertion as four base64url parts joined by dots: the credential id, the authenticator data, the
            client data JSON and the signature; its challenge is SHA-256 of that same message. Without
            PLATFORM_CONFIRMATION=device_key the other methods are taken as before; with it, only device_key
            and passkey.
    ExchangeAuthorizeBinding:
      type: object
      description: What the customer signs. No expires_in_minutes here, unlike a transfer.
      required:
        - amount
        - currency
        - payee
      properties:
        amount:
          type: string
          description: sell_minor as decimal major units, e.g. "1000.00"
        currency:
          type: string
          description: sell_currency
        payee:
          type: string
          description: Always "Your <buy_currency> account"
    ExchangeQuoteView:
      type: object
      description: routes/exchange.rs `view` (json!). Every key is always present.
      required:
        - id
        - from_account
        - to_account
        - sell_minor
        - sell_currency
        - buy_minor
        - buy_currency
        - mid
        - rate
        - spread_bps
        - spread_minor
        - remainder
        - remainder_denominator
        - rate_source
        - rate_as_of
        - state
        - refused_reason
        - created_at
        - authorize
        - out_core_ref
        - in_core_ref
        - money_moved
      properties:
        id:
          type: string
          format: uuid
        from_account:
          type: string
        to_account:
          type: string
        sell_minor:
          type: integer
          format: int64
          minimum: 1
        sell_currency:
          type: string
        buy_minor:
          type: integer
          format: int64
          minimum: 1
        buy_currency:
          type: string
        mid:
          type: string
          description: rust_decimal string of rate_numerator/rate_denominator
        rate:
          type: string
          description: mid * (10000 - spread_bps) / 10000, rust_decimal string
        spread_bps:
          type: integer
          format: int64
          minimum: 0
          maximum: 9999
        spread_minor:
          type: integer
          format: int64
          minimum: 0
        remainder:
          type: integer
          format: int64
          minimum: 0
        remainder_denominator:
          type: integer
          format: int64
          minimum: 1
        rate_source:
          type: string
        rate_as_of:
          type: string
          format: date
        state:
          type: string
          description: >
            DB check constraint exchange_quotes_state_check. `awaiting_cosignature`: a company's exchange
            crossing its threshold or its maker's limits, confirmed by its maker and waiting for a second
            person of the company
          enum:
            - awaiting_confirmation
            - awaiting_cosignature
            - authorized
            - refused
            - expired
            - posting
            - posted
        refused_reason:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        authorize:
          $ref: '#/components/schemas/ExchangeAuthorizeBinding'
        out_core_ref:
          type: string
          nullable: true
        in_core_ref:
          type: string
          nullable: true
          description: Never set without out_core_ref
        money_moved:
          type: boolean
          description: true iff state is posted
        business_id:
          type: string
          format: uuid
          nullable: true
          description: The company whose accounts it is between
        cosign_reason:
          type: string
          nullable: true
          enum:
            - threshold
            - per_payment
            - per_day
            - null
        cosigned:
          type: boolean
          description: A second person of the company signed it
    ExchangeQuoteList:
      type: object
      required:
        - quotes
      properties:
        quotes:
          type: array
          description: At most 50, newest first
          items:
            $ref: '#/components/schemas/ExchangeQuoteView'
    ExchangeQuoteBody:
      type: object
      description: routes/exchange.rs `QuoteBody`.
      required:
        - from_account
        - to_account
        - sell_minor
      properties:
        from_account:
          type: string
          description: Caller's core account; otherwise 404
        to_account:
          type: string
          description: Caller's core account, in a different currency; otherwise 404 / 400
        sell_minor:
          type: integer
          format: int64
          minimum: 1
          description: <= 0 is 400 (Refused::NoAmount)
    SessionView:
      type: object
      description: routes/sessions.rs `SessionView`. Never carries device_id.
      required:
        - id
        - name
        - model
        - os
        - app_version
        - began_at
        - last_used_at
        - expires_at
        - current
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          nullable: true
          description: Always null today (view() hard-codes None)
        model:
          type: string
          maxLength: 64
          description: May be ""
        os:
          type: string
          maxLength: 64
          description: May be ""
        app_version:
          type: string
          maxLength: 64
          description: May be ""
        began_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        current:
          type: boolean
          description: Always true on POST /sessions
    SessionEnding:
      type: string
      description: DB check constraint on sessions.ending
      enum:
        - signed_out
        - ended_by_customer
        - ended_by_operator
        - replaced
        - expired
    SessionEndedView:
      type: object
      description: >
        routes/sessions.rs `EndedView`. Both fields are Option in Rust and so nullable on the wire, but on a
        200 the session has ended, and the DB constraint makes ended_at and ending non-null together.
      required:
        - id
        - ended_at
        - ending
      properties:
        id:
          type: string
          format: uuid
        ended_at:
          type: string
          format: date-time
          nullable: true
        ending:
          allOf:
            - $ref: '#/components/schemas/SessionEnding'
          nullable: true
    NoticeTarget:
      type: object
      description: >
        modules/notifications `Target`, serde tag = "kind". `id` is present only for card, transfer, dispute,
        exchange, account and loan; `onboarding` and `nothing` carry `kind` alone. `id` is the id string from
        the event payload, and "" if the payload lacked it.
      required:
        - kind
      properties:
        kind:
          type: string
          enum:
            - onboarding
            - card
            - transfer
            - dispute
            - exchange
            - account
            - loan
            - nothing
        id:
          type: string
    NoticeView:
      type: object
      description: routes/notifications.rs `notice_of` (json!). Every key is always present.
      required:
        - id
        - title
        - line
        - at
        - tone
        - target
      properties:
        id:
          type: string
          format: uuid
          description: The stored event row's id
        title:
          type: string
        line:
          type: string
        at:
          type: string
          format: date-time
          description: The event's occurred_at
        tone:
          type: string
          nullable: true
          enum:
            - positive
            - attention
        target:
          $ref: '#/components/schemas/NoticeTarget'
    NoticeList:
      type: object
      required:
        - notices
      properties:
        notices:
          type: array
          description: Newest first; built from at most 100 events, so there may be fewer than 100 notices
          items:
            $ref: '#/components/schemas/NoticeView'
    LoanInstalment:
      type: object
      description: One row of a schedule (platform_module_lending::Instalment).
      required:
        - number
        - due_on
        - principal_minor
        - interest_minor
        - total_minor
        - balance_after_minor
      properties:
        number:
          type: integer
          format: int32
          minimum: 1
        due_on:
          type: string
          format: date
        principal_minor:
          type: integer
          format: int64
        interest_minor:
          type: integer
          format: int64
        total_minor:
          type: integer
          format: int64
          description: principal + interest
        balance_after_minor:
          type: integer
          format: int64
    LoanAffordabilityVerdict:
      type: object
      description: >
        platform_module_lending::AffordabilityVerdict, internally tagged by `verdict`. Which of the other
        fields are present depends on the tag: `affordable` has ratio_percent and left_over_minor; `too_much`
        has ratio_percent, allowed_percent and left_over_minor; `more_than_is_left` has left_over_minor and
        short_by_minor; `no_income` has none.
      required:
        - verdict
      properties:
        verdict:
          type: string
          enum:
            - affordable
            - too_much
            - more_than_is_left
            - no_income
        ratio_percent:
          type: integer
          format: int64
        allowed_percent:
          type: integer
          format: int64
        left_over_minor:
          type: integer
          format: int64
        short_by_minor:
          type: integer
          format: int64
    LoanAffordability:
      type: object
      required:
        - affordable
        - detail
        - verdict
      properties:
        affordable:
          type: boolean
        detail:
          type: string
          description: The module's sentence, e.g. "this takes 28% of your monthly income, leaving 3,500.00"
        verdict:
          $ref: '#/components/schemas/LoanAffordabilityVerdict'
    LoanQuote:
      type: object
      description: POST /loans/quote. Always indicative (house rate 12.9%).
      required:
        - instalment_minor
        - total_repayable_minor
        - total_interest_minor
        - annual_rate_percent
        - indicative
        - instalments
      properties:
        instalment_minor:
          type: integer
          format: int64
        total_repayable_minor:
          type: integer
          format: int64
        total_interest_minor:
          type: integer
          format: int64
        annual_rate_percent:
          type: string
          description: Exact decimal as a string, e.g. "12.9"
        indicative:
          type: boolean
          enum:
            - true
          description: Always true on this route
        instalments:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/LoanInstalment'
        affordability:
          allOf:
            - $ref: '#/components/schemas/LoanAffordability'
          description: >
            Present only when monthly_income_minor was sent (and not null). Absent otherwise
            (skip_serializing_if), never null.
    LoanOffer:
      type: object
      description: >
        The application's `offer`. Written at apply as the indicative quote (`indicative: true`) and replaced
        by the provider's priced offer on an approval (`indicative: false`). Rows written before the
        platform’s specification carry only instalment_minor, total_repayable_minor, total_interest_minor,
        annual_rate_percent and indicative, hence the shorter `required`.
      required:
        - instalment_minor
        - total_repayable_minor
        - total_interest_minor
        - annual_rate_percent
        - indicative
      properties:
        principal_minor:
          type: integer
          format: int64
          description: Capped at what was asked on a priced offer
        term_months:
          type: integer
          format: int32
        instalment_minor:
          type: integer
          format: int64
        total_repayable_minor:
          type: integer
          format: int64
        total_interest_minor:
          type: integer
          format: int64
        annual_rate_percent:
          type: string
          description: Exact decimal as a string
        instalments:
          type: array
          items:
            $ref: '#/components/schemas/LoanInstalment'
        indicative:
          type: boolean
    LoanDecisionReason:
      type: object
      required:
        - code
        - detail
      properties:
        code:
          type: string
          description: >
            Provider codes pass through as-is. The platform's own are no_decision_provider,
            decision_unavailable, decision_unusable and priced_offer_unaffordable. For decision_unavailable /
            decision_unusable the customer always reads the bank's sentence ("We could not decide this
            automatically, so a person will look at it.").
        detail:
          type: string
    LoanDecisionFigures:
      type: object
      description: Approved only.
      required:
        - limit_minor
        - rate_percent
        - max_term_months
      properties:
        limit_minor:
          type: integer
          format: int64
        rate_percent:
          type: string
          description: Exact decimal as a string
        max_term_months:
          type: integer
          format: int32
    LoanBureauRecord:
      type: object
      description: >-
        What is kept of a credit-bureau check (never the person). Present only when the applicant consented
        and a bureau is configured.
      required:
        - bureau
        - outcome
        - score
        - score_name
        - reasons
        - inquiry_ref
        - consent
      properties:
        bureau:
          type: string
          description: Bureau and environment, e.g. "Círculo de Crédito (sandbox-stub)"
        outcome:
          type: string
          enum:
            - scored
            - no_file
            - unavailable
        score:
          type: integer
          format: int32
          nullable: true
        score_name:
          type: string
          nullable: true
        reasons:
          type: array
          items:
            type: string
        inquiry_ref:
          type: string
          nullable: true
        consent:
          type: object
          required:
            - given_at
            - version
          properties:
            given_at:
              type: string
              format: date-time
            version:
              type: string
              example: circulo-consulta-v1
    LoanEngineDecision:
      type: object
      description: >
        The decision step's record, as the customer receives it: `operator_note` is removed by customer_view.
        Rows decided before the platform’s specification carry only outcome, reasons and provenance
        ("platform"), which is why provider, policy_version and decided_at are not in `required`; every row
        written by the current code has all three.
      required:
        - outcome
        - reasons
        - provenance
      properties:
        outcome:
          type: string
          enum:
            - approved
            - declined
            - referred
        reasons:
          type: array
          items:
            $ref: '#/components/schemas/LoanDecisionReason'
        provider:
          type: string
          description: '`none` when no provider is configured, else the provider''s name (`zen`, `in-memory`, a vendor)'
        policy_version:
          type: string
          nullable: true
          description: Key always present on current rows; null for provider `none`
        provenance:
          type: string
          enum:
            - live
            - synthetic
            - unavailable
            - platform
          description: '`platform` appears only on pre-spec-013 rows'
        decided_at:
          type: string
          format: date-time
        figures:
          allOf:
            - $ref: '#/components/schemas/LoanDecisionFigures'
          description: Present only when outcome is approved
        bureau:
          allOf:
            - $ref: '#/components/schemas/LoanBureauRecord'
          description: Present only when the applicant consented and a bureau is configured. Absent, never null
        explanation:
          allOf:
            - $ref: '#/components/schemas/LoanDecisionExplanation'
          description: >-
            Spec 075. Present on a person's decisions from the platform’s specification on; absent on older
            ones
        operator_note:
          type: string
          nullable: true
          description: >-
            Back office only (GET /backoffice/loans): what the policy said for the operator. customer_view()
            removes it, so a customer response never carries it.
    LoanOperatorDecision:
      type: object
      description: >
        Back office only (GET /backoffice/loans): what a person decided on a referral, written as the
        application's `operator_decision` when the approvals executor carries out an approved POST
        /backoffice/loans/decisions. Kept beside `decision`, never over it: the policy's record survives the
        referral. Names the maker and the checker, which is why the customer is sent
        `LoanCustomerOperatorDecision` instead.
      required:
        - decision
        - reason
        - decided_at
        - decided_by
        - proposed_by
      properties:
        decision:
          type: string
          enum:
            - approve
            - decline
        reason:
          type: string
          description: The proposer's reason, from which an adverse-action notice is produced
        decided_at:
          type: string
          format: date-time
          description: >
            When the approval was carried out. Absent on rows a person decided before the schema, which moved
            them here as they were
        decided_by:
          type: string
          nullable: true
          description: Subject of the approving checker
        proposed_by:
          type: string
          description: Subject of the proposing operator
    LoanCustomerOperatorDecision:
      type: object
      description: >
        What customer_view() sends of a person's decision: what was decided and why, never who. Built from a
        list of these three keys, so a key added to the stored record is not sent by default.
      required:
        - decision
        - reason
      properties:
        decision:
          type: string
          enum:
            - approve
            - decline
        reason:
          type: string
        decided_at:
          type: string
          format: date-time
          description: Absent on rows a person decided before the schema
    LoanDecision:
      description: >
        The decision step's record. It stays the policy's after a person decides a referral — that decision is
        `operator_decision` — so a referral a person approved still reads `outcome: referred` here.
      allOf:
        - $ref: '#/components/schemas/LoanEngineDecision'
    LoanDecisionExplanation:
      type: object
      description: >
        Spec 075. Why a person's application was decided as it was, kept on the decision: how the provider
        weighed it (FinLend's explainable factor model — a risk score, lower is better, its grade and every
        factor), the policy that read the score, the figures of the person's own statement, their latest
        screening (back office only: removed from the customer's copy, which must never say whether a list
        matched), and whether a bureau was asked, on what consent.
      required:
        - assessment
        - policy
        - statement
        - bureau
      properties:
        assessment:
          type: object
          nullable: true
          description: Null when the provider does not explain itself, or there is no provider
          required:
            - score
            - grade
            - band
            - factors
          properties:
            score:
              type: integer
              minimum: 3
              maximum: 97
              description: Lower is better
            grade:
              type: string
              enum:
                - A
                - B
                - C
                - D
                - E
            band:
              type: string
            factors:
              type: array
              items:
                type: object
                required:
                  - name
                  - detail
                  - points
                properties:
                  name:
                    type: string
                    description: The factor with its figure, e.g. "Debt to income 14%"
                  detail:
                    type: string
                    description: What it measures
                  points:
                    type: integer
                    description: >-
                      Added to the risk score (negative: taken away); the score is 50 plus every factor, held
                      to 3–97
        policy:
          type: object
          required:
            - provider
            - version
            - model
            - thresholds
          properties:
            provider:
              type: string
              nullable: true
            version:
              type: string
              nullable: true
            provenance:
              type: string
              nullable: true
              enum:
                - live
                - synthetic
                - unavailable
                - null
            model:
              type: string
              nullable: true
              description: e.g. "FinLend factor model for a person, v1"
            thresholds:
              type: object
              nullable: true
              required:
                - approve_below
                - refer_below
              properties:
                approve_below:
                  type: integer
                  description: '40: a lower score is approved'
                refer_below:
                  type: integer
                  description: '70: a lower score not approved is referred; at or above, declined'
        statement:
          type: object
          required:
            - read
            - currency
            - instalment_minor
          properties:
            read:
              type: boolean
              description: 'False when the statement could not be read: every figure null, never zero'
            currency:
              type: string
            from:
              type: string
              format: date
              nullable: true
            to:
              type: string
              format: date
              nullable: true
            money_in_minor:
              type: integer
              format: int64
              nullable: true
              description: Money between the person's own accounts left out
            money_out_minor:
              type: integer
              format: int64
              nullable: true
            left_monthly_minor:
              type: integer
              format: int64
              nullable: true
              description: In less out, a month's worth, before the new instalment
            instalment_minor:
              type: integer
              format: int64
              description: The new instalment
              at the quoted price: null
            left_after_instalment_minor:
              type: integer
              format: int64
              nullable: true
            history_days:
              type: integer
              nullable: true
              description: How far back the person's history goes
        screening:
          type: object
          description: Back office only; absent from the customer's copy
          required:
            - screened
          properties:
            screened:
              type: boolean
            screened_at:
              type: string
              format: date-time
            outcome:
              type: string
              enum:
                - clear
                - pending
                - blocked
            names_checked:
              type: integer
            lists:
              type: array
              items:
                type: object
                required:
                  - list
                properties:
                  list:
                    type: string
                  version:
                    type: string
                    nullable: true
            matched:
              type: boolean
            open_matches:
              type: integer
            provider:
              type: string
              nullable: true
        bureau:
          type: object
          required:
            - asked
          properties:
            asked:
              type: boolean
            bureau:
              type: string
            outcome:
              type: string
              enum:
                - scored
                - no_file
                - unavailable
            consent:
              type: object
              required:
                - version
                - given_at
              properties:
                version:
                  type: string
                given_at:
                  type: string
                  format: date-time
    LoanAgreementView:
      type: object
      description: >
        loan_agreements.rs `view`, the platform’s specification. A person's loan agreement: the terms of the
        offer, its schedule, the account it is paid into and repaid from, and the document with what to
        confirm to sign it. The loan is `accepted` once signed, and only then paid out.
      required:
        - loan_id
        - state
        - signed
        - terms
        - schedule
        - agreement
      properties:
        loan_id:
          type: string
          format: uuid
        state:
          type: string
        signed:
          type: boolean
        signed_at:
          type: string
          format: date-time
          nullable: true
        account_id:
          type: string
          nullable: true
        terms:
          type: object
          required:
            - currency
          properties:
            amount_minor:
              type: integer
              format: int64
              nullable: true
            currency:
              type: string
            annual_rate_percent:
              type: string
              nullable: true
            term_months:
              type: integer
              nullable: true
            instalment_minor:
              type: integer
              format: int64
              nullable: true
            total_repayable_minor:
              type: integer
              format: int64
              nullable: true
            total_interest_minor:
              type: integer
              format: int64
              nullable: true
        schedule:
          type: array
          items:
            type: object
            required:
              - number
              - due_on
              - total_minor
              - principal_minor
              - interest_minor
            properties:
              number:
                type: integer
              due_on:
                type: string
                format: date
              total_minor:
                type: integer
                format: int64
              principal_minor:
                type: integer
                format: int64
              interest_minor:
                type: integer
                format: int64
              balance_after_minor:
                type: integer
                format: int64
        agreement:
          type: object
          nullable: true
          required:
            - id
            - state
            - sha256
            - pdf
          properties:
            id:
              type: string
              format: uuid
            state:
              type: string
              enum:
                - unsigned
                - signed
            sha256:
              type: string
            signed_at:
              type: string
              format: date-time
              nullable: true
            auth_method:
              type: string
              nullable: true
            authorize:
              type: object
              nullable: true
              description: What the customer's device confirms to sign — the amount and the document's SHA-256
              required:
                - amount
                - currency
                - payee
              properties:
                amount:
                  type: string
                currency:
                  type: string
                payee:
                  type: string
            pdf:
              type: string
            signed_pdf:
              type: string
              nullable: true
            created_at:
              type: string
              format: date-time
    MyLoans:
      type: object
      description: >-
        lending.rs `my_loans`. Each of the caller's loans, drawn or not; a drawn one with its position,
        derived at the moment of asking.
      required:
        - loans
      properties:
        loans:
          type: array
          items:
            type: object
            required:
              - id
              - state
              - currency
              - amount_minor
              - drawn
            properties:
              id:
                type: string
                format: uuid
              state:
                type: string
              currency:
                type: string
              amount_minor:
                type: integer
                format: int64
                description: 'What was lent: the offer''s principal, or what was asked'
              term_months:
                type: integer
                nullable: true
              drawn:
                type: boolean
              disbursed_on:
                type: string
                format: date
                nullable: true
              held_by:
                type: string
                enum:
                  - core
                  - platform
              position:
                type: object
                required:
                  - as_of
                  - received_minor
                  - due_by_now_minor
                  - arrears_minor
                  - days_past_due
                  - in_arrears
                  - settled
                  - provenance
                  - instalments
                properties:
                  as_of:
                    type: string
                    format: date
                  received_minor:
                    type: integer
                    format: int64
                  due_by_now_minor:
                    type: integer
                    format: int64
                  arrears_minor:
                    type: integer
                    format: int64
                  days_past_due:
                    type: integer
                  in_arrears:
                    type: boolean
                  settled:
                    type: boolean
                  provenance:
                    type: string
                    enum:
                      - derived
                  instalments:
                    type: array
                    items:
                      type: object
                      required:
                        - number
                        - due_on
                        - due_minor
                        - paid_minor
                        - outstanding_minor
                      properties:
                        number:
                          type: integer
                        due_on:
                          type: string
                          format: date
                        due_minor:
                          type: integer
                          format: int64
                        paid_minor:
                          type: integer
                          format: int64
                        outstanding_minor:
                          type: integer
                          format: int64
                        cleared_on:
                          type: string
                          format: date
                          nullable: true
                        days_past_due:
                          type: integer
    LoanApplicationView:
      type: object
      description: lending.rs customer_view(). Returned by GET /loans (items), POST /loans and POST /loans/{id}/withdraw.
      required:
        - id
        - state
        - awaiting_us
        - is_final
        - amount_minor
        - currency
        - term_months
        - purpose
        - decision
        - operator_decision
        - offer
        - core_loan_ref
        - created_at
        - your_moves
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - draft
            - submitted
            - referred
            - approved
            - accepted
            - declined
            - withdrawn
            - disbursed
            - expired
            - written_off
        awaiting_us:
          type: boolean
          description: true for submitted, referred, approved, accepted
        is_final:
          type: boolean
          description: >-
            true for declined, withdrawn, expired, written_off — not disbursed: a drawn loan is still being
            repaid
        amount_minor:
          type: integer
          format: int64
          description: What was asked (requested_minor)
          not the offer's principal: null
        currency:
          type: string
        term_months:
          type: integer
          format: int32
        purpose:
          type: string
          nullable: true
        decision:
          allOf:
            - $ref: '#/components/schemas/LoanDecision'
          nullable: true
        operator_decision:
          allOf:
            - $ref: '#/components/schemas/LoanCustomerOperatorDecision'
          nullable: true
          description: Null until a person decides a referral and a second person approves it
        offer:
          allOf:
            - $ref: '#/components/schemas/LoanOffer'
          nullable: true
        core_loan_ref:
          type: string
          nullable: true
        business_id:
          type: string
          format: uuid
          nullable: true
          description: Spec 072. The company that borrows; absent or null for a person.
        declared_turnover_minor:
          type: integer
          nullable: true
          description: Spec 072. What the company declared it turned over in twelve months.
        read_figures:
          type: object
          nullable: true
          description: Spec 072. What the bank read of the company, each figure with its provenance.
        offer_expires_on:
          type: string
          format: date
          nullable: true
          description: Spec 072. The last day a company's offer can be signed.
        account_id:
          type: string
          nullable: true
          description: Spec 075. The account it is paid into and repaid from, named on its agreement
        created_at:
          type: string
          format: date-time
        your_moves:
          type: array
          description: >-
            draft: [submit, withdraw]; submitted/referred: [withdraw]; approved: [withdraw, accept] — accept
            is signing the agreement (POST /loans/{id}/agreement/sign); accepted: [withdraw]; final states: []
          items:
            type: string
            enum:
              - submit
              - withdraw
              - accept
    OnboardingNextStep:
      type: object
      required:
        - kind
        - prompt
      properties:
        kind:
          type: string
          enum:
            - identity
            - address
            - tax_residency
            - source_of_funds
            - terms_accepted
        prompt:
          type: string
          description: The module's sentence, e.g. "read and accept the terms"
    OnboardingRequirementView:
      type: object
      required:
        - kind
        - status
        - blocking
        - expires_on
        - lapsed
        - failure_detail
        - provider_ref
      properties:
        kind:
          type: string
          enum:
            - identity
            - address
            - screening
            - tax_residency
            - source_of_funds
            - terms_accepted
          description: '`screening` is listed here (it is the bank''s own check) but never offered as next_step'
        status:
          type: string
          enum:
            - outstanding
            - submitted
            - satisfied
            - failed
        blocking:
          type: boolean
        expires_on:
          type: string
          format: date
          nullable: true
        lapsed:
          type: boolean
          description: status is satisfied and expires_on is before today
        failure_detail:
          type: string
          nullable: true
        provider_ref:
          type: string
          nullable: true
    OnboardingJourney:
      type: object
      description: >
        onboarding.rs view(). Two shapes share this schema. Normal: state is awaiting_customer, awaiting_us,
        ready or refused and `final` is present. Ineligible (eligibility refused at start): state is
        `ineligible`, detail is the recorded reason, next_step is null, requirements is [] and `final` is
        ABSENT.
      required:
        - id
        - state
        - detail
        - next_step
        - requirements
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - awaiting_customer
            - awaiting_us
            - ready
            - refused
            - ineligible
        final:
          type: boolean
          description: true for ready and refused. Absent when state is ineligible
        detail:
          type: string
          description: >
            awaiting_customer: "N thing(s) left for you"; awaiting_us: "nothing for you to do; we are checking
            N thing(s)"; ready: "everything checks out"; refused: "kind: detail;..." joined; ineligible: the
            eligibility reason.
        next_step:
          allOf:
            - $ref: '#/components/schemas/OnboardingNextStep'
          nullable: true
          description: Non-null only when state is awaiting_customer
        requirements:
          type: array
          items:
            $ref: '#/components/schemas/OnboardingRequirementView'
    OnboardingScreeningResult:
      type: object
      required:
        - status
        - screened_at
      properties:
        status:
          type: string
          enum:
            - clear
            - in_review
        screened_at:
          type: string
          format: date-time
    FundingView:
      type: object
      description: >
        crypto.rs view(). GET /funding items; POST /funding adds `note` (FundingStarted). Spec 076, by
        decision: the platform holds no tokens and keeps no crypto account — a funding is credited in dollars
        to the customer's account, one leg, under `crypto-credit:<funding id>`. There is no holdings view and
        no average cost; a client must not pair statement legs into one, and must not treat an account as
        crypto by its kind. Holdings come with a custodian adapter.
      required:
        - id
        - stage
        - detail
        - pending
        - final
        - network
        - token
        - token_decimals
        - deposit_address
        - deposit_memo
        - address_provenance
        - amount_observed
        - confirmations
        - confirmations_required
        - tx_hash
        - settles_in
        - fee_minor
        - credited_minor
        - failure_detail
        - created_at
      properties:
        id:
          type: string
          format: uuid
        stage:
          type: string
          enum:
            - awaiting_deposit
            - confirming
            - converting
            - credited
            - failed
        detail:
          type: string
          enum:
            - waiting for your deposit
            - confirming on the network
            - converting to your account currency
            - credited to your account
            - this could not be completed, and somebody is looking at it
          description: Fixed per stage, in the order of the stage enum
        pending:
          type: boolean
          description: awaiting_deposit, confirming, converting
        final:
          type: boolean
          description: credited, failed
        network:
          type: string
        token:
          type: string
          description: Token symbol
        token_decimals:
          type: integer
          format: int32
        deposit_address:
          type: string
          nullable: true
          description: The ramp's node address, the customer's own; null when no ramp is bound or none is free
        deposit_memo:
          type: string
          nullable: true
        address_provenance:
          type: string
          enum:
            - live
            - synthetic
            - unavailable
          description: unavailable iff deposit_address is null; synthetic on the bank's sandbox chain
        amount_observed:
          type: string
          nullable: true
          description: i128 in the token's smallest unit, as a decimal-digit string
        confirmations:
          type: integer
          format: int32
          minimum: 0
        confirmations_required:
          type: integer
          format: int32
          description: ethereum 12, polygon 128, arbitrum 20, any other network 256
        tx_hash:
          type: string
          nullable: true
        settles_in:
          type: string
        fee_minor:
          type: integer
          format: int64
          minimum: 0
          description: Never null (column NOT NULL DEFAULT 0)
        credited_minor:
          type: integer
          format: int64
          nullable: true
        failure_detail:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    FundingStarted:
      allOf:
        - $ref: '#/components/schemas/FundingView'
        - type: object
          required:
            - note
          properties:
            note:
              type: string
              description: 'Fixed text: no ramp provider is configured, so there is no deposit address...'
    FundingConversionRate:
      type: object
      required:
        - numerator
        - denominator
      properties:
        numerator:
          type: string
          description: i128 as a decimal-digit string
        denominator:
          type: string
          description: i128 as a decimal-digit string
    FundingConversion:
      type: object
      description: POST /funding/conversion. House rate 9993/10000 and fee 50 minor until a provider quotes.
      required:
        - gross_minor
        - fee_minor
        - net_minor
        - fee_exceeded_deposit
        - settles_in
        - peg
        - rate
        - indicative
      properties:
        gross_minor:
          type: integer
          format: int64
        fee_minor:
          type: integer
          format: int64
        net_minor:
          type: integer
          format: int64
          minimum: 0
        fee_exceeded_deposit:
          type: boolean
        settles_in:
          type: string
        peg:
          type: string
          pattern: ^-?[0-9]+\.[0-9]{4}$
          description: The ratio at four places, e.g. "0.9993"
        rate:
          $ref: '#/components/schemas/FundingConversionRate'
        indicative:
          type: boolean
          enum:
            - true
          description: Always true today
    InvestmentRiskProfile:
      type: string
      enum:
        - cautious
        - balanced
        - adventurous
    InvestmentTarget:
      type: object
      required:
        - instrument
        - weight_bps
      properties:
        instrument:
          type: string
        weight_bps:
          type: integer
          format: int64
    InvestmentModel:
      type: object
      required:
        - name
        - risk_profile
        - targets
        - drift_band_bps
      properties:
        name:
          type: string
          enum:
            - cautious
            - balanced
            - adventurous
        risk_profile:
          $ref: '#/components/schemas/InvestmentRiskProfile'
        targets:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/InvestmentTarget'
        drift_band_bps:
          type: integer
          format: int64
    InvestmentModelList:
      type: object
      required:
        - models
      properties:
        models:
          type: array
          items:
            $ref: '#/components/schemas/InvestmentModel'
    InvestmentSuitabilityVerdict:
      type: object
      description: >
        platform_module_investments::SuitabilityVerdict, internally tagged by `verdict`. `too_risky` has
        assessed and required; `more_cautious_than_assessed` has assessed and model; `stale` has days_ago and
        valid_for_days; `suitable` has none.
      required:
        - verdict
      properties:
        verdict:
          type: string
          enum:
            - suitable
            - too_risky
            - more_cautious_than_assessed
            - stale
        assessed:
          $ref: '#/components/schemas/InvestmentRiskProfile'
        required:
          $ref: '#/components/schemas/InvestmentRiskProfile'
        model:
          $ref: '#/components/schemas/InvestmentRiskProfile'
        days_ago:
          type: integer
          format: int32
          minimum: 0
        valid_for_days:
          type: integer
          format: int32
          minimum: 0
    InvestmentSuitability:
      type: object
      required:
        - permits
        - detail
        - verdict
      properties:
        permits:
          type: boolean
        detail:
          type: string
        verdict:
          $ref: '#/components/schemas/InvestmentSuitabilityVerdict'
    InvestmentPosition:
      type: object
      required:
        - instrument
        - quantity
        - quantity_scale
        - price_minor
      properties:
        instrument:
          type: string
        quantity:
          type: string
          description: >-
            The raw scaled i128 as digits (e.g. "124000" with quantity_scale 4 means 12.4000). Not a decimal
            string
        quantity_scale:
          type: integer
          format: int32
        price_minor:
          type: integer
          format: int64
        cost_minor:
          type: integer
          format: int64
          nullable: true
          description: >
            Spec 076. What the holding cost, from the broker's fills at average cost (GET /investments/fills);
            null where nothing was filled. Gain is the position's value less this. Tax lots are not accounted
            for
    InvestmentDrift:
      type: object
      required:
        - instrument
        - target_bps
        - actual_bps
        - drift_bps
        - target_minor
        - actual_minor
      properties:
        instrument:
          type: string
        target_bps:
          type: integer
          format: int64
        actual_bps:
          type: integer
          format: int64
        drift_bps:
          type: integer
          format: int64
          description: actual_bps - target_bps
        target_minor:
          type: integer
          format: int64
        actual_minor:
          type: integer
          format: int64
    InvestmentOrder:
      type: object
      required:
        - instrument
        - side
        - amount_minor
      properties:
        instrument:
          type: string
        side:
          type: string
          enum:
            - buy
            - sell
        amount_minor:
          type: integer
          format: int64
          description: By value, not quantity
    InvestmentRebalancePlan:
      type: object
      required:
        - within_band
        - orders
        - cash_after_minor
      properties:
        within_band:
          type: boolean
        orders:
          type: array
          items:
            $ref: '#/components/schemas/InvestmentOrder'
        cash_after_minor:
          type: integer
          format: int64
    InvestmentContribution:
      type: object
      required:
        - id
        - from_account
        - amount_minor
        - currency
        - reference
        - created_at
      properties:
        id:
          type: string
          format: uuid
        from_account:
          type: string
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
        reference:
          type: string
          description: '`investment:<id>`, as the statement shows it'
        created_at:
          type: string
          format: date-time
    InvestmentContributions:
      type: object
      required:
        - contributions
      properties:
        contributions:
          type: array
          items:
            $ref: '#/components/schemas/InvestmentContribution'
    InvestmentFills:
      type: object
      required:
        - fills
        - broker_bound
      properties:
        broker_bound:
          type: boolean
        fills:
          type: array
          items:
            type: object
            required:
              - order_id
              - instrument
              - side
              - quantity
              - quantity_scale
              - value_minor
              - filled_at
            properties:
              order_id:
                type: string
                format: uuid
              instrument:
                type: string
              side:
                type: string
                enum:
                  - buy
                  - sell
              quantity:
                type: string
                description: Scaled digits
                as positions': null
              quantity_scale:
                type: integer
              value_minor:
                type: integer
                format: int64
              filled_at:
                type: string
                format: date-time
    InvestmentAccountView:
      type: object
      description: investments.rs account_view(). GET /investments (and POST /investments 201, not in this group).
      required:
        - id
        - model
        - risk_profile
        - assessed_on
        - suitability
        - total_minor
        - positions
        - drift
        - proposed_rebalance
        - provenance
        - contributions
      properties:
        contributions:
          type: object
          description: Spec 076. Adding money to investing (POST /investments/contributions)
          required:
            - open
            - currency
            - payee
          properties:
            open:
              type: boolean
              description: Whether the bank takes money into investing yet
            currency:
              type: string
            payee:
              type: string
              description: What the device confirms as the payee
              beside the amount: null
        id:
          type: string
          format: uuid
        model:
          type: string
          enum:
            - cautious
            - balanced
            - adventurous
        risk_profile:
          type: string
          description: Stored text; written only as cautious|balanced|adventurous
        assessed_on:
          type: string
          format: date
        suitability:
          allOf:
            - $ref: '#/components/schemas/InvestmentSuitability'
          nullable: true
          description: Null only if the stored risk_profile is not one of the three
        total_minor:
          type: integer
          format: int64
        positions:
          type: array
          items:
            $ref: '#/components/schemas/InvestmentPosition'
        drift:
          type: array
          items:
            $ref: '#/components/schemas/InvestmentDrift'
        proposed_rebalance:
          $ref: '#/components/schemas/InvestmentRebalancePlan'
        provenance:
          type: string
          enum:
            - unavailable
          description: 'Hard-coded: there is no broker'
    DisputeEvidenceAsk:
      type: object
      required:
        - kind
        - ask
      properties:
        kind:
          type: string
          enum:
            - order_detail
            - cancellation_proof
            - agreed_amount
            - merchant_contacted
        ask:
          type: string
    DisputeCaseView:
      type: object
      description: disputes.rs customer_view() for a row this build can read.
      required:
        - id
        - payment_kind
        - payment_ref
        - amount_minor
        - currency
        - reason
        - reason_prompt
        - stage
        - next_step
        - deadline
        - outstanding
        - provisional_credit_proposed
        - outcome
        - raised_at
        - your_moves
      properties:
        id:
          type: string
          format: uuid
        payment_kind:
          type: string
          enum:
            - card
            - transfer
        payment_ref:
          type: string
          description: Card authorisation id, or transfer intent id (uuid)
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
        reason:
          type: string
          enum:
            - not_recognised
            - not_received
            - not_as_described
            - cancelled
            - duplicate
            - wrong_amount
        reason_prompt:
          type: string
        stage:
          type: string
          enum:
            - raised
            - evidence_required
            - with_the_merchant
            - decided
            - withdrawn
        next_step:
          type: string
        deadline:
          type: string
          format: date-time
          nullable: true
          description: raised_at + 30 days, only while with_the_merchant
        outstanding:
          type: array
          items:
            $ref: '#/components/schemas/DisputeEvidenceAsk'
        provisional_credit_proposed:
          type: boolean
        outcome:
          type: string
          nullable: true
          enum:
            - upheld
            - declined
            - null
        raised_at:
          type: string
          format: date-time
        your_moves:
          type: array
          description: >-
            raised/with_the_merchant: [withdraw]; evidence_required: [supply_evidence, withdraw];
            decided/withdrawn: []
          items:
            type: string
            enum:
              - supply_evidence
              - withdraw
    DisputeUnreadableView:
      type: object
      description: What customer_view() returns for a stored row whose reason or stage this build does not know.
      required:
        - id
        - stage
        - unreadable
      properties:
        id:
          type: string
          format: uuid
        stage:
          type: string
          description: The stored stage text
          unparsed: null
        unreadable:
          type: boolean
          enum:
            - true
    DisputeView:
      oneOf:
        - $ref: '#/components/schemas/DisputeCaseView'
        - $ref: '#/components/schemas/DisputeUnreadableView'
    DisputeReasonOption:
      type: object
      required:
        - reason
        - prompt
        - within_days
        - needs
        - provisional_credit
      properties:
        reason:
          type: string
          enum:
            - not_recognised
            - not_received
            - not_as_described
            - cancelled
            - duplicate
            - wrong_amount
        prompt:
          type: string
        within_days:
          type: integer
          format: int64
          description: not_recognised 540, all others 120
        needs:
          type: array
          items:
            $ref: '#/components/schemas/DisputeEvidenceAsk'
        provisional_credit:
          type: boolean
          description: true for not_recognised and duplicate
    DisputeList:
      type: object
      required:
        - disputes
        - reasons
      properties:
        disputes:
          type: array
          items:
            $ref: '#/components/schemas/DisputeView'
        reasons:
          type: array
          items:
            $ref: '#/components/schemas/DisputeReasonOption'
    DisputeAlreadyOpen:
      type: object
      description: POST /disputes 409 body. NOT a Problem document.
      required:
        - detail
        - dispute
      properties:
        detail:
          type: string
          description: '"You already have a case open on this payment, raised on <d Month>. <next_step>"'
        dispute:
          $ref: '#/components/schemas/DisputeView'
    SupportMessage:
      type: object
      required:
        - author
        - channel
        - body
        - at
        - from_assistant
      properties:
        author:
          type: string
          enum:
            - customer
            - agent
            - assistant
            - system
        channel:
          type: string
          enum:
            - chat
            - voice
        body:
          type: string
        at:
          type: string
          format: date-time
        from_assistant:
          type: boolean
          description: author == assistant
    SupportCallback:
      type: object
      required:
        - state
        - attempts
        - still_owed
        - topic
        - requested_at
      properties:
        state:
          type: string
          enum:
            - requested
            - scheduled
            - missed
            - completed
            - abandoned
        attempts:
          type: integer
          format: int32
          minimum: 0
        still_owed:
          type: boolean
          description: requested, scheduled, missed
        topic:
          type: string
          nullable: true
        requested_at:
          type: string
          format: date-time
    AssistantHandover:
      type: object
      description: platform-module-assistant `Handover`, the platform’s specification
      required:
        - reason
        - kind
        - intent
        - checks
        - actions
        - turns
      properties:
        reason:
          type: string
        kind:
          type: string
        intent:
          type: string
          nullable: true
        checks:
          type: array
          items:
            type: string
        actions:
          type: array
          items:
            type: string
        turns:
          type: integer
    AssistantConversation:
      type: object
      description: routes/assistant.rs `view`, the platform’s specification
      required:
        - id
        - phase
        - finished
        - answered_by
        - support_id
        - may
        - never
        - messages
        - reply
        - voice
        - created_at
      properties:
        id:
          type: string
          format: uuid
        phase:
          type: string
          enum:
            - open
            - confirm
            - done
        finished:
          type: boolean
        answered_by:
          type: string
          description: 'What read the last turn: rules, or model <name>'
        support_id:
          type: string
          format: uuid
          nullable: true
          description: The support conversation it was handed to
        may:
          type: array
          items:
            type: string
        never:
          type: array
          items:
            type: string
        messages:
          type: array
          items:
            type: object
            required:
              - who
              - text
              - at
              - from_assistant
            properties:
              who:
                type: string
                enum:
                  - customer
                  - assistant
              text:
                type: string
              at:
                type: string
                format: date-time
              from_assistant:
                type: boolean
        reply:
          type: object
          nullable: true
          required:
            - lines
            - spoken
            - offers
            - suggestions
            - finished
            - handed_over
          properties:
            lines:
              type: array
              items:
                type: string
            spoken:
              type: array
              items:
                type: string
              description: >-
                Spec 063: the same lines as a person says them (amounts, dates and numbers in words, nothing
                unreadable), one for each line. What the apps speak.
            offers:
              type: array
              items:
                type: object
                required:
                  - label
                  - href
                properties:
                  label:
                    type: string
                  href:
                    type: string
                    description: A platform path (a PDF) or an app screen (/disputes)
            suggestions:
              type: array
              items:
                type: string
            finished:
              type: boolean
            handed_over:
              type: boolean
        voice:
          type: object
          description: 'Spec 063: how the apps speak, on the device; no audio is sent to the platform'
          required:
            - voice
            - speed
            - acknowledgements
            - not_heard
          properties:
            voice:
              type: string
              description: The on-device voice's name (Kokoro's)
            speed:
              type: number
            acknowledgements:
              type: array
              items:
                type: string
              description: Said the moment the customer stops speaking, while the answer is made
            not_heard:
              type: string
              description: Said when nothing could be made of what was heard
        created_at:
          type: string
          format: date-time
    SupportConversation:
      type: object
      description: support.rs view(). Every customer support route returns this (GET as a bare array).
      required:
        - id
        - topic
        - turn
        - waiting_on_us
        - resolved
        - created_at
        - messages
        - callback
        - callback_note
      properties:
        id:
          type: string
          format: uuid
        topic:
          type: string
          nullable: true
        turn:
          type: string
          enum:
            - us
            - customer
            - resolved
        waiting_on_us:
          type: boolean
          description: turn == us
        resolved:
          type: boolean
        created_at:
          type: string
          format: date-time
        messages:
          type: array
          items:
            $ref: '#/components/schemas/SupportMessage'
        callback:
          allOf:
            - $ref: '#/components/schemas/SupportCallback'
          nullable: true
        handover:
          description: 'Spec 061, the back office''s queue only: what the assistant handed over with it'
          nullable: true
          allOf:
            - $ref: '#/components/schemas/AssistantHandover'
        callback_note:
          type: string
          enum:
            - we call you on the number we already hold; we will never ask you to give one out
    BackofficeDeadLetterList:
      type: object
      description: >
        GET /backoffice/events/dead-letters (routes/events.rs `dead_letters`). The tenant's outbox rows with
        status 'dead_lettered', oldest first (ORDER BY created_at, id), at most `limit` (default 100, clamped
        to 1..500).
      required:
        - dead_letters
      properties:
        dead_letters:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeDeadLetter'
    BackofficeDeadLetter:
      type: object
      description: platform_store::unit_of_work::DeadLetter, written key by key by the handler.
      required:
        - id
        - type
        - attempts
        - last_error
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: The outbox row's id (also the event id). This is the id POST.../{id}/requeue takes.
        type:
          type: string
          nullable: true
          description: >
            `event->>'type'` of the stored row, e.g. "transfer.posted". Null when the row's JSON has no string
            `type` (a row the relay could not decode is dead-lettered at once).
        attempts:
          type: integer
          format: int32
          minimum: 0
        last_error:
          type: string
          nullable: true
          description: The relay's last failure text; free prose, not an enum.
        created_at:
          type: string
          format: date-time
          description: When the outbox row was written, not when it was dead-lettered.
    BackofficeDeadLetterRequeued:
      type: object
      description: >
        POST /backoffice/events/dead-letters/{id}/requeue. A fixed shape: the path id echoed and the literal
        state "pending" (the row's status, attempts reset to 0, last_error NULL).
      required:
        - id
        - state
      properties:
        id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - pending
    BackofficeReplayResult:
      type: object
      description: >
        202 of POST /events/replay: platform_events::replay::Replayed plus the topic echoed. Positions count
        the tenant's PUBLISHED outbox rows (status 'published'), all topics together, in outbox order. `sent`
        counts only events whose topic is `topic`; rows that are not decodable events are skipped but still
        advance the position.
      required:
        - topic
        - sent
        - from
        - next
      properties:
        topic:
          $ref: '#/components/schemas/BackofficeReplayTopic'
        sent:
          type: integer
          format: int64
          minimum: 0
        from:
          type: integer
          format: int64
          minimum: 0
          description: >
            Where this call started: 0 for "earliest", the tenant's published count at the time for "latest",
            or the number sent.
        next:
          type: integer
          format: int64
          minimum: 0
          description: The `from` to send on the following call. Equals `from` when nothing was looked at.
    BackofficeReplayInterrupted:
      type: object
      description: >
        503 of POST /events/replay when the bus stopped answering part way (ReplayError::Bus). NOT a Problem:
        a plain object with no `from` and no type/title/status/correlation_id. `sent` events went out; resume
        with from = `next`.
      required:
        - topic
        - sent
        - next
        - detail
      properties:
        topic:
          $ref: '#/components/schemas/BackofficeReplayTopic'
        sent:
          type: integer
          format: int64
          minimum: 0
        next:
          type: integer
          format: int64
          minimum: 0
        detail:
          type: string
          description: '"the bus stopped answering; resume from <next>: <reason>"'
    BackofficeReplayTopic:
      type: string
      description: >
        platform_events::kafka::TOPICS. The handler refuses any other value with 400 (a Problem naming the
        list), so a request can only succeed with one of these.
      enum:
        - platform.payments
        - platform.core
        - platform.approvals
        - platform.audit
        - platform.dlq
    BackofficeNoticeList:
      type: object
      description: >
        GET /backoffice/notifications (routes/notifications.rs `whose`). The same `notices_for` the customer's
        GET /notifications uses, run for `subject` instead of the caller, plus the subject echoed. Differs
        from NoticeList only by the `subject` key. Built from the subject's newest 100 outbox rows (any
        status, ORDER BY id DESC); rows the notifications module has no notice for are dropped, so there may
        be fewer than 100. An unknown subject is not an error: it is 200 with notices: [].
      required:
        - subject
        - notices
      properties:
        subject:
          type: string
          description: The query parameter echoed exactly as sent (not trimmed, not validated).
        notices:
          type: array
          items:
            $ref: '#/components/schemas/NoticeView'
    BackofficeCallbackOutcome:
      type: string
      description: >
        What happened to a call. `scheduled`: requested|missed -> scheduled. `no_answer`: scheduled|missed ->
        missed (attempts+1), or -> abandoned when attempts reaches 3 (MAX_ATTEMPTS). `completed`:
        requested|scheduled|missed -> completed. Any other value is 400; a move the current state does not
        allow is 409.
      enum:
        - scheduled
        - no_answer
        - completed
    BackofficeStatementUpload:
      type: object
      description: >
        201 of POST /backoffice/statements (routes/reconciliation.rs `ingest_statement`). 201 even when every
        statement was already held and nothing was stored.
      required:
        - source
        - statements
      properties:
        source:
          type: string
          description: The `source` query parameter, trimmed.
        statements:
          type: array
          minItems: 1
          description: One per <Stmt> in the file, in file order. Never empty (an empty parse is 400).
          items:
            $ref: '#/components/schemas/BackofficeStatementUploadResult'
    BackofficeStatementUploadResult:
      type: object
      description: >
        routes/reconciliation.rs `store_statement` view (json!). Counts and balances-check are computed from
        the file as uploaded, including when outcome is already_held.
      required:
        - statement_ref
        - account_ref
        - currency
        - period_from
        - period_to
        - outcome
        - statement_id
        - entries
        - pending_entries
        - reconciles
      properties:
        statement_ref:
          type: string
          description: The bank's statement id (<Stmt><Id>).
        account_ref:
          type: string
          description: The account as the file names it (IBAN or other id).
        currency:
          type: string
          description: ISO 4217 code from the file, e.g. USD.
        period_from:
          type: string
          format: date
        period_to:
          type: string
          format: date
        outcome:
          type: string
          enum:
            - stored
            - already_held
          description: >
            already_held when a statement with the same (account_ref, statement_ref) exists in the tenant;
            nothing is stored and statement_id is the held one's.
        statement_id:
          type: string
          format: uuid
        entries:
          type: integer
          minimum: 0
          description: Lines in the statement (usize).
        pending_entries:
          type: integer
          minimum: 0
          description: Lines whose bank status is not booked (pending AND other statuses both count).
        reconciles:
          type: boolean
          description: >
            Whether the BOOKED lines account for the movement from opening to closing balance. false is stored
            anyway and only flagged here.
    BackofficeProducts:
      type: object
      description: routes/settings.rs `products`, the platform’s specification
      required:
        - products
      properties:
        products:
          type: array
          items:
            type: object
            required:
              - id
              - kind
              - name
              - short_name
              - currency
              - active
              - accounting
            properties:
              id:
                type: string
              kind:
                type: string
                enum:
                  - savings
                  - loan
              name:
                type: string
              short_name:
                type: string
              description:
                type: string
                nullable: true
              currency:
                type: string
              annual_rate:
                type: string
                nullable: true
                description: The nominal yearly rate in per cent
              active:
                type: boolean
              accounting:
                type: string
              principal_minor:
                type: integer
                format: int64
                nullable: true
              min_principal_minor:
                type: integer
                format: int64
                nullable: true
              max_principal_minor:
                type: integer
                format: int64
                nullable: true
              repayments:
                type: integer
                nullable: true
              min_repayments:
                type: integer
                nullable: true
              max_repayments:
                type: integer
                nullable: true
              repayment_every:
                type: string
                nullable: true
              overdraft:
                type: boolean
                nullable: true
              min_balance_minor:
                type: integer
                format: int64
                nullable: true
    BackofficeCalendars:
      type: object
      description: routes/settings.rs `calendars`, the platform’s specification
      required:
        - year
        - rails
        - rules
        - note
      properties:
        year:
          type: integer
        note:
          type: string
        rules:
          type: array
          description: What each set of rules keeps in the year, whether a rail follows it or not
          items:
            type: object
            required:
              - rules
              - holidays
            properties:
              rules:
                type: string
                enum:
                  - fed
                  - target2
              holidays:
                type: array
                items:
                  type: object
                  required:
                    - date
                    - name
                  properties:
                    date:
                      type: string
                      format: date
                    name:
                      type: string
        rails:
          type: array
          items:
            type: object
            required:
              - rail
              - rules
              - holidays
            properties:
              rail:
                type: string
              rules:
                type: string
                enum:
                  - none
                  - fed
                  - target2
              holidays:
                type: array
                items:
                  type: object
                  required:
                    - date
                    - name
                    - contract
                  properties:
                    date:
                      type: string
                      format: date
                    name:
                      type: string
                    contract:
                      type: boolean
                      description: A day the bank's contract with the rail adds
    BackofficeBranches:
      type: object
      description: routes/settings.rs `branches`, the platform’s specification
      required:
        - branches
      properties:
        branches:
          type: array
          items:
            type: object
            required:
              - id
              - code
              - name
              - city
              - vault_account
              - currency
              - closed
              - staff
              - customers
            properties:
              id:
                type: string
                format: uuid
              code:
                type: string
              name:
                type: string
              city:
                type: string
              vault_account:
                type: string
              currency:
                type: string
              closed:
                type: boolean
              staff:
                type: integer
              customers:
                type: integer
    BackofficeAuthority:
      type: object
      description: routes/settings.rs `authority`, the platform’s specification
      required:
        - roles
        - most_minor
        - note
      properties:
        most_minor:
          type: integer
          format: int64
        note:
          type: string
        roles:
          type: array
          items:
            type: object
            required:
              - role
              - title
              - decides
            properties:
              role:
                type: string
              title:
                type: string
              posting:
                type: object
                nullable: true
                required:
                  - default_minor
                  - limit_minor
                properties:
                  default_minor:
                    type: integer
                    format: int64
                  limit_minor:
                    type: integer
                    format: int64
                  set:
                    $ref: '#/components/schemas/BackofficeAuthoritySet'
              decides:
                type: array
                items:
                  type: object
                  required:
                    - domain
                    - limit_minor
                  properties:
                    domain:
                      type: string
                    limit_minor:
                      type: integer
                      format: int64
                      nullable: true
                    default_minor:
                      type: integer
                      format: int64
                      nullable: true
                    set:
                      $ref: '#/components/schemas/BackofficeAuthoritySet'
    BackofficeAuthoritySet:
      type: object
      nullable: true
      required:
        - by
        - at
        - approval_id
      properties:
        by:
          type: string
        at:
          type: string
          format: date-time
        approval_id:
          type: string
          format: uuid
    BusinessBureauSubject:
      type: object
      description: platform_ports::business_bureau_port::BusinessSubject, the platform’s specification
      required:
        - legal_name
        - city
        - region
      properties:
        legal_name:
          type: string
        street:
          type: string
          nullable: true
        city:
          type: string
        region:
          type: string
          description: The two-letter state
        postcode:
          type: string
          nullable: true
        tax_id:
          type: string
          nullable: true
          description: The EIN
        phone:
          type: string
          nullable: true
    BusinessBureauMatch:
      type: object
      required:
        - bureau_id
        - name
        - address
      properties:
        bureau_id:
          type: string
        name:
          type: string
        address:
          type: string
        reliability:
          type: integer
          nullable: true
          description: How sure the bureau is, in hundredths (9640 is 96.40)
        tradelines:
          type: integer
          nullable: true
    BusinessBureauScore:
      type: object
      required:
        - model
        - score
        - factors
      properties:
        model:
          type: string
        score:
          type: integer
        percentile:
          type: integer
          nullable: true
        risk_class:
          type: integer
          nullable: true
          description: 1 low … 5 high
        risk:
          type: string
          nullable: true
        recommended_limit_minor:
          type: integer
          format: int64
          nullable: true
        factors:
          type: array
          items:
            type: object
            required:
              - code
              - text
            properties:
              code:
                type: string
              text:
                type: string
    BusinessBureauAnswer:
      type: object
      description: 'platform_ports::business_bureau_port::BusinessAnswer: scored or no_file'
      required:
        - outcome
        - bureau
        - bureau_id
      properties:
        outcome:
          type: string
          enum:
            - scored
            - no_file
        bureau:
          type: string
        bureau_id:
          type: string
        business_name:
          type: string
        payment:
          allOf:
            - $ref: '#/components/schemas/BusinessBureauScore'
          nullable: true
        stability:
          allOf:
            - $ref: '#/components/schemas/BusinessBureauScore'
          nullable: true
        inquiry_ref:
          type: string
          nullable: true
        reasons:
          type: array
          items:
            type: string
    BusinessCreditCheck:
      type: object
      description: routes/business_credit.rs `check_view`, the platform’s specification
      required:
        - id
        - bureau
        - bureau_id
        - business_name
        - outcome
        - answer
        - asked_by
        - asked_at
      properties:
        id:
          type: string
          format: uuid
        bureau:
          type: string
        bureau_id:
          type: string
        business_name:
          type: string
        outcome:
          type: string
          enum:
            - scored
            - no_file
        answer:
          $ref: '#/components/schemas/BusinessBureauAnswer'
        inquiry_ref:
          type: string
          nullable: true
        asked_by:
          type: string
        asked_at:
          type: string
          format: date-time
    BackofficeBusinessCredit:
      type: object
      description: routes/business_credit.rs `credit`, the platform’s specification
      required:
        - business
        - bureau_configured
        - suggested
        - checks
        - stands_days
      properties:
        business:
          type: object
          required:
            - id
            - legal_name
          properties:
            id:
              type: string
              format: uuid
            legal_name:
              type: string
            registered_address:
              type: string
              nullable: true
        bureau_configured:
          type: boolean
        suggested:
          $ref: '#/components/schemas/BusinessBureauSubject'
        checks:
          type: array
          items:
            $ref: '#/components/schemas/BusinessCreditCheck'
        stands_days:
          type: integer
    BackofficeTrialBalance:
      type: object
      description: routes/books.rs `trial_balance`, the platform’s specification
      required:
        - accounts
        - totals
        - entries_read
        - cut_short
      properties:
        to:
          type: string
          format: date
          nullable: true
        accounts:
          type: array
          items:
            type: object
            required:
              - id
              - code
              - name
              - kind
              - manual_entries
              - amounts
            properties:
              id:
                type: string
              code:
                type: string
              name:
                type: string
              kind:
                type: string
                enum:
                  - asset
                  - liability
                  - equity
                  - income
                  - expense
                  - other
              manual_entries:
                type: boolean
              amounts:
                type: array
                items:
                  type: object
                  required:
                    - currency
                    - debited_minor
                    - credited_minor
                    - balance_minor
                  properties:
                    currency:
                      type: string
                    debited_minor:
                      type: integer
                      format: int64
                    credited_minor:
                      type: integer
                      format: int64
                    balance_minor:
                      type: integer
                      format: int64
        totals:
          type: array
          items:
            type: object
            required:
              - currency
              - debited_minor
              - credited_minor
              - agrees
            properties:
              currency:
                type: string
              debited_minor:
                type: integer
                format: int64
              credited_minor:
                type: integer
                format: int64
              agrees:
                type: boolean
        entries_read:
          type: integer
        cut_short:
          type: boolean
          description: More than 10,000 entries; the rest were not read
    BackofficeJournal:
      type: object
      description: routes/books.rs `journal`, the platform’s specification
      required:
        - entries
        - cut_short
      properties:
        entries:
          type: array
          items:
            type: object
            required:
              - id
              - posted_at
              - lines
            properties:
              id:
                type: string
              posted_at:
                type: string
                format: date-time
              correlation_id:
                type: string
                nullable: true
              lines:
                type: array
                items:
                  type: object
                  required:
                    - gl
                    - direction
                    - currency
                  properties:
                    gl:
                      type: string
                    direction:
                      type: string
                      enum:
                        - debit
                        - credit
                    amount_minor:
                      type: integer
                      format: int64
                      nullable: true
                    currency:
                      type: string
        cut_short:
          type: boolean
    BackofficeClosures:
      type: object
      description: routes/books.rs `closures`, the platform’s specification
      required:
        - closures
      properties:
        closures:
          type: array
          items:
            type: object
            required:
              - id
              - closing_date
            properties:
              id:
                type: string
              closing_date:
                type: string
                format: date
              comments:
                type: string
                nullable: true
    BackofficeOverview:
      type: object
      description: routes/books.rs `overview`, the platform’s specification
      required:
        - customers
        - tills
        - approvals_waiting
        - tasks
      properties:
        customers:
          type: object
          required:
            - total
            - by_state
            - by_branch
          properties:
            total:
              type: integer
            by_state:
              type: object
              additionalProperties:
                type: integer
            by_branch:
              type: array
              items:
                type: object
                required:
                  - branch
                  - customers
                  - deposits
                properties:
                  branch:
                    type: string
                  customers:
                    type: integer
                  deposits:
                    type: object
                    description: Minor units by currency
                    additionalProperties:
                      type: integer
                      format: int64
        tills:
          type: array
          items:
            type: object
            required:
              - branch
              - name
              - open_tills
              - cash_minor
              - closed_over_short_minor
            properties:
              branch:
                type: string
              name:
                type: string
              open_tills:
                type: integer
              cash_minor:
                type: integer
                format: int64
              closed_over_short_minor:
                type: integer
                format: int64
        approvals_waiting:
          type: object
          required:
            - total
            - by_kind
          properties:
            total:
              type: integer
            by_kind:
              type: object
              additionalProperties:
                type: integer
        tasks:
          type: object
          required:
            - open
            - overdue
          properties:
            open:
              type: integer
            overdue:
              type: integer
    BackofficePortfolio:
      type: object
      description: routes/portfolio.rs `portfolio`, the platform’s specification
      required:
        - loans
        - stages
        - at_risk
        - credit_lines
      properties:
        loans:
          type: array
          items:
            type: object
            required:
              - id
              - customer
              - stage
              - requested_minor
              - currency
              - term_months
              - created_at
            properties:
              id:
                type: string
                format: uuid
              customer:
                type: string
              stage:
                $ref: '#/components/schemas/LoanStage'
              requested_minor:
                type: integer
              currency:
                type: string
              term_months:
                type: integer
              purpose:
                type: string
                nullable: true
              owed_minor:
                type: integer
                nullable: true
              arrears_minor:
                type: integer
                nullable: true
              days_past_due:
                type: integer
                nullable: true
              created_at:
                type: string
                format: date-time
        stages:
          type: array
          items:
            type: object
            required:
              - stage
              - count
            properties:
              stage:
                $ref: '#/components/schemas/LoanStage'
              count:
                type: integer
        at_risk:
          type: object
          required:
            - outstanding_minor
            - par1_minor
            - par30_minor
            - par90_minor
          properties:
            outstanding_minor:
              type: integer
            par1_minor:
              type: integer
            par30_minor:
              type: integer
            par90_minor:
              type: integer
            par1_bps:
              type: integer
              nullable: true
            par30_bps:
              type: integer
              nullable: true
            par90_bps:
              type: integer
              nullable: true
        credit_lines:
          type: object
          properties:
            states:
              type: object
              additionalProperties:
                type: integer
            open_limits_minor:
              type: integer
    LoanStage:
      type: string
      enum:
        - partial_application
        - pending_approval
        - approved
        - rejected
        - withdrawn
        - active
        - active_in_arrears
        - closed_obligations_met
        - written_off
    BackofficeLoan:
      type: object
      description: routes/portfolio.rs `loan`, the platform’s specification
      required:
        - id
        - customer
        - stage
        - state
        - requested_minor
        - currency
        - term_months
        - repayments
        - created_at
      properties:
        id:
          type: string
          format: uuid
        customer:
          type: string
        stage:
          $ref: '#/components/schemas/LoanStage'
        state:
          type: string
        requested_minor:
          type: integer
        currency:
          type: string
        term_months:
          type: integer
        purpose:
          type: string
          nullable: true
        declared_income_minor:
          type: integer
        declared_obligations_minor:
          type: integer
        decision:
          type: object
          nullable: true
          additionalProperties: true
        operator_decision:
          type: object
          nullable: true
          additionalProperties: true
        offer:
          type: object
          nullable: true
          additionalProperties: true
        disbursed_on:
          type: string
          format: date
          nullable: true
        instalments:
          type: array
          nullable: true
          items:
            type: object
            properties:
              number:
                type: integer
              due_on:
                type: string
                format: date
              due_minor:
                type: integer
              paid_minor:
                type: integer
              cleared_on:
                type: string
                format: date
                nullable: true
        position:
          type: object
          nullable: true
          properties:
            owed_minor:
              type: integer
            received_minor:
              type: integer
            arrears_minor:
              type: integer
            days_past_due:
              type: integer
            settled:
              type: boolean
        repayments:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              amount_minor:
                type: integer
              received_on:
                type: string
                format: date
              recorded_by:
                type: string
              reversed:
                type: boolean
              reversal_of:
                type: string
                format: uuid
                nullable: true
        written_off:
          type: object
          nullable: true
          properties:
            owed_minor:
              type: integer
            reason:
              type: string
            at:
              type: string
              format: date-time
        created_at:
          type: string
          format: date-time
    BackofficeTask:
      type: object
      required:
        - id
        - title
        - assignee
        - state
        - created_by
        - created_at
      properties:
        id:
          type: string
          format: uuid
        customer:
          type: string
          nullable: true
        title:
          type: string
        detail:
          type: string
          nullable: true
        due_on:
          type: string
          format: date
          nullable: true
        assignee:
          type: string
        state:
          type: string
          enum:
            - open
            - done
        created_by:
          type: string
        created_at:
          type: string
          format: date-time
        done_by:
          type: string
          nullable: true
        done_at:
          type: string
          format: date-time
          nullable: true
    BackofficeBranch:
      type: object
      required:
        - id
        - code
        - name
        - city
        - currency
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
        name:
          type: string
        city:
          type: string
        currency:
          type: string
    BackofficeTill:
      type: object
      description: routes/tills.rs `till_view`, the platform’s specification
      required:
        - id
        - code
        - branch_id
        - teller
        - currency
        - state
        - opened_by
        - opened_at
        - opening_minor
        - expected_minor
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
        branch_id:
          type: string
          format: uuid
        teller:
          type: string
        currency:
          type: string
        state:
          type: string
          enum:
            - open
            - closed
        opened_by:
          type: string
        opened_at:
          type: string
          format: date-time
        opening_minor:
          type: integer
        expected_minor:
          type: integer
          nullable: true
          description: What the drawer should hold
        closed_by:
          type: string
          nullable: true
        closed_at:
          type: string
          format: date-time
          nullable: true
        counted_minor:
          type: integer
          nullable: true
        difference_minor:
          type: integer
          nullable: true
          description: 'Counted less expected: positive over, negative short'
        close_note:
          type: string
          nullable: true
        entries:
          type: array
          items:
            type: object
            required:
              - id
              - kind
              - amount_minor
              - authority
              - by
              - at
            properties:
              id:
                type: string
                format: uuid
              kind:
                type: string
                enum:
                  - cash_in
                  - cash_out
                  - buy
                  - sell
              amount_minor:
                type: integer
                description: What the drawer gained
              customer:
                type: string
                nullable: true
              account:
                type: string
                nullable: true
              authority:
                type: string
                enum:
                  - teller
                  - approval
                  - vault
              approval_id:
                type: string
                format: uuid
                nullable: true
              core_ref:
                type: string
                nullable: true
              by:
                type: string
              at:
                type: string
                format: date-time
    BackofficeTills:
      type: object
      required:
        - branch_id
        - tills
        - tellers
      properties:
        branch_id:
          type: string
          format: uuid
        tills:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeTill'
        tellers:
          type: array
          items:
            type: object
            required:
              - subject
              - label
              - has_open_till
            properties:
              subject:
                type: string
              label:
                type: string
                nullable: true
              has_open_till:
                type: boolean
    BackofficeRole:
      type: object
      required:
        - role
        - title
        - description
        - permissions
        - branch_scoped
        - posting_limit_minor
        - decides
      properties:
        role:
          type: string
          enum:
            - teller
            - head_teller
            - branch_manager
            - service_agent
            - operations
            - credit_officer
            - credit_committee
            - compliance
            - accountant
            - supervisor
            - auditor
            - iam_admin
        title:
          type: string
        description:
          type: string
        permissions:
          type: array
          items:
            type: string
        branch_scoped:
          type: boolean
          description: Sees its branch's customers only
        posting_limit_minor:
          type: integer
          description: Cash a teller posts on their own; 0 for a role that does not
        decides:
          type: array
          description: The authority matrix
          items:
            type: object
            required:
              - domain
              - limit_minor
            properties:
              domain:
                type: string
                enum:
                  - money
                  - lending
                  - risk
                  - onboarding
                  - cards
                  - teller
                  - books
                  - accounts
                  - clients
                  - settings
                  - alerts
                  - iam
              limit_minor:
                type: integer
                nullable: true
                description: Null is without limit
    BackofficeMe:
      type: object
      description: routes/iam.rs `me`, the platform’s specification
      required:
        - subject
        - name
        - roles
        - permissions
        - posting_limit_minor
        - branches
        - all_branches
      properties:
        subject:
          type: string
        name:
          type: string
          nullable: true
        roles:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeRole'
        posting_limit_minor:
          type: integer
        branches:
          type: array
          items:
            type: object
            required:
              - id
              - code
              - name
            properties:
              id:
                type: string
                format: uuid
              code:
                type: string
              name:
                type: string
        all_branches:
          type: boolean
          description: Head office — sees every branch
        permissions:
          type: array
          items:
            type: string
          description: >-
            signin, customers.read, teller.act, tills.own, tills.manage, tills.close, accounts.manage,
            clients.manage, notes.write, tasks.work, cards.freeze, cards.manage, support.handle, sessions.end,
            onboarding.review, lending.manage, risk.review, money.ops, operations.run, approvals.read,
            approvals.decide, audit.read, iam.manage, alerts.work, alerts.rules.manage, read.all
      example:
        subject: staff-teller
        name: Tomás Herrera
        roles:
          - role: teller
            title: Teller
            description: Serves customers at the desk
            permissions:
              - signin
              - customers.read
              - teller.act
              - tills.own
              - cards.freeze
              - approvals.read
            branch_scoped: true
            posting_limit_minor: 1000000
            decides: []
        permissions:
          - signin
          - customers.read
          - teller.act
          - cards.freeze
          - approvals.read
        posting_limit_minor: 1000000
        branches:
          - id: 0199d000-0000-7000-8000-0000000000b1
            code: MEX-01
            name: Reforma
        all_branches: false
    BackofficeIamStaff:
      type: object
      description: routes/iam.rs `staff`, the platform’s specification
      required:
        - staff
        - roles
        - waiting
        - branches
      properties:
        staff:
          type: array
          items:
            type: object
            required:
              - subject
              - label
              - roles
              - branches
            properties:
              subject:
                type: string
              label:
                type: string
                nullable: true
              roles:
                type: array
                items:
                  type: string
              branches:
                type: array
                items:
                  type: string
                description: Branch codes they work
        roles:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeRole'
        branches:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              code:
                type: string
              name:
                type: string
        waiting:
          type: array
          items:
            type: object
            properties:
              approval_id:
                type: string
                format: uuid
              proposed_by:
                type: string
              subject:
                type: string
              role:
                type: string
              grant:
                type: boolean
              reason:
                type: string
    BackofficeCustomers:
      type: object
      description: routes/customers.rs `find`, the platform’s specification
      required:
        - customers
        - term
      properties:
        term:
          type: string
        customers:
          type: array
          items:
            type: object
            required:
              - subject
              - name
              - client_id
              - matched_by
            properties:
              subject:
                type: string
              name:
                type: string
                nullable: true
              client_id:
                type: string
                nullable: true
                description: The core's client number
              matched_by:
                type: string
                enum:
                  - name
                  - subject
                  - client
                  - account
                  - card
                  - all
    BackofficeCustomer:
      type: object
      description: routes/customers.rs `profile`, the platform’s specification
      required:
        - subject
        - name
        - client_id
        - onboarding
        - identity
        - screening
        - accounts
        - credit_line
        - loans
        - cards
        - transfers
        - crypto
        - disputes
        - support
        - sessions
        - businesses
        - activity
        - teller
        - unavailable
      properties:
        subject:
          type: string
        name:
          type: string
          nullable: true
        client_id:
          type: string
          nullable: true
        onboarding:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            declared_country:
              type: string
            declared_age_years:
              type: integer
            expected_monthly_minor:
              type: integer
            ineligible_reason:
              type: string
              nullable: true
            own_account:
              type: boolean
            created_at:
              type: string
              format: date-time
        identity:
          type: object
          nullable: true
          description: What the document said, unsealed for an operator
          properties:
            document_type:
              type: string
            liveness_passed:
              type: boolean
            mismatches:
              type: array
              items:
                type: string
            recorded_at:
              type: string
              format: date-time
            details:
              type: object
              nullable: true
              additionalProperties: true
        screening:
          type: array
          items:
            type: object
            properties:
              match_key:
                type: string
              verdict:
                type: string
              decided_by:
                type: string
              decided_at:
                type: string
                format: date-time
              note:
                type: string
        accounts:
          type: array
          items:
            type: object
            required:
              - id
              - name
              - kind
              - currency
              - balance_minor
            properties:
              id:
                type: string
              name:
                type: string
              kind:
                type: string
              currency:
                type: string
              balance_minor:
                type: integer
                nullable: true
              business:
                type: string
                nullable: true
                description: The company it is held for
              standing:
                type: object
                nullable: true
                description: 'Spec 068: where it stands beyond its balance, as the core keeps it'
                properties:
                  state:
                    type: string
                    enum:
                      - active
                      - closed
                      - submitted
                      - approved
                      - other
                  blocked:
                    type: boolean
                  debits_blocked:
                    type: boolean
                  credits_blocked:
                    type: boolean
                  dormant:
                    type: boolean
                  currency:
                    type: string
                  balance_minor:
                    type: integer
                    nullable: true
                  available_minor:
                    type: integer
                    nullable: true
                  on_hold_minor:
                    type: integer
                  holds:
                    type: array
                    items:
                      type: object
                      properties:
                        hold_ref:
                          type: string
                        amount_minor:
                          type: integer
                        reason:
                          type: string
                          nullable: true
                        'on':
                          type: string
                          format: date
                          nullable: true
        credit_line:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            state:
              type: string
            limit_minor:
              type: integer
            asked_minor:
              type: integer
            currency:
              type: string
            annual_rate:
              type: string
            term_months:
              type: integer
            account_id:
              type: string
            refused_reason:
              type: string
              nullable: true
            closed_reason:
              type: string
              nullable: true
            created_at:
              type: string
              format: date-time
        loans:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              state:
                type: string
              requested_minor:
                type: integer
              currency:
                type: string
              term_months:
                type: integer
              purpose:
                type: string
                nullable: true
              core_loan_ref:
                type: string
                nullable: true
              created_at:
                type: string
                format: date-time
        cards:
          type: array
          items:
            type: object
            required:
              - id
              - last_four
              - network
              - status
              - expires_on
              - currency
              - limits
            properties:
              id:
                type: string
                format: uuid
              last_four:
                type: string
              network:
                type: string
              status:
                type: string
                enum:
                  - requested
                  - issued
                  - active
                  - frozen
                  - cancelled
                  - expired
              expires_on:
                type: string
                format: date
              currency:
                type: string
              account_ref:
                type: string
              limits:
                type: object
                properties:
                  per_transaction:
                    type: integer
                    nullable: true
                  daily:
                    type: integer
                    nullable: true
                  monthly:
                    type: integer
                    nullable: true
              business_id:
                type: string
                format: uuid
                nullable: true
        transfers:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              from_account:
                type: string
              payee:
                type: string
              amount_minor:
                type: integer
              currency:
                type: string
              state:
                type: string
              refused_reason:
                type: string
                nullable: true
              created_at:
                type: string
                format: date-time
        crypto:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              network:
                type: string
              token:
                type: string
              stage:
                type: string
              credited_minor:
                type: integer
                nullable: true
              settles_in:
                type: string
              created_at:
                type: string
                format: date-time
        disputes:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              amount_minor:
                type: integer
              currency:
                type: string
              reason:
                type: string
              stage:
                type: string
              outcome:
                type: string
                nullable: true
              raised_at:
                type: string
                format: date-time
        support:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              topic:
                type: string
                nullable: true
              resolved:
                type: boolean
              created_at:
                type: string
                format: date-time
        sessions:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              device:
                type: string
              os:
                type: string
              app_version:
                type: string
              began_at:
                type: string
                format: date-time
              last_used_at:
                type: string
                format: date-time
        businesses:
          type: array
          items:
            type: object
            properties:
              business_id:
                type: string
                format: uuid
              name:
                type: string
              role:
                type: string
        activity:
          type: array
          description: Approvals whose proposal names this customer, newest first
          items:
            type: object
            required:
              - approval_id
              - kind
              - state
              - proposed_by
              - proposed_at
            properties:
              approval_id:
                type: string
                format: uuid
              kind:
                type: string
              state:
                type: string
              proposed_by:
                type: string
              decided_by:
                type: string
                nullable: true
              proposed_at:
                type: string
                format: date-time
              decided_at:
                type: string
                format: date-time
                nullable: true
              reason:
                type: string
                nullable: true
        state:
          type: object
          nullable: true
          description: 'Spec 068: active, inactive, exited or blacklisted, and why'
          properties:
            state:
              type: string
              enum:
                - active
                - inactive
                - exited
                - blacklisted
            reason:
              type: string
              nullable: true
            changed_at:
              type: string
              format: date-time
              nullable: true
        notes:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              author:
                type: string
              body:
                type: string
              created_at:
                type: string
                format: date-time
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeTask'
        branch:
          type: object
          nullable: true
          description: Their home branch
          properties:
            id:
              type: string
              format: uuid
            code:
              type: string
            name:
              type: string
        teller:
          type: object
          required:
            - takes_cash
          properties:
            takes_cash:
              type: boolean
              description: Whether the bank has a till account for cash
        unavailable:
          type: array
          items:
            type: string
          description: The parts that could not be read this time
    BackofficeCustomerMovements:
      type: object
      description: routes/customers.rs `movements`, the platform’s specification
      required:
        - account
        - movements
      properties:
        account:
          type: object
          required:
            - id
            - name
            - currency
            - balance_minor
          properties:
            id:
              type: string
            name:
              type: string
            currency:
              type: string
            balance_minor:
              type: integer
              nullable: true
        movements:
          type: array
          items:
            type: object
            required:
              - id
              - posted_on
              - amount_minor
              - currency
            properties:
              id:
                type: string
              posted_on:
                type: string
                format: date
              amount_minor:
                type: integer
                description: Negative is money out of the account
              currency:
                type: string
              reference:
                type: string
                nullable: true
    BackofficeMoneyProposed:
      type: object
      description: >
        202 body of the proposing routes that return only the approval id: POST /backoffice/loans/repayments
        (lending::propose_repayment) and POST /backoffice/exchange/completions (exchange::propose_completion).
        Two separate json! literals of the same shape. Nothing has happened yet; the approval is `proposed`
        and a different person decides it at POST /approvals/{id}/decision.
      required:
        - approval_id
        - state
      properties:
        approval_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
    BackofficeMoneyProposedNoted:
      type: object
      description: >
        202 body of POST /backoffice/screening/adjudications, /backoffice/transfers/{id}/resolution,
        /backoffice/funding/{id}/credit and /backoffice/cards/limits. Four separate json! literals of the same
        shape; only the fixed `note` sentence differs per route.
      required:
        - approval_id
        - state
        - note
      properties:
        approval_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
        note:
          type: string
          description: One fixed sentence per route saying nothing has changed yet (prose, not an enum).
    BackofficeMoneyReversalProposed:
      type: object
      description: 202 body of POST /backoffice/loans/{id}/repayments/{repayment_id}/reverse.
      required:
        - approval_id
        - state
        - reversal_of
      properties:
        approval_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
        reversal_of:
          type: string
          format: uuid
          description: The repayment row the reversal will point at (the path's repayment_id).
    BackofficeMoneyRebalanceProposed:
      type: object
      description: 202 body of POST /backoffice/investments/rebalances.
      required:
        - approval_id
        - state
        - orders
        - note
      properties:
        approval_id:
          type: string
          format: uuid
        state:
          type: string
          enum:
            - proposed
        orders:
          type: integer
          minimum: 1
          description: >
            A COUNT (planned.orders.len()), not the orders themselves. The orders are on the approval's
            subject. At least 1 in practice, since a plan within its band is refused (409).
        note:
          type: string
          description: Fixed sentence ("no orders have been placed; a different person has to agree").
    BackofficeRiskScreeningListKind:
      type: string
      enum:
        - sanctions
        - pep
        - adverse_media
        - internal_deny_list
        - law_enforcement
      description: screening::ListKind::label(). A source kind the route does not recognise is stored as sanctions.
    BackofficeRiskScreeningBand:
      type: string
      enum:
        - weak
        - possible
        - strong
      description: screening::MatchBand::label().
    BackofficeRiskScreeningReason:
      type: object
      description: >
        screening::MatchReason. Codes the module writes: name_similarity, dob_exact, yob_same, dob_conflict,
        dob_conflict_discounted, dob_unknown, country_same, identifier_exact (listed for reading; kept a
        string because the list lives in modules/screening/src/matching.rs).
      required:
        - code
        - detail
      properties:
        code:
          type: string
        detail:
          type: string
    BackofficeRiskScreeningAlert:
      type: object
      description: >
        routes/screening.rs `alert_view(&StoredMatch)`. An open alert: a queued match on the subject's latest
        run with no confirmation and no clearance against its current revision.
      required:
        - key
        - list
        - kind
        - entry_id
        - revision
        - matched_name
        - score
        - band
        - identifier_match
        - previously_cleared
        - reasons
      properties:
        key:
          type: string
          description: '`subject|list|entry_id`'
        list:
          type: string
        kind:
          $ref: '#/components/schemas/BackofficeRiskScreeningListKind'
        entry_id:
          type: string
        revision:
          type: string
          description: The entry revision compared; an adjudication must send exactly this.
        matched_name:
          type: string
        score:
          type: number
          format: double
          minimum: 0
          maximum: 1
        band:
          $ref: '#/components/schemas/BackofficeRiskScreeningBand'
        identifier_match:
          type: boolean
        previously_cleared:
          type: string
          nullable: true
          description: The older revision a clearance was given against, when a clearance lapsed; else null.
        reasons:
          type: array
          description: The stored jsonb, written by the route as [{code, detail}]. Never empty.
          items:
            $ref: '#/components/schemas/BackofficeRiskScreeningReason'
        source:
          type: string
          enum:
            - lists
            - service
          description: >
            Spec 073. `lists` for the bank's own, matched locally; `service` for a provider's hit, whose
            reasons are code `provider` (the provider's words) or `provider_unexplained`.
        provider:
          type: string
          nullable: true
          description: The provider that reported a `service` hit; null for the bank's own lists.
        provider_reference:
          type: string
          nullable: true
          description: The provider's own reference for the answer, for raising a query with it.
        source_ref:
          type: string
          nullable: true
          description: Spec 077. The list's own reference for the entry, OFAC's `ent_num`; null where the list has none.
    BackofficeAlert:
      type: object
      description: Spec 077. One alert, as the queue lists it.
      required:
        - id
        - code
        - rule_version
        - severity
        - message
        - role
        - weight
        - subject
        - customer
        - group_key
        - state
        - occurrences
        - first_at
        - last_at
        - provenance
        - delivery
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
          description: The rule's id
        rule_version:
          type: integer
          minimum: 1
        severity:
          type: string
          enum:
            - info
            - low
            - medium
            - high
            - critical
        message:
          type: string
          description: The rule's message with the event's declared fields
        role:
          type: string
        weight:
          type: integer
          minimum: 1
        subject:
          type: object
          required:
            - kind
            - ref
            - href
          properties:
            kind:
              type: string
              description: transfer, screening_run, fraud_assessment, loan, dispute
            ref:
              type: string
            href:
              type: string
              nullable: true
              description: The back office page of the object
              where it has one: null
        customer:
          type: string
          nullable: true
          description: The customer
          for a rule grouped by customer: null
        group_key:
          type: string
        state:
          type: string
          enum:
            - open
            - acknowledged
            - resolved
            - cleared
        occurrences:
          type: integer
          minimum: 1
        first_at:
          type: string
          format: date-time
        last_at:
          type: string
          format: date-time
        provenance:
          type: string
          enum:
            - live
            - synthetic
            - unavailable
        delivery:
          type: string
          enum:
            - none
            - pending
            - sent
            - undelivered
            - unavailable
    BackofficeAlertDetail:
      allOf:
        - $ref: '#/components/schemas/BackofficeAlert'
        - type: object
          required:
            - event
            - occurrences_at
            - deliveries
            - acknowledged_by
            - acknowledged_at
            - resolved_by
            - resolved_at
            - resolution_note
            - cleared_by_event
          properties:
            event:
              type: object
              nullable: true
              required:
                - id
                - kind
                - occurred_at
                - fields
              properties:
                id:
                  type: string
                  format: uuid
                kind:
                  type: string
                occurred_at:
                  type: string
                  format: date-time
                  nullable: true
                fields:
                  type: object
                  additionalProperties: true
                  description: Only the catalogue's declared fields
            occurrences_at:
              type: array
              items:
                type: object
                required:
                  - event_id
                  - at
                  - object
                properties:
                  event_id:
                    type: string
                    format: uuid
                  at:
                    type: string
                    format: date-time
                  object:
                    type: object
                    required:
                      - kind
                      - ref
                      - href
                    properties:
                      kind:
                        type: string
                      ref:
                        type: string
                      href:
                        type: string
                        nullable: true
            deliveries:
              type: array
              items:
                type: object
                required:
                  - recipient
                  - outcome
                  - reason
                  - tries
                properties:
                  recipient:
                    type: string
                  outcome:
                    type: string
                    enum:
                      - pending
                      - sent
                      - failed
                      - unavailable
                  reason:
                    type: string
                    nullable: true
                  tries:
                    type: integer
                    minimum: 0
            acknowledged_by:
              type: string
              nullable: true
            acknowledged_at:
              type: string
              format: date-time
              nullable: true
            resolved_by:
              type: string
              nullable: true
            resolved_at:
              type: string
              format: date-time
              nullable: true
            resolution_note:
              type: string
              nullable: true
            cleared_by_event:
              type: string
              format: uuid
              nullable: true
            raised_at:
              type: string
              format: date-time
              description: When the evaluator raised it; SC-7704 measures the gap from the event
    AlertCondition:
      type: object
      required:
        - event
        - field
        - op
        - threshold
      properties:
        event:
          type: string
        field:
          type: string
        op:
          type: string
          enum:
            - <
            - <=
            - '>'
            - '>='
            - '='
            - '!='
            - in
        threshold:
          description: A number, a word, a list for in, or {amount_minor, currency} for money
    AlertRule:
      type: object
      description: Spec 077, research R5. One rule, in the catalogue's own shape.
      required:
        - id
        - name
        - event
        - field
        - op
        - threshold
        - severity
        - role
        - message
        - cooldown_minutes
        - weight
        - group_by
      properties:
        id:
          type: string
          description: Lower-case words joined by hyphens
        name:
          type: string
        event:
          type: string
        field:
          type: string
        op:
          type: string
          enum:
            - <
            - <=
            - '>'
            - '>='
            - '='
            - '!='
            - in
        threshold:
          description: A number, a word, a list for in, or {amount_minor, currency} for money
        severity:
          type: string
          enum:
            - info
            - low
            - medium
            - high
            - critical
        role:
          type: string
        message:
          type: string
        cooldown_minutes:
          type: integer
          minimum: 0
          maximum: 10080
        weight:
          type: integer
          minimum: 1
          maximum: 10
        group_by:
          type: string
          enum:
            - customer
            - object
        email_from:
          type: string
          nullable: true
          enum:
            - info
            - low
            - medium
            - high
            - critical
        clears_on:
          allOf:
            - $ref: '#/components/schemas/AlertCondition'
          nullable: true
    BackofficeAlertRules:
      type: object
      required:
        - rules
        - mailboxes
      properties:
        rules:
          type: array
          items:
            type: object
            required:
              - rule
              - version
              - state
              - fired
              - not_evaluated
              - cleared
              - change_waiting
              - created_by
              - created_at
            properties:
              rule:
                $ref: '#/components/schemas/AlertRule'
              version:
                type: integer
                minimum: 1
              state:
                type: string
                enum:
                  - active
                  - 'off'
              fired:
                type: integer
                minimum: 0
              not_evaluated:
                type: integer
                minimum: 0
              cleared:
                type: integer
                minimum: 0
              change_waiting:
                type: boolean
              created_by:
                type: string
              created_at:
                type: string
                format: date-time
        mailboxes:
          type: array
          items:
            type: object
            required:
              - role
              - address
            properties:
              role:
                type: string
              address:
                type: string
    BackofficeScreeningList:
      type: object
      description: Spec 077. One declared list, as screening_lists' load report gives it.
      required:
        - list
        - kind
        - format
        - version
        - provenance
        - entries
        - aliases
        - state
        - skipped_rows
        - skipped_total
        - unmapped_countries
        - missing_files
        - error
        - read_at
      properties:
        list:
          type: string
        kind:
          type: string
        format:
          type: string
          enum:
            - json
            - ofac_csv
        version:
          type: string
          nullable: true
          description: Null while no version is in use
        provenance:
          type: string
          nullable: true
          enum:
            - live
            - synthetic
            - unavailable
        entries:
          type: integer
          minimum: 0
        aliases:
          type: integer
          minimum: 0
        state:
          type: string
          enum:
            - current
            - partial
            - stale
            - failed
        skipped_rows:
          type: array
          items:
            type: string
          description: file:line of the first 20 rows skipped
        skipped_total:
          type: integer
          minimum: 0
        unmapped_countries:
          type: array
          items:
            type: string
        missing_files:
          type: array
          items:
            type: string
        error:
          type: string
          nullable: true
          description: Why the last read failed
        read_at:
          type: string
          format: date-time
          nullable: true
    BackofficeRiskScreeningQueue:
      type: object
      description: GET /backoffice/screening. Open alerts, score descending then key, at most 200.
      required:
        - count
        - alerts
      properties:
        count:
          type: integer
          minimum: 0
        alerts:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeRiskScreeningAlert'
    BackofficeRiskScreeningDecision:
      type: object
      description: One row of screening_decisions, as GET /backoffice/screening/alert lists it.
      required:
        - verdict
        - entry_revision
        - decided_by
        - decided_at
        - note
      properties:
        verdict:
          type: string
          enum:
            - cleared
            - confirmed
        entry_revision:
          type: string
        decided_by:
          type: string
        decided_at:
          type: string
          format: date-time
        note:
          type: string
    BackofficeRiskScreeningAlertDetail:
      type: object
      description: >
        GET /backoffice/screening/alert?key=. ALWAYS 200: an unknown or already-decided key answers {key,
        alert: null, history: [...]} (history may be empty); the handler never returns 404.
      required:
        - key
        - alert
        - history
      properties:
        key:
          type: string
          description: The query key echoed back.
        alert:
          allOf:
            - $ref: '#/components/schemas/BackofficeRiskScreeningAlert'
          nullable: true
          description: >
            The alert while it is open; null once decided, or when the key never existed. Looked up in the
            first 500 open alerts only.
        history:
          type: array
          description: Every decision ever recorded against this key, oldest decided_at first.
          items:
            $ref: '#/components/schemas/BackofficeRiskScreeningDecision'
        run:
          type: object
          nullable: true
          description: >
            Spec 073. The run that raised the open alert: `id`, `screened_at`, `list_versions` (the bank's
            own), `service_state` (`not_asked`, `answered`, `unavailable`, `closed_local_only`),
            `service_provider`, and `service_list_versions`, null when the provider did not say which lists it
            used. Spec 077 adds `narrowing` (`narrowed`, `full`), `narrowing_reason` (`too_common`,
            `too_short`, `no_strong_token`, or null), `missing_lists` and `lists_state` (`complete`,
            `waiting`, `closed_loaded_only`). Null when there is no open alert.
        proposal:
          allOf:
            - $ref: '#/components/schemas/Proposal'
          nullable: true
          description: >
            What automated judgement proposes about the open alert — the rule-based proposer until an AI plane
            exists. Asked once per alert and entry revision, and recorded before it is shown; null when there
            is no open alert or the proposal could not be recorded. Never a decision: the officer's
            adjudication still goes through a second person. Spec 073 adds `model_version` (null for the
            rules) and `trained_on`: `synthetic` for the image's model, or `{bank_decisions, synthetic,
            synthetic_weight_share}` for the bank's own.
    BackofficeRiskScreeningMatch:
      type: object
      description: One screening::Match as POST /backoffice/screening/rescreen renders it.
      required:
        - key
        - list
        - kind
        - entry_id
        - revision
        - matched_name
        - score
        - band
        - identifier_match
        - previously_cleared
        - reasons
      properties:
        key:
          type: string
          description: '`subject|list|entry_id`'
        list:
          type: string
        kind:
          $ref: '#/components/schemas/BackofficeRiskScreeningListKind'
        entry_id:
          type: string
        revision:
          type: string
        matched_name:
          type: string
        score:
          type: number
          format: double
          minimum: 0
          maximum: 1
        band:
          $ref: '#/components/schemas/BackofficeRiskScreeningBand'
        identifier_match:
          type: boolean
        previously_cleared:
          type: string
          nullable: true
        reasons:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeRiskScreeningReason'
    BackofficeRiskScreeningRun:
      type: object
      description: >
        200 body of POST /backoffice/screening/rescreen: the screening::Run just filed. Includes matches that
        were neither queued nor suppressed (everything at or above the recording threshold), strongest first.
      required:
        - subject_reference
        - screened_at
        - outcome
        - requires_enhanced_diligence
        - list_versions
        - queued
        - suppressed
        - matches
      properties:
        subject_reference:
          type: string
        screened_at:
          type: string
          format: date-time
        outcome:
          type: string
          enum:
            - clear
            - pending
            - blocked
          description: screening::RunOutcome::label().
        requires_enhanced_diligence:
          type: boolean
        list_versions:
          type: array
          description: |
            Vec<(String, String)> serialised by serde as 2-element arrays: [[list, version],...], NOT objects.
          items:
            type: array
            minItems: 2
            maxItems: 2
            items:
              type: string
        queued:
          type: array
          description: Match keys a person has to look at (they become alerts).
          items:
            type: string
        suppressed:
          type: array
          description: Match keys that would have queued but were already cleared against this revision.
          items:
            type: string
        matches:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeRiskScreeningMatch'
    BackofficeRiskHeldTransfer:
      type: object
      required:
        - id
        - customer
        - amount_minor
        - currency
        - payee
        - held_since
        - assessment_id
      properties:
        id:
          type: string
          format: uuid
        customer:
          type: string
          description: The customer's token subject (transfer_intents.subject).
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
        payee:
          type: string
          description: The payee's display name (payee_display), not an object.
        held_since:
          type: string
          format: date-time
          nullable: true
          description: authorized_at (Option in the store; set in practice for a held intent).
        assessment_id:
          type: string
          format: uuid
          nullable: true
          description: The fraud assessment that held it; GET /backoffice/fraud/{id} reads it.
    BackofficeRiskHeldTransferQueue:
      type: object
      description: >
        GET /backoffice/transfers. Intents in state `held`, ordered by authorized_at then created_at (longest
        waiting first), at most 200.
      required:
        - count
        - waiting_longest_since
        - transfers
      properties:
        count:
          type: integer
          minimum: 0
        waiting_longest_since:
          type: string
          format: date-time
          nullable: true
          description: authorized_at of the first row; null when the queue is empty.
        transfers:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeRiskHeldTransfer'
    BackofficeMoneyHoldsExpired:
      type: object
      description: >
        200 body of POST /backoffice/cards/holds/expire. The card_holds rows of this tenant whose placed_on is
        more than 7 days before today are DELETED (not marked).
      required:
        - released
        - released_minor
        - older_than_days
      properties:
        released:
          type: integer
          format: int64
          minimum: 0
          description: Number of holds removed.
        released_minor:
          type: integer
          format: int64
          minimum: 0
          description: >
            SUM(amount_minor) of the removed holds, 0 when none. Summed across all cards and currencies
            without regard to currency.
        older_than_days:
          type: integer
          enum:
            - 7
          description: The fixed MAX_AGE_DAYS constant.
    BackofficeMoneyExchangeQuote:
      description: >
        One item of GET /backoffice/exchange: routes/exchange.rs `view(&StoredQuote)` (the SAME fn that builds
        ExchangeQuoteView for GET /exchange/quotes) with one extra key, `subject`.
      allOf:
        - $ref: '#/components/schemas/ExchangeQuoteView'
        - type: object
          required:
            - subject
          properties:
            subject:
              type: string
              description: The customer's token subject; only the back office sees it.
    BackofficeMoneyExchangeQueue:
      type: object
      description: GET /backoffice/exchange. Tenant-wide quotes, newest created_at first, at most 100.
      required:
        - quotes
      properties:
        quotes:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeMoneyExchangeQuote'
    BackofficeMoneyInvestmentAccountError:
      type: object
      description: >
        What GET /backoffice/investments lists in place of an account account_view() cannot render (a retired
        model, or a drift/plan error), instead of omitting it.
      required:
        - id
        - model
        - error
      properties:
        id:
          type: string
          format: uuid
        model:
          type: string
          description: The stored model_name as-is (NOT limited to cautious|balanced|adventurous here).
        error:
          type: string
          description: e.g. "this account is on a model we no longer offer", or the module's error text.
    BackofficeMoneyInvestmentQueue:
      type: object
      description: GET /backoffice/investments. Tenant's accounts, newest created_at first, at most 200.
      required:
        - count
        - accounts
      properties:
        count:
          type: integer
          minimum: 0
          description: Equals accounts.length (errors included).
        accounts:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/InvestmentAccountView'
              - $ref: '#/components/schemas/BackofficeMoneyInvestmentAccountError'
    BackofficeMoneyDisputeEvidence:
      type: object
      description: One disputes evidence row, as the customer wrote it.
      required:
        - kind
        - reference
        - supplied_at
      properties:
        kind:
          type: string
          enum:
            - order_detail
            - cancellation_proof
            - agreed_amount
            - merchant_contacted
          description: Only written after Evidence::from_label succeeds, so always one of these.
        reference:
          type: string
          description: The customer's own text (trimmed, 1..2000 chars when supplied).
        supplied_at:
          type: string
          format: date-time
    BackofficeMoneyDisputeCase:
      type: object
      description: >
        routes/disputes.rs `operator_view()` for a row this build can read: customer_view() (the Rust value
        behind DisputeCaseView) with `your_moves` REMOVED and customer, evidence, provisional_approval_id,
        settlement_approval_id and moves added. Not an allOf of DisputeCaseView because that schema requires
        `your_moves`, which this view never carries.
      required:
        - id
        - payment_kind
        - payment_ref
        - amount_minor
        - currency
        - reason
        - reason_prompt
        - stage
        - next_step
        - deadline
        - outstanding
        - provisional_credit_proposed
        - outcome
        - raised_at
        - customer
        - evidence
        - provisional_approval_id
        - settlement_approval_id
        - moves
      properties:
        id:
          type: string
          format: uuid
        payment_kind:
          type: string
          enum:
            - card
            - transfer
        payment_ref:
          type: string
          description: Card authorisation id, or transfer intent id (uuid).
        amount_minor:
          type: integer
          format: int64
        currency:
          type: string
        reason:
          type: string
          enum:
            - not_recognised
            - not_received
            - not_as_described
            - cancelled
            - duplicate
            - wrong_amount
        reason_prompt:
          type: string
        stage:
          type: string
          enum:
            - raised
            - evidence_required
            - with_the_merchant
            - decided
            - withdrawn
          description: >
            In the queue only raised|evidence_required|with_the_merchant. After a `decide` move it is
            `decided`; after `send_to_merchant`, `with_the_merchant`.
        next_step:
          type: string
        deadline:
          type: string
          format: date-time
          nullable: true
          description: raised_at + 30 days, only while with_the_merchant.
        outstanding:
          type: array
          items:
            $ref: '#/components/schemas/DisputeEvidenceAsk'
        provisional_credit_proposed:
          type: boolean
        outcome:
          type: string
          nullable: true
          enum:
            - upheld
            - declined
            - null
        raised_at:
          type: string
          format: date-time
        customer:
          type: string
          description: The customer's token subject.
        evidence:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeMoneyDisputeEvidence'
        provisional_approval_id:
          type: string
          format: uuid
          nullable: true
          description: The `dispute_refund` approval for the provisional credit, when one was raised.
        settlement_approval_id:
          type: string
          format: uuid
          nullable: true
          description: >
            The `dispute_refund` approval raised by a decision (refund on upheld, reverse_provisional on
            declined); null when the decision moves no money or the case is undecided.
        moves:
          type: array
          description: >
            moves_for(stage, Operator): raised -> [send_to_merchant, decide]; evidence_required and
            with_the_merchant -> [decide]; decided and withdrawn -> [].
          items:
            type: string
            enum:
              - send_to_merchant
              - decide
    BackofficeMoneyDisputeUnreadable:
      type: object
      description: >
        operator_view() for a stored row whose reason or stage this build does not know: customer_view()'s
        unreadable stub plus the operator keys. `moves` is [] when the stage itself is unknown, else the
        stage's operator moves.
      required:
        - id
        - stage
        - unreadable
        - customer
        - evidence
        - provisional_approval_id
        - settlement_approval_id
        - moves
      properties:
        id:
          type: string
          format: uuid
        stage:
          type: string
          description: The stored stage text.
        unreadable:
          type: boolean
          enum:
            - true
        customer:
          type: string
        evidence:
          type: array
          items:
            $ref: '#/components/schemas/BackofficeMoneyDisputeEvidence'
        provisional_approval_id:
          type: string
          format: uuid
          nullable: true
        settlement_approval_id:
          type: string
          format: uuid
          nullable: true
        moves:
          type: array
          items:
            type: string
            enum:
              - send_to_merchant
              - decide
    BackofficeMoneyDisputeQueue:
      type: object
      description: >
        GET /backoffice/disputes. Cases with stage NOT IN (decided, withdrawn), oldest raised_at first, at
        most 200.
      required:
        - count
        - disputes
      properties:
        count:
          type: integer
          minimum: 0
        disputes:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/BackofficeMoneyDisputeCase'
              - $ref: '#/components/schemas/BackofficeMoneyDisputeUnreadable'
