openapi: 3.0.3
info:
  title: Paynovus CIP Partner API
  description: >
    The CIP Partner API exposes a JWT-authenticated surface for trusted partners
    to

    manage individuals they onboard, create and operate cards under those

    individuals, and query fees / delivery options.


    All endpoints are mounted under `/api/partner` and require an **ES512**

    Bearer JWT whose `iss` matches the partner's registered URL and whose

    `sub` equals the partner's `_id`.


    ## Authentication


    1. Generate a P-521 (secp521r1) ES512 keypair and provide the public key
       to Paynovus — it is stored on the `Partner` record.
    2. Sign a JWT with the partner's private key. The token must contain:

       ```json
       {
         "iss": "https://your-registered-url.com",
         "sub": "<partnerObjectId>",
         "exp": <unix-seconds, +1 minute>,
         "iat": <unix-seconds>
       }
       ```

    3. Send the token on every request:

       ```
       Authorization: Bearer <jwt>
       ```

    Partners are pre-trusted: a successful `partnerAuth` middleware pass grants

    full access to every resource owned by `req.auth.partnerId`. No additional

    role/permission checks are enforced.


    Card-level endpoints additionally verify that the card (`cardKey`) belongs

    to the calling partner via `assertPartnerCardOwnership`.
  version: 1.0.0
  contact:
    name: Paynovus Development Team
    email: office@paynovus.com
servers:
  - url: https://api.card-management-staging.paynovus.com
    description: Staging server
security:
  - JWTBearerAuth: []
tags:
  - name: Partner API
    description: |
      Endpoints that target the authenticated partner itself (`/health-check`,
      `/me`). Useful for connectivity checks and self-introspection.
  - name: Individual Accounts
    description: |
      Endpoints for managing individual cardholders onboarded by the partner.
      All paths under this tag are tenant-scoped via
      `assertPartnerIndividualAccess`.
  - name: Cards
    description: |
      Card-level endpoints that operate on a single IT-Card `cardKey`.
      The `:id` path parameter is the IT-Card `cardKey` (for example
      `KDRYV10100033065`). Every endpoint under `/cards/:id/...` runs the
      `assertPartnerCardOwnership` middleware before reaching the controller.
  - name: Fees & Delivery
    description: |
      Fee and delivery-method lookups. The issuing/delivery fee endpoints
      require a `validateRequest` pass against the shared
      `checkFeePayloadSchema`.
paths:
  /api/partner/health-check:
    get:
      tags:
        - Partner API
      summary: Health check
      description: |
        Public liveness probe. **Does not require authentication** — useful
        for partner integration connectivity checks and infrastructure
        readiness probes. Always returns `200 OK` with the body `OK`.
      operationId: partnerHealthCheck
      security: []
      responses:
        '200':
          description: Service is reachable
          content:
            text/plain:
              schema:
                type: string
                example: OK
  /api/partner/me:
    get:
      tags:
        - Partner API
      summary: Get partner identity
      description: |
        Returns the authenticated partner profile along with every B2B tenant
        and individual onboarding owned by that partner.

        The partner object is sourced from the already-verified partner that
        `partnerAuth` attaches to the request, so the response always
        reflects exactly the authenticated partner.
      operationId: getPartnerMe
      responses:
        '200':
          description: Partner profile + owned tenants + owned individuals
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerMeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/partner/individual/accounts:
    get:
      tags:
        - Individual Accounts
      summary: List individual accounts
      description: Returns every individual onboarding owned by the authenticated partner.
      operationId: listPartnerIndividuals
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Paginated list of individual accounts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IndividualAccountListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Individual Accounts
      summary: Create individual account
      description: |
        Creates a partner-owned individual cardholder. The individual never
        logs into the CIP dashboard — the partner has its own dashboard for
        that; the record exists here only so CIP admins can see/control it.

        Validates the payload against `partnerCreateIndividualSchemaValidation`
        and delegates to `createPartnerIndividualOnboarding`, which creates
        the `User` + `IndividualOnboarding` records and calls ryvyl-proxy-orasys
        to create the upstream account. Rolls both records back if the proxy
        call fails.
      operationId: createPartnerIndividual
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerCreateIndividualRequest'
      responses:
        '201':
          description: Individual created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IndividualAccount'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/partner/individual/accounts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Individual onboarding `_id` (Mongo ObjectId)
        schema:
          type: string
    get:
      tags:
        - Individual Accounts
      summary: Get individual account
      description: |
        Returns the individual onboarding identified by `:id`. Requires
        `assertPartnerIndividualAccess('id')` — the partner must own the record.
      operationId: getPartnerIndividual
      responses:
        '200':
          description: Individual account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IndividualAccount'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/partner/individual/accounts/{id}/cards:
    parameters:
      - name: id
        in: path
        required: true
        description: Individual onboarding `_id`
        schema:
          type: string
    get:
      tags:
        - Individual Accounts
      summary: List cards for an individual
      description: Returns every active card owned by the given individual.
      operationId: getPartnerIndividualCards
      responses:
        '200':
          description: Card list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CardSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/individual/accounts/{id}/cards/virtual:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Individual Accounts
      summary: Create virtual card for an individual
      description: |
        Issues a virtual card under the given individual. Forwards the
        request to IT-Card's `POST /cards/v1/create-card-virtual` and stores
        the resulting card under the partner/individual ownership chain.
      operationId: createPartnerIndividualVirtualCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCardRequest'
      responses:
        '201':
          description: Virtual card created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/individual/accounts/{id}/cards/physical:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      tags:
        - Individual Accounts
      summary: Create physical card for an individual
      description: Issues a physical card under the given individual.
      operationId: createPartnerIndividualPhysicalCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePhysicalCardRequest'
      responses:
        '201':
          description: Physical card created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/individual/accounts/{id}/logs/audit:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Individual Accounts
      summary: List audit logs for an individual
      description: |
        Returns the B2B audit log entries scoped to the given individual.
        Pagination is via `page` and `limit`.
      operationId: listPartnerIndividualAuditLogs
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Audit log entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditLogListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/individual/accounts/{id}/logs/card-status-history:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Individual Accounts
      summary: Card status history for an individual
      description: Returns every card-status change recorded for this individual.
      operationId: listPartnerIndividualCardStatusHistory
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Status history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusHistoryListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/individual/accounts/{id}/transactions:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Individual Accounts
      summary: List transactions for an individual
      description: |
        Fetches transactions from Card Processing by account identification
        and card id. Empty results are returned for partners who have not
        configured Card Processing yet.
      operationId: listPartnerIndividualTransactions
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Transactions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards:
    get:
      tags:
        - Cards
      summary: List all partner cards
      description: |
        Returns every card owned by the authenticated partner across all
        account types (B2B + individual). **Not** tenant-scoped per-card —
        returns the full partner-wide list.
      operationId: listPartnerCards
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          description: Filter by IT-Card status
          schema:
            $ref: '#/components/schemas/CardStatus'
        - name: type
          in: query
          description: '`virtual` or `physical`'
          schema:
            type: string
            enum:
              - virtual
              - physical
        - name: search
          in: query
          description: Case-insensitive search on `cardMask` / `name`
          schema:
            type: string
      responses:
        '200':
          description: Card list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/partner/cards/{id}:
    parameters:
      - $ref: '#/components/parameters/CardId'
    get:
      tags:
        - Cards
      summary: Get card details
      description: |
        Returns full IT-Card card details. **Does not include the PAN or
        CVV** — use the dedicated sensitive endpoints below for those.
      operationId: getCardDetails
      responses:
        '200':
          description: Card details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/partner/cards/{id}/status:
    parameters:
      - $ref: '#/components/parameters/CardId'
    get:
      tags:
        - Cards
      summary: Get card status
      description: Returns the current IT-Card status + status code.
      operationId: getCardStatus
      responses:
        '200':
          description: Card status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/lock:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Lock card
      description: |
        Locks the card. The card stays in the holder's possession but cannot
        authorize transactions.
      operationId: lockCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardLifecycleRequest'
      responses:
        '200':
          description: Card locked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Card already locked or in non-lockable state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/partner/cards/{id}/unlock:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Unlock card
      description: Unlocks a previously-locked card.
      operationId: unlockCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardLifecycleRequest'
      responses:
        '200':
          description: Card unlocked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/activate:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Activate card
      description: Activates a freshly-issued card.
      operationId: activateCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardLifecycleRequest'
      responses:
        '200':
          description: Card activated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/temporary-lock-card:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Temporary lock card
      description: Temporarily locks the card (e.g. while the holder travels).
      operationId: temporaryLockCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardLifecycleRequest'
      responses:
        '200':
          description: Card temp-locked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/restrict:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Restrict card (generic)
      description: Generic card restriction.
      operationId: restrictCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardRestrictRequest'
      responses:
        '200':
          description: Card restricted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/restrict-card-as-lost:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Restrict card as lost
      description: |
        Restricts the card as **lost**. The card cannot be used; the holder
        must request a [replacement](./replace-card).
      operationId: restrictCardAsLost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardRestrictRequest'
      responses:
        '200':
          description: Card restricted as lost
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/restrict-card-as-stolen:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Restrict card as stolen
      description: Restricts the card as stolen.
      operationId: restrictCardAsStolen
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardRestrictRequest'
      responses:
        '200':
          description: Card restricted as stolen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/restrict-card-as-closed:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Restrict card as closed
      description: Restricts the card as closed.
      operationId: restrictCardAsClosed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardRestrictRequest'
      responses:
        '200':
          description: Card restricted as closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/restrict-card-fraud:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Restrict card as fraud
      description: Restricts the card as fraud.
      operationId: restrictCardAsFraud
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardRestrictRequest'
      responses:
        '200':
          description: Card restricted as fraud
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/restrict-card-mobile-lost:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Restrict card as mobile-lost
      description: Restricts the card as lost on mobile (device).
      operationId: restrictCardAsMobileLost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardRestrictRequest'
      responses:
        '200':
          description: Card restricted as mobile-lost
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/change-nickname:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Change card nickname
      description: Local-only — sets a friendly name for the card.
      operationId: changeCardNickname
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - nickname
              properties:
                nickname:
                  type: string
                  maxLength: 64
      responses:
        '200':
          description: Nickname updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/embossName:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Change emboss name
      description: |
        Updates the embossed name on the card. **Note:** IT-Card does not
        allow spaces in the emboss-name request.
      operationId: changeCardEmbossName
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - embossedName
              properties:
                embossedName:
                  type: string
                  maxLength: 26
      responses:
        '200':
          description: Emboss name updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/replace-card:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Replace card
      description: |
        Replaces the physical card. IT-Card returns a **new** `cardKey`;
        the local `AccountCard` record is updated to the new key and a
        `card_created` webhook is sent to Card Processing.

        CIP then enriches the response with `cardMask` (not present in
        IT-Card's replace response) by fetching the new card from
        `GET /cards/{newCardKey}`.
      operationId: replaceCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardReplaceRequest'
      responses:
        '200':
          description: Replacement card created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/resign-card:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Re-issue / resign card
      description: |
        Re-issues the card (e.g. PIN reissue, expiration). IT-Card returns
        a **new** `cardKey`; the old card is closed locally and a
        `card_created` webhook is sent to Card Processing.

        The old `statusCode` is captured from the local `AccountCard` row
        BEFORE the local update overwrites it, then forwarded to
        `POST /api/cip/update-card-status` with the OLD `cardKey`.
      operationId: resignCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardReplaceRequest'
      responses:
        '200':
          description: Re-issued card created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/card-cvv:
    parameters:
      - $ref: '#/components/parameters/CardId'
    get:
      tags:
        - Cards
      summary: Get card CVV
      description: |
        Returns the CVV2 for the card. **Sensitive** — never log this
        response. Requires `expDate` and `securityKey` (RSA-OAEP, base64).
      operationId: getCardCvv
      parameters:
        - name: expDate
          in: query
          required: true
          description: Card expiration in `MM/YYYY` format
          schema:
            type: string
            pattern: ^(0[1-9]|1[0-2])\/\d{4}$
        - name: securityKey
          in: query
          required: true
          description: RSA-OAEP security key (base64)
          schema:
            type: string
      responses:
        '200':
          description: CVV
          content:
            application/json:
              schema:
                type: object
                properties:
                  cvv:
                    type: string
                    example: '123'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/card-number:
    parameters:
      - $ref: '#/components/parameters/CardId'
    get:
      tags:
        - Cards
      summary: Get full card number (PAN)
      description: |
        Returns the full PAN for the card. **Highly sensitive** — never log
        this response. Requires `expDate` and `securityKey` (RSA-OAEP, base64).
      operationId: getCardNumber
      parameters:
        - name: expDate
          in: query
          required: true
          description: Card expiration in `MM/YYYY` format
          schema:
            type: string
            pattern: ^(0[1-9]|1[0-2])\/\d{4}$
        - name: securityKey
          in: query
          required: true
          description: RSA-OAEP security key (base64)
          schema:
            type: string
      responses:
        '200':
          description: Card number
          content:
            application/json:
              schema:
                type: object
                properties:
                  cardNumber:
                    type: string
                    example: '4111111111111111'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/3dsAnswer:
    parameters:
      - $ref: '#/components/parameters/CardId'
    put:
      tags:
        - Cards
      summary: Set 3DS answer
      description: Updates the 3-D Secure answer configured on the card.
      operationId: set3dsAnswer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - answer
              properties:
                answer:
                  type: string
                  maxLength: 200
      responses:
        '200':
          description: 3DS answer updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/change-3ds-phone:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Change 3DS phone number
      description: Local-only — updates the 3-D Secure phone number on the card.
      operationId: change3dsPhone
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - phoneNumber
              properties:
                phoneNumber:
                  type: string
                  maxLength: 32
      responses:
        '200':
          description: 3DS phone updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/resetPinTries:
    parameters:
      - $ref: '#/components/parameters/CardId'
    get:
      tags:
        - Cards
      summary: Reset PIN tries
      description: Resets the cardholder's PIN-attempt counter.
      operationId: resetPinTries
      responses:
        '200':
          description: PIN tries reset
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/change-pin:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Change PIN
      description: |
        Changes the card PIN. `pin` is the **RSA-OAEP encrypted** PIN
        (base64). **Known limitation:** IT-Card currently returns an error
        on this operation.
      operationId: changeCardPin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - expDate
                - pin
              properties:
                expDate:
                  type: string
                  pattern: ^(0[1-9]|1[0-2])\/\d{4}$
                pin:
                  type: string
                  description: RSA-OAEP encrypted PIN (base64)
      responses:
        '200':
          description: PIN changed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: IT-Card reported an error (current limitation)
  /api/partner/cards/{id}/force-pin-lock:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Force PIN lock
      description: |
        Forces a PIN-attempt lock on the card. **Known limitation:**
        IT-Card currently returns an error on this operation.
      operationId: forcePinLock
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - expDate
              properties:
                expDate:
                  type: string
                  pattern: ^(0[1-9]|1[0-2])\/\d{4}$
      responses:
        '200':
          description: PIN locked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardStatusObject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          description: IT-Card reported an error (current limitation)
  /api/partner/cards/{id}/holder/authorize:
    parameters:
      - $ref: '#/components/parameters/CardId'
    post:
      tags:
        - Cards
      summary: Authorize cardholder
      description: |
        Authorizes a cardholder action (PIN-less). Requires the holder's
        `pin` (RSA-OAEP encrypted, base64).
      operationId: authorizeCardHolder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - pin
              properties:
                pin:
                  type: string
                  description: RSA-OAEP encrypted PIN (base64)
      responses:
        '200':
          description: Holder authorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/limits:
    parameters:
      - $ref: '#/components/parameters/CardId'
    get:
      tags:
        - Cards
      summary: Get all card limits
      description: Returns the full set of card limits.
      operationId: getCardLimits
      responses:
        '200':
          description: All card limits
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CardLimit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    patch:
      tags:
        - Cards
      summary: Patch card limits
      description: Updates multiple card limits at once.
      operationId: patchCardLimits
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: '#/components/schemas/CardLimitUpdate'
      responses:
        '200':
          description: Limits updated
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CardLimit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/limits/{limitName}:
    parameters:
      - $ref: '#/components/parameters/CardId'
      - name: limitName
        in: path
        required: true
        schema:
          type: string
    put:
      tags:
        - Cards
      summary: Set a single card limit
      description: Sets a named card limit (e.g. `dailyPurchase`).
      operationId: setCardLimit
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardLimitUpdate'
      responses:
        '200':
          description: Limit updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardLimit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    get:
      tags:
        - Cards
      summary: Get a single card limit
      description: Returns the value of a single named limit.
      operationId: getCardLimit
      responses:
        '200':
          description: Single limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardLimit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/cards/{id}/limits/{limitName}/available:
    parameters:
      - $ref: '#/components/parameters/CardId'
      - name: limitName
        in: path
        required: true
        schema:
          type: string
    get:
      tags:
        - Cards
      summary: Get available limit
      description: Returns the remaining available amount for the named limit.
      operationId: getCardLimitAvailable
      responses:
        '200':
          description: Available limit
          content:
            application/json:
              schema:
                type: object
                properties:
                  available:
                    type: number
                    format: double
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/partner/fees/delivery-methods:
    get:
      tags:
        - Fees & Delivery
      summary: List delivery methods by country
      description: |
        Returns PocztaPost + DHL delivery methods available for the given
        ISO 3166-1 alpha-2 country code.
      operationId: getCountryDeliveryMethods
      parameters:
        - name: country
          in: query
          required: true
          description: ISO 3166-1 alpha-2 country code (e.g. `PL`, `DE`)
          schema:
            type: string
            minLength: 2
            maxLength: 2
      responses:
        '200':
          description: Delivery methods
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                      country:
                        type: string
                      deliveryMethods:
                        type: array
                        items:
                          type: object
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/partner/fees/issuing-fee:
    post:
      tags:
        - Fees & Delivery
      summary: Check card issuing fee
      description: |
        Forwards a `CheckTransactionFee` request to Card Processing with
        `OperationType` fixed server-side to `Card Issuing Fee`.

        At least one of `cardKey`, `clientId`, or `iban` must be present.
      operationId: checkIssuingFee
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckFeeRequest'
      responses:
        '200':
          description: Fee record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Fee not found for the given parameters
  /api/partner/fees/delivery-fee:
    post:
      tags:
        - Fees & Delivery
      summary: Check card delivery fee
      description: |
        Forwards a `CheckTransactionFee` request to Card Processing with
        `OperationType` fixed server-side to `Card Delivery Fee`.

        At least one of `cardKey`, `clientId`, or `iban` must be present.
      operationId: checkDeliveryFee
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckFeeRequest'
      responses:
        '200':
          description: Fee record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Fee not found for the given parameters
components:
  parameters:
    Page:
      name: page
      in: query
      description: Page number for pagination
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
    Limit:
      name: limit
      in: query
      description: Items per page
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    CardId:
      name: id
      in: path
      required: true
      description: |
        IT-Card `cardKey` (for example `KDRYV10100033065`). The
        `assertPartnerCardOwnership` middleware verifies that the card is
        owned by the calling partner before the controller runs.
      schema:
        type: string
  securitySchemes:
    JWTBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Bearer JWT signed with ES512. The `iss` claim must match the
        partner's registered URL and `sub` must equal the partner's `_id`.
  responses:
    BadRequest:
      description: Bad request — missing or invalid fields
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Unauthorized:
      description: Unauthorized — missing or invalid JWT
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Forbidden:
      description: Forbidden — resource not owned by the calling partner
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
  schemas:
    ApiError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
        error:
          type: string
    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        pages:
          type: integer
    Partner:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        status:
          type: string
          enum:
            - active
            - inactive
            - suspended
        webhooks:
          type: object
          properties:
            webhookIndividualOnboardingApplicantCreated:
              type: array
              items:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                  hasSecret:
                    type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    B2BTenant:
      type: object
      properties:
        _id:
          type: string
        companyName:
          type: string
        clientCode:
          type: string
        status:
          type: string
        email:
          type: string
          format: email
        phoneNumber:
          type: string
        createdAt:
          type: string
          format: date-time
    PartnerMeResponse:
      type: object
      properties:
        partner:
          $ref: '#/components/schemas/Partner'
        activeTenantId:
          type: string
          nullable: true
          description: |
            The B2B tenant this request was scoped to (null when the
            partner has no B2B tenant — common for individual-only partners).
        b2bAccounts:
          type: array
          items:
            $ref: '#/components/schemas/B2BTenant'
        individuals:
          type: array
          items:
            $ref: '#/components/schemas/IndividualAccountSummary'
    IndividualAccountSummary:
      type: object
      properties:
        _id:
          type: string
        clientID:
          type: string
        personalInfo:
          type: object
          additionalProperties: true
        address:
          type: object
          additionalProperties: true
        applicationStatus:
          type: string
        dashboardStatus:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    IndividualAccount:
      allOf:
        - $ref: '#/components/schemas/IndividualAccountSummary'
    IndividualAccountListResponse:
      type: object
      properties:
        count:
          type: integer
        data:
          type: array
          items:
            $ref: '#/components/schemas/IndividualAccountSummary'
    PartnerCreateIndividualRequest:
      type: object
      required:
        - firstName
        - middleName
        - lastName
        - country
        - city
        - zipCode
        - buildingNumber
        - idType
        - address1
        - passportNumber
        - issuingDate
        - validityDate
        - passportCountry
        - birthCountry
        - birthDate
        - nationality
        - phone
        - email
      properties:
        baasAccountClientId:
          type: integer
          description: BaaS account/program the individual is created under
        firstName:
          type: string
          maxLength: 200
        middleName:
          type: string
          minLength: 2
          maxLength: 200
        lastName:
          type: string
          maxLength: 200
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
          minLength: 2
          maxLength: 2
        city:
          type: string
          maxLength: 100
        stateProvince:
          type: string
          maxLength: 100
        zipCode:
          type: string
          maxLength: 20
        buildingNumber:
          type: string
          maxLength: 100
        idType:
          type: string
          enum:
            - PASSPORT
            - DRIVER_LICENSE
            - RESIDENCE_CARD
            - MINOR_WITHOUT_ID
            - OTHER
        address1:
          type: string
          maxLength: 200
        address2:
          type: string
          maxLength: 200
        passportNumber:
          type: string
          maxLength: 40
        issuingDate:
          type: string
          description: yyyy-mm-dd
        validityDate:
          type: string
          description: yyyy-mm-dd
        passportCountry:
          type: string
          description: ISO 3166-1 alpha-2 country code
          minLength: 2
          maxLength: 2
        taxCountry:
          type: string
        taxIdNumber:
          type: string
          description: Defaults to `passportNumber` when omitted
        birthCountry:
          type: string
          minLength: 2
          maxLength: 2
        birthDate:
          type: string
          description: yyyy-mm-dd
        nationality:
          type: string
          minLength: 2
          maxLength: 2
        fatca:
          type: string
          description: Defaults to `F` when omitted
        phone:
          type: string
        email:
          type: string
          format: email
    CreateCardRequest:
      type: object
      required:
        - expDate
        - clientCode
        - currency
        - productCode
      properties:
        expDate:
          type: string
          pattern: ^(0[1-9]|1[0-2])\/\d{4}$
        clientCode:
          type: string
        currency:
          type: string
          minLength: 3
          maxLength: 3
        productCode:
          type: string
        embossedName:
          type: string
        shippingAddressId:
          type: string
    CreatePhysicalCardRequest:
      allOf:
        - $ref: '#/components/schemas/CreateCardRequest'
    CardStatus:
      type: string
      enum:
        - ACTIVE
        - LOCKED
        - CLOSED
        - LOST
        - STOLEN
        - FRAUD
        - MOBILE_LOST
        - RESTRICTED
        - TEMP_LOCKED
    CardStatusObject:
      type: object
      properties:
        cardKey:
          type: string
        status:
          $ref: '#/components/schemas/CardStatus'
        statusCode:
          type: string
          description: IT-Card raw status code (e.g. `00`, `S1`, `L1`)
    CardSummary:
      type: object
      properties:
        cardKey:
          type: string
        cardMask:
          type: string
        type:
          type: string
          enum:
            - virtual
            - physical
        status:
          $ref: '#/components/schemas/CardStatus'
        statusCode:
          type: string
        currency:
          type: string
        ownerType:
          type: string
          enum:
            - individual
            - b2b
        ownerId:
          type: string
        createdAt:
          type: string
          format: date-time
    CardDetails:
      allOf:
        - $ref: '#/components/schemas/CardSummary'
        - type: object
          properties:
            productCode:
              type: string
            holderClientCode:
              type: string
            expDate:
              type: string
            embossedName:
              type: string
    CardListResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/CardSummary'
        pagination:
          $ref: '#/components/schemas/Pagination'
    CardLifecycleRequest:
      type: object
      required:
        - expDate
      properties:
        expDate:
          type: string
          pattern: ^(0[1-9]|1[0-2])\/\d{4}$
          description: Card expiration in `MM/YYYY`
        reason:
          type: string
          description: Audit reason (free-form)
    CardRestrictRequest:
      type: object
      properties:
        expDate:
          type: string
          pattern: ^(0[1-9]|1[0-2])\/\d{4}$
        reason:
          type: string
        description:
          type: string
        cardLocation:
          type: string
          description: Where the card was last seen
    CardReplaceRequest:
      type: object
      required:
        - expDate
      properties:
        expDate:
          type: string
          pattern: ^(0[1-9]|1[0-2])\/\d{4}$
        reason:
          type: string
        pinMailer:
          type: string
          description: Postal pin-mailer reference
    CardLimit:
      type: object
      properties:
        name:
          type: string
          example: dailyPurchase
        value:
          type: number
          format: double
        currency:
          type: string
        used:
          type: number
          format: double
        available:
          type: number
          format: double
    CardLimitUpdate:
      type: object
      required:
        - name
        - value
      properties:
        name:
          type: string
          example: dailyPurchase
        value:
          type: number
          format: double
    AuditLogEntry:
      type: object
      properties:
        _id:
          type: string
        action:
          type: string
        actor:
          type: string
        payload:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
    AuditLogListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AuditLogEntry'
        pagination:
          $ref: '#/components/schemas/Pagination'
    CardStatusHistoryEntry:
      type: object
      properties:
        _id:
          type: string
        cardKey:
          type: string
        fromStatus:
          type: string
        toStatus:
          type: string
        fromStatusCode:
          type: string
        toStatusCode:
          type: string
        reason:
          type: string
        createdAt:
          type: string
          format: date-time
    CardStatusHistoryListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/CardStatusHistoryEntry'
        pagination:
          $ref: '#/components/schemas/Pagination'
    Transaction:
      type: object
      properties:
        id:
          type: string
        cardKey:
          type: string
        amount:
          type: number
          format: double
        currency:
          type: string
        merchant:
          type: string
        mcc:
          type: string
        status:
          type: string
        createdAt:
          type: string
          format: date-time
    TransactionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Transaction'
        pagination:
          $ref: '#/components/schemas/Pagination'
    CheckFeeRequest:
      type: object
      required:
        - CustomerType
        - ProductType
      properties:
        CustomerType:
          type: string
          enum:
            - Consumer Debit
            - Business Debit
            - Consumer Prepaid
            - Business Prepaid
        ProductType:
          type: string
          enum:
            - R001
            - R002
            - R003
            - R004
            - R005
            - R006
        cardKey:
          type: string
          description: At least one of cardKey / clientId / iban must be present
        clientId:
          type: string
        iban:
          type: string
    FeeResponse:
      type: object
      properties:
        OperationType:
          type: string
        CustomerType:
          type: string
        ProductType:
          type: string
        fee:
          type: number
          format: double
        currency:
          type: string
