openapi: 3.1.0
info:
  title: Travel Rule Protocol (TRP) API
  version: 3.2.1
  summary: Public TRP API for VASP-to-VASP Travel Rule data exchange.
  x-logo:
    url: ./assets/logo.png
    altText: TRP
    href: /
    backgroundColor: '#FFFFFF'
  contact:
    name: KYCAID TRP Team
    url: https://docs.travel-rule.com
    email: support@travel-rule.com
  license:
    name: Proprietary
    url: https://docs.travel-rule.com
  description: >
    # Working with the documentation


    This section describes the public endpoints of the **Travel Rule

    Protocol (TRP) Registry API**. These endpoints allow third-party

    systems and Virtual Asset Service Providers (VASPs) to discover,

    identify and register participants in the TRP network.


    The API follows REST conventions and returns responses in JSON

    format. All requests should be made over **HTTPS**.


    ## Introduction: How the TRP Public API Works


    The TRP API enables VASPs to:


    - Authenticate using **API keys**

    - Generate unique **Travel Addresses** for beneficiaries

    - Initiate **Travel Rule transfers**

    - Exchange originator and beneficiary data securely

    - Receive **callback** notifications from TRP during the verification
    process


    TRP follows global compliance standards (**FATF**, **IVMS101**) and

    ensures secure, encrypted communication between VASPs.


    Every integration follows one simple flow:


    1. **Request JWT token** → authenticate your VASP

    2. **Generate Travel Address** → unique identifier for the beneficiary VASP

    3. **Initiate Transfer** → send IVMS101-compliant data

    4. **Receive callback** → TRP sends status updates to your backend


    ## Authentication Overview


    Before calling any protected endpoint, your system must generate a

    **JWT access token** using your API Key. This ensures:


    - Secure API access

    - Verified VASP identity

    - Ability to track API usage

    - Authorization for initiating Travel Rule transfers


    The access token is short-lived (**TTL ≈ 1 hour**) for security

    reasons. Pass it as `Authorization: Bearer <jwt>` on every protected

    endpoint.


    ## Conventions


    - All bodies are `application/json` unless explicitly stated.

    - Successful responses are wrapped in `{ "status": true, "data": ... }`.

    - Errors are wrapped in `{ "status": false, "errors": [{ "code", "message"
    }] }`.

    - Timestamps use ISO-8601 in UTC.

    - Country codes follow ISO-3166-1 alpha-2.
servers:
  - url: https://trp.travel-rule.com
    description: Production public API host
  - url: http://localhost:3000
tags:
  - name: Identity
    x-displayName: Identity
    description: |
      Public identity endpoints. These return non-sensitive VASP
      metadata (legal name, LEI, x509 public key) and server health.
      They do **not** require authentication.
  - name: Auth
    x-displayName: Auth
    description: |
      Authentication endpoints. Use your **API Key** (issued in the TRP
      dashboard) to obtain a short-lived **JWT** that authorises every
      protected call.

      ## Authentication header

      You may pass the API Key in either of these headers:

      ```http
      x-api-key: <YOUR_API_KEY>
      ```

      or

      ```http
      Authorization: ApiKey <YOUR_API_KEY>
      ```

      Once you receive the JWT, all subsequent calls must include:

      ```http
      Authorization: Bearer <JWT>
      ```
  - name: Address
    x-displayName: Address
    description: |
      **Travel Address** endpoints. A Travel Address is an opaque,
      URL-safe encoding of a beneficiary VASP route. It is generated by
      the beneficiary VASP and shared with the originator VASP so that
      IVMS101 data can be routed back.
  - name: Transfer
    x-displayName: Transfer
    description: |
      High-level Travel Rule **transfer** operations from the originator
      VASP perspective. Transfers can flow two ways depending on what was
      provided at initiation:

      - `OPEN_VASP` — beneficiary VASP is reachable via Travel Address.
      - `EMAIL` — beneficiary VASP is unknown; we send an email so the
        recipient can self-attest.
  - name: Compliance
    x-displayName: Compliance
    description: |
      Inter-VASP **compliance** endpoints called when TRP routes data
      between participating VASPs. They are typically invoked by other
      VASPs / TRP itself, **not** by your front-end.
  - name: Discovery
    x-displayName: Discovery
    description: |
      Look up other VASPs in the TRP registry by domain, LEI, name,
      email, or directory ID.
  - name: Callbacks
    x-displayName: Callbacks
    description: >
      # Callbacks


      TRP delivers asynchronous updates to your `callback` URL via

      signed `POST` requests. Each callback is queued and retried with

      exponential backoff.


      ## Retry schedule


      The KYCAID TRP backend retries failed callbacks at:


      ```

      1m → 5m → 15m → 30m → 1h → 3h → 6h → 12h → 24h

      ```


      After 9 unsuccessful attempts the callback is marked `FAILED` and

      dropped.


      ## Signature verification


      Every callback ships an `x-data-integrity` header computed as:


      ```

      HMAC-SHA512( BASE64( request_body ), customer_signing_secret )

      ```


      Pseudocode:


      ```js

      import crypto from 'node:crypto';


      function verify(rawBody, signatureHeader, secret) {
          const base64 = Buffer.from(rawBody, 'utf8').toString('base64');
          const expected = crypto
              .createHmac('sha512', secret)
              .update(base64)
              .digest('hex');
          return crypto.timingSafeEqual(
              Buffer.from(expected),
              Buffer.from(signatureHeader),
          );
      }

      ```


      ## Callback types


      | Type | When | Body |

      |------|------|------|

      | `INQUIRY` | Sent by originator VASP to beneficiary VASP after
      `transfers/initiate`. | `{ asset, amount, callback, IVMS101 }` |

      | `INQUIRY_RESOLUTION` | Sent by beneficiary VASP back to originator. | `{
      approved, callback }` |

      | `TRANSFER_RESOLUTION` | Sent to the originator's `callback` once the
      beneficiary VASP approves. | `{ transferId, status, statusComment,
      callback }` |

      | `TRANSFER_CONFIRMATION` | Sent to the beneficiary's stored callback once
      `txId` is broadcast. | `{ txid }` |


      Respond with **2xx** to acknowledge. Any non-2xx response is

      retried per the schedule above.
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        Customer API Key issued in the TRP dashboard. Used **only** to
        obtain a JWT through `POST /auth/token`.
    ApiKeyAuth:
      type: apiKey
      in: header
      name: Authorization
      description: |
        Alternative API Key header. Use the format
        `Authorization: ApiKey <YOUR_API_KEY>`.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Short-lived JWT (TTL ≈ 1h) obtained from
        [`POST /auth/token`](#tag/Auth/operation/createToken). Pass as
        `Authorization: Bearer <jwt>` on every protected endpoint.
  parameters:
    TransferQueryId:
      name: q
      in: query
      required: true
      description: Public ID of the transfer (UUID).
      schema:
        type: string
        format: uuid
    TransferPublicId:
      name: transferPublicId
      in: path
      required: true
      description: Public ID of the transfer (UUID).
      schema:
        type: string
        format: uuid
    BeneficiaryPublicId:
      name: beneficiaryPublicId
      in: path
      required: true
      description: Public ID of the beneficiary applicant (UUID).
      schema:
        type: string
        format: uuid
    CommentId:
      name: commentId
      in: path
      required: true
      description: Numeric ID of the comment.
      schema:
        type: integer
        format: int64
        minimum: 1
    ApiVersionHeader:
      name: api-version
      in: header
      required: false
      description: TRP protocol version. Defaults to `3.2.1`.
      schema:
        type: string
        example: 3.2.1
    RequestIdentifierHeader:
      name: request-identifier
      in: header
      required: false
      description: UUID used to correlate the call with its asynchronous callback.
      schema:
        type: string
        format: uuid
  schemas:
    SuccessEnvelope:
      type: object
      properties:
        status:
          type: boolean
          const: true
        data:
          type: object
      required:
        - status
    ErrorItem:
      type: object
      properties:
        code:
          type: string
          description: Machine-readable error code.
          example: INVALID_REQUEST
        message:
          type: string
          description: Human-readable error description.
          example: Asset is required
      required:
        - code
        - message
    ErrorEnvelope:
      type: object
      properties:
        status:
          type: boolean
          const: false
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ErrorItem'
      required:
        - status
        - errors
    ApplicantCommon:
      type: object
      description: |
        Shared fields available for both natural and legal applicants.
        These properties are accepted on the `originator` and
        `beneficiary` objects of `POST /transfers/initiate`
        regardless of `personType`.
      properties:
        id:
          type: integer
          readOnly: true
          description: Internal database ID. Read-only.
        externalId:
          type: string
          maxLength: 64
          description: Your internal identifier for this applicant.
        middleName:
          type: string
          maxLength: 100
          description: Middle name (typically used for natural persons).
        dateOfBirth:
          type: string
          format: date
          maxLength: 10
          example: '1990-01-15'
          description: Applicant date of birth. ISO-8601 date.
        placeOfBirth:
          type: string
          maxLength: 70
          example: London
        addressType:
          type: string
          enum:
            - HOME
            - BIZZ
            - GEOG
          description: IVMS101 address type — Home / Business / Geographic.
        streetName:
          type: string
          maxLength: 70
        buildingNumber:
          type: string
          maxLength: 16
        buildingName:
          type: string
          maxLength: 35
        postCode:
          type: string
          maxLength: 16
        townName:
          type: string
          maxLength: 35
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO-3166-1 alpha-2 country code.
        countrySubDivision:
          type: string
          maxLength: 35
          description: State / province / region code.
        nationalId:
          type: string
          maxLength: 35
          description: National identifier value (passport number, LEI, etc.).
        nationalIdType:
          type: string
          maxLength: 10
          enum:
            - ARNU
            - CCPT
            - RAID
            - DRLC
            - FIIN
            - TXID
            - SOCS
            - IDCD
            - LEIX
            - MISC
          description: |
            IVMS101 national-identifier type. `LEIX` for legal
            entities (LEI), `CCPT` for passport, etc.
        countryOfIssue:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO-3166-1 alpha-2 country that issued `nationalId`.
        accountNumber:
          type: string
          maxLength: 100
          description: VASP-internal account identifier for this applicant.
        walletAddress:
          type: string
          maxLength: 100
          description: On-chain wallet address belonging to this applicant.
        email:
          type: string
          format: email
          maxLength: 255
          description: |
            Contact email. Not persisted on the `Applicant` record
            in the database — used at request time (e.g. EMAIL
            flow) and forwarded to email notifications.
        isSelfHosted:
          type: boolean
          description: |
            `true` if this applicant is a self-hosted
            (non-custodial) wallet owner.
        vasp:
          $ref: '#/components/schemas/ApplicantVasp'
    ApplicantVasp:
      type: object
      description: |
        VASP that holds (or self-hosts on behalf of) this applicant.
        Used to identify the counterparty service provider when it
        cannot be resolved from a Travel Address.
      properties:
        name:
          type: string
          maxLength: 100
          description: Display name of the VASP.
          example: Wonderland Exchange
    NaturalApplicant:
      title: Natural person
      description: |
        A **natural person** (физ. лицо) participating in a transfer.
        Requires `firstName` + `lastName`. All fields from
        `ApplicantCommon` are also available (address, KYC,
        `dateOfBirth`, `placeOfBirth`, etc.).
      allOf:
        - $ref: '#/components/schemas/ApplicantCommon'
        - type: object
          required:
            - personType
            - firstName
            - lastName
          properties:
            personType:
              type: string
              enum:
                - natural
              default: natural
              description: Discriminator. Must be `natural`.
            firstName:
              type: string
              maxLength: 100
              example: Alice
            lastName:
              type: string
              maxLength: 100
              example: Liddell
    LegalApplicant:
      title: Legal entity
      description: |
        A **legal entity** (юр. лицо) participating in a transfer.
        Requires `legalName`. `firstName` / `lastName` are optional and,
        if omitted, are derived from `legalName` server-side.
      allOf:
        - $ref: '#/components/schemas/ApplicantCommon'
        - type: object
          required:
            - personType
            - legalName
          properties:
            personType:
              type: string
              enum:
                - legal
              description: Discriminator. Must be `legal`.
            legalName:
              type: string
              maxLength: 100
              example: Wonderland Holdings Ltd
            firstName:
              type: string
              maxLength: 100
              description: Optional contact-person first name.
            lastName:
              type: string
              maxLength: 100
              description: Optional contact-person last name.
    Applicant:
      description: |
        A natural or legal person participating in a transfer
        (originator or beneficiary).

        **Use the `personType` switcher** to choose between
        Natural person (`natural`) and Legal entity (`legal`).
      oneOf:
        - $ref: '#/components/schemas/NaturalApplicant'
        - $ref: '#/components/schemas/LegalApplicant'
      discriminator:
        propertyName: personType
        mapping:
          natural: '#/components/schemas/NaturalApplicant'
          legal: '#/components/schemas/LegalApplicant'
    IVMS101Name:
      type: object
      properties:
        primaryIdentifier:
          type: string
          minLength: 1
        secondaryIdentifier:
          type: string
        nameIdentifierType:
          type: string
          enum:
            - LEGL
            - ALIA
            - BIRT
            - MAID
            - NICK
            - TITL
            - FOTH
            - GIVN
            - SURN
      required:
        - primaryIdentifier
        - nameIdentifierType
    IVMS101Address:
      type: object
      properties:
        addressType:
          type: string
          enum:
            - HOME
            - BIZZ
            - MLTO
            - PBOX
        addressLine:
          type: array
          items:
            type: string
        country:
          type: string
          pattern: ^[A-Z]{2}$
      required:
        - addressType
        - country
    IVMS101NationalIdentification:
      type: object
      properties:
        nationalIdentifier:
          type: string
          minLength: 1
        nationalIdentifierType:
          type: string
          enum:
            - ARNU
            - CCPT
            - RAID
            - DRLC
            - FIIN
            - TXID
            - SOCS
            - IDCD
            - LEIX
            - MISC
        countryOfIssue:
          type: string
          pattern: ^[A-Z]{2}$
        registrationAuthority:
          type: string
      required:
        - nationalIdentifier
        - nationalIdentifierType
    IVMS101NaturalPerson:
      type: object
      description: IVMS101 natural-person payload.
      properties:
        naturalPerson:
          type: object
          properties:
            name:
              type: object
              properties:
                nameIdentifier:
                  type: array
                  minItems: 1
                  items:
                    $ref: '#/components/schemas/IVMS101Name'
              required:
                - nameIdentifier
            geographicAddress:
              type: array
              items:
                $ref: '#/components/schemas/IVMS101Address'
            nationalIdentification:
              $ref: '#/components/schemas/IVMS101NationalIdentification'
            customerIdentification:
              type: string
            dateAndPlaceOfBirth:
              type: object
              properties:
                dateOfBirth:
                  type: string
                  format: date
                placeOfBirth:
                  type: string
              required:
                - dateOfBirth
                - placeOfBirth
          required:
            - name
      required:
        - naturalPerson
    IVMS101LegalPerson:
      type: object
      description: IVMS101 legal-person payload.
      properties:
        legalPerson:
          type: object
          properties:
            name:
              type: object
              properties:
                nameIdentifier:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    properties:
                      legalPersonName:
                        type: string
                        minLength: 1
                      legalPersonNameIdentifierType:
                        type: string
                        enum:
                          - LEGL
                          - SHOR
                          - TRAD
                          - DBA
                    required:
                      - legalPersonName
                      - legalPersonNameIdentifierType
              required:
                - nameIdentifier
            geographicAddress:
              type: array
              items:
                $ref: '#/components/schemas/IVMS101Address'
            customerNumber:
              type: string
            nationalIdentification:
              $ref: '#/components/schemas/IVMS101NationalIdentification'
          required:
            - name
      required:
        - legalPerson
    IVMS101Person:
      description: An IVMS101 person — either natural or legal.
      oneOf:
        - $ref: '#/components/schemas/IVMS101NaturalPerson'
        - $ref: '#/components/schemas/IVMS101LegalPerson'
    IVMS101Originator:
      type: object
      description: |
        Full IVMS101 originator envelope sent in callbacks. Mirrors
        the `originatingVASP` and `originator` fields used by the
        KYCAID TRP backend.
      properties:
        originator:
          type: object
          properties:
            originatorPersons:
              type: array
              items:
                $ref: '#/components/schemas/IVMS101Person'
            accountNumber:
              type: array
              items:
                type: string
        beneficiary:
          type: object
          properties:
            beneficiaryPersons:
              type: array
              items:
                $ref: '#/components/schemas/IVMS101Person'
            accountNumber:
              type: array
              items:
                type: string
        originatingVASP:
          type: object
          properties:
            originatingVASP:
              $ref: '#/components/schemas/IVMS101Person'
        beneficiaryVASP:
          type: object
          properties:
            beneficiaryVASP:
              $ref: '#/components/schemas/IVMS101Person'
    TransferStatus:
      type: string
      description: |
        Lifecycle state of a Travel Rule transfer (Prisma
        `TransferStatus` enum).

        - `PENDING` — initial state after `transfers/initiate`,
          waiting for the beneficiary VASP to resolve the inquiry.
        - `APPROVED` — beneficiary VASP confirmed the destination
          wallet via `inquiryResolution`.
        - `REJECTED` — beneficiary VASP refused the transfer.
        - `COMPLETED` — the on-chain transaction was broadcast and
          confirmed.
        - `CANCELLED` — the originator cancelled before completion.
      enum:
        - PENDING
        - APPROVED
        - REJECTED
        - COMPLETED
        - CANCELLED
    TransferInitiateRequest:
      type: object
      required:
        - asset
        - amount
        - originator
        - beneficiary
      description: |
        **At least one** of `travelAddress`, top-level `email`, or
        `beneficiary.email` must be provided.

        - If `travelAddress` is set → `OPEN_VASP` flow.
        - Otherwise an `EMAIL` flow is used and TRP emails the
          beneficiary.
      properties:
        travelAddress:
          type: string
          minLength: 1
          maxLength: 200
          description: Encoded Travel Address routing the request to the beneficiary VASP.
        email:
          type: string
          format: email
          maxLength: 255
          description: >-
            Top-level recipient email (used in EMAIL flow if `beneficiary.email`
            is empty).
        asset:
          type: string
          minLength: 1
          maxLength: 20
          example: BTC
        amount:
          type: string
          minLength: 1
          maxLength: 20
          example: '0.05'
          description: |
            Asset amount as a decimal string (e.g. `0.05`). The
            backend validates only the length range (`1..20`)
            on this endpoint — pattern validation kicks in on
            the `transfers/inquiry` callback.
        callback:
          type: string
          pattern: ^https?:\/\/.+$
          description: HTTPS callback URL that will receive `TRANSFER_RESOLUTION` updates.
        dti:
          type: string
          maxLength: 50
          description: Digital Token Identifier (ISO 24165), e.g. `4H95J0R2X`.
        originator:
          $ref: '#/components/schemas/Applicant'
        beneficiary:
          $ref: '#/components/schemas/Applicant'
    TransferInitiateResponse:
      type: object
      properties:
        status:
          type: boolean
          const: true
        data:
          type: object
          properties:
            transferId:
              type: string
              format: uuid
            status:
              $ref: '#/components/schemas/TransferStatus'
            createdAt:
              type: string
              format: date-time
    TravelAddressGenerateNatural:
      title: Natural person
      description: Generate a Travel Address for a **natural person** (физ. лицо).
      type: object
      required:
        - personType
        - firstName
        - lastName
      properties:
        personType:
          type: string
          enum:
            - natural
          default: natural
        firstName:
          type: string
          maxLength: 100
          example: Alice
        lastName:
          type: string
          maxLength: 100
          example: Liddell
        externalId:
          type: string
          minLength: 1
          maxLength: 255
        walletAddress:
          type: string
          minLength: 1
          maxLength: 100
          pattern: '[a-fA-F0-9]'
          description: |
            Beneficiary wallet address. Must contain at least one
            hex character (validator enforces `/[a-fA-F0-9]/`).
        isSelfHosted:
          type: boolean
    TravelAddressGenerateLegal:
      title: Legal entity
      description: Generate a Travel Address for a **legal entity** (юр. лицо).
      type: object
      required:
        - personType
        - legalName
      properties:
        personType:
          type: string
          enum:
            - legal
        legalName:
          type: string
          maxLength: 100
          example: Wonderland Holdings Ltd
        firstName:
          type: string
          maxLength: 100
          description: Optional contact-person first name.
        lastName:
          type: string
          maxLength: 100
          description: Optional contact-person last name.
        externalId:
          type: string
          minLength: 1
          maxLength: 255
        walletAddress:
          type: string
          minLength: 1
          maxLength: 100
          pattern: '[a-fA-F0-9]'
          description: |
            Beneficiary wallet address. Must contain at least one
            hex character (validator enforces `/[a-fA-F0-9]/`).
        isSelfHosted:
          type: boolean
    TravelAddressGenerateRequest:
      description: |
        Provide either `firstName`/`lastName` for natural persons,
        or `legalName` for legal entities. **Use the `personType`
        switcher** in the right panel to toggle between the two.
      oneOf:
        - $ref: '#/components/schemas/TravelAddressGenerateNatural'
        - $ref: '#/components/schemas/TravelAddressGenerateLegal'
      discriminator:
        propertyName: personType
        mapping:
          natural: '#/components/schemas/TravelAddressGenerateNatural'
          legal: '#/components/schemas/TravelAddressGenerateLegal'
    TravelAddressGenerateResponse:
      type: object
      properties:
        status:
          type: boolean
          const: true
        data:
          type: object
          properties:
            travelAddress:
              type: string
              description: URL-safe encoded Travel Address (e.g. `ta1q…`).
    TravelAddressDecodeRequest:
      type: object
      required:
        - travelAddress
      properties:
        travelAddress:
          type: string
          minLength: 1
          maxLength: 200
          pattern: '[a-fA-F0-9]'
          description: |
            Encoded Travel Address. Must contain at least one hex
            character (validator enforces `/[a-fA-F0-9]/`).
    TravelAddressDecodeResponse:
      type: object
      properties:
        status:
          type: boolean
          const: true
        data:
          type: object
          properties:
            parsedUrl:
              type: string
              description: Decoded HTTP URL that TRP will route the inquiry to.
              example: example.com/transfers/inquiry/0193abc4-1234-...?t=i
    TransferInquiryRequest:
      type: object
      required:
        - asset
        - amount
        - IVMS101
        - callback
      properties:
        asset:
          type: string
          minLength: 1
          maxLength: 20
        amount:
          type: string
          minLength: 1
          maxLength: 50
          pattern: ^\d+(\.\d+)?$
        IVMS101:
          $ref: '#/components/schemas/IVMS101Originator'
        callback:
          type: string
          pattern: ^https?:\/\/.+$
    InquiryResolutionRequest:
      type: object
      required:
        - approved
      properties:
        approved:
          description: |
            Either a plain wallet address string, or a structured
            object with an `address` and an optional `callback`
            override.
          oneOf:
            - type: string
              maxLength: 100
              example: bc1qxy2…
            - type: object
              required:
                - address
              properties:
                address:
                  type: string
                  maxLength: 100
                callback:
                  type: string
                  maxLength: 100
        callback:
          type: string
          pattern: ^https?:\/\/.+$
          description: Optional override for the next callback URL.
    TransferTxIdRequest:
      type: object
      required:
        - txId
      properties:
        txId:
          type: string
          minLength: 1
          maxLength: 255
    TransferConfirmationRequest:
      $ref: '#/components/schemas/TransferTxIdRequest'
    TransferComment:
      type: object
      properties:
        id:
          type: integer
        text:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        author:
          type: object
          properties:
            id:
              type: integer
            name:
              type: string
            domain:
              type: string
        canEdit:
          type: boolean
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/TransferCommentAttachment'
    TransferCommentAttachment:
      type: object
      properties:
        id:
          type: string
        originalName:
          type: string
        mimeType:
          type: string
        sizeBytes:
          type: string
        bucket:
          type: string
        objectKey:
          type: string
        downloadUrl:
          type: string
          description: Short-lived signed URL to download the attachment.
    TransferCommentCreateMultipart:
      type: object
      required:
        - text
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 2000
        files:
          type: array
          maxItems: 5
          items:
            type: string
            format: binary
          description: Up to 5 images / PDF attachments (≤ 50 MB each).
    TransferCommentUpdateRequest:
      type: object
      required:
        - text
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 2000
    Vasp:
      type: object
      properties:
        id:
          type: integer
        directoryId:
          type: string
        name:
          type: string
        email:
          type: string
          format: email
        commonName:
          type: string
        website:
          type: string
        endpoint:
          type: string
        country:
          type: string
        businessCategory:
          type: string
        vaspCategories:
          type: array
          items:
            type: string
        lei:
          type: string
        registeredDirectory:
          type: string
        protocol:
          type: string
        source:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    VaspSearchRequest:
      type: object
      description: |
        At least one search parameter is required. `domain` matches
        against both `website` and `endpoint`; the rest combine with
        logical **AND**.
      properties:
        domain:
          type: string
        name:
          type: string
        email:
          type: string
        commonName:
          type: string
        directoryId:
          type: string
        lei:
          type: string
    VaspIdentity:
      type: object
      properties:
        name:
          type: string
        lei:
          type: string
        x509:
          type: string
          description: PEM-encoded x509 public key.
    HealthResponse:
      type: object
      properties:
        status:
          type: boolean
          const: true
        data:
          type: object
          properties:
            version:
              type: string
              example: 26.4.0
            timestamp:
              type: string
              format: date-time
    InquiryCallbackBody:
      type: object
      description: Body of an `INQUIRY` callback.
      properties:
        asset:
          type: string
        amount:
          type: string
        callback:
          type: string
          description: >-
            URL that the beneficiary VASP must call back with
            `inquiryResolution`.
        IVMS101:
          $ref: '#/components/schemas/IVMS101Originator'
    TransferResolutionCallbackBody:
      type: object
      description: >-
        Body of a `TRANSFER_RESOLUTION` callback delivered to the originator's
        `callback` URL.
      properties:
        transferId:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/TransferStatus'
        statusComment:
          type:
            - string
            - 'null'
        callback:
          type: string
          description: URL the originator must POST the `txId` to once broadcast.
    TransferConfirmationCallbackBody:
      type: object
      properties:
        txid:
          type: string
          description: Final on-chain transaction id.
  responses:
    BadRequest:
      description: Validation error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            validation:
              value:
                status: false
                errors:
                  - code: VALIDATION_ERROR
                    message: Asset is required
    Unauthorized:
      description: Missing or invalid API key / JWT.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: API key inactive, expired, or customer disabled.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            transferNotFound:
              value:
                status: false
                errors:
                  - code: TRANSFER_NOT_FOUND
                    message: Transfer was not found
    InternalError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
security:
  - BearerAuth: []
webhooks:
  inquiryCallback:
    post:
      tags:
        - Callbacks
      operationId: webhookInquiry
      summary: INQUIRY callback
      description: |
        Delivered to the beneficiary VASP after a successful
        `transfers/initiate` in `OPEN_VASP` flow. Respond with **2xx**
        to acknowledge; non-2xx responses are retried per the
        schedule in [Callbacks](#tag/Callbacks).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InquiryCallbackBody'
      responses:
        '200':
          description: Acknowledged.
  inquiryResolutionCallback:
    post:
      tags:
        - Callbacks
      operationId: webhookInquiryResolution
      summary: INQUIRY_RESOLUTION callback
      description: >-
        Delivered to the originator VASP once the beneficiary VASP resolves the
        inquiry.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InquiryResolutionRequest'
      responses:
        '200':
          description: Acknowledged.
  transferResolutionCallback:
    post:
      tags:
        - Callbacks
      operationId: webhookTransferResolution
      summary: TRANSFER_RESOLUTION callback
      description: |
        Delivered to the originator's `callback` URL once the
        beneficiary VASP approves the transfer. The body includes a
        `callback` field — the URL to which the originator must
        later `POST` the on-chain `txId`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferResolutionCallbackBody'
      responses:
        '200':
          description: Acknowledged.
  transferConfirmationCallback:
    post:
      tags:
        - Callbacks
      operationId: webhookTransferConfirmation
      summary: TRANSFER_CONFIRMATION callback
      description: |
        Delivered to the beneficiary's stored confirmation URL once
        the originator submits `txId`. Final stage of the Travel
        Rule exchange.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmationCallbackBody'
      responses:
        '200':
          description: Acknowledged.
paths:
  /identity:
    get:
      tags:
        - Identity
      operationId: getVaspIdentity
      summary: Public VASP identity
      description: |
        Returns public identity attributes of the VASP that owns the
        requested hostname. Used by counterparties to verify our
        legal name, LEI and signing key.
      security: []
      responses:
        '200':
          description: VASP identity payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VaspIdentity'
              example:
                name: KYCAID TRP
                lei: 529900T8BM49AURSDO55
                x509: |
                  -----BEGIN PUBLIC KEY-----
                  MIIBIjANBgkqh…
                  -----END PUBLIC KEY-----
        '404':
          $ref: '#/components/responses/NotFound'
  /health:
    get:
      tags:
        - Identity
      operationId: getHealth
      summary: Health check
      description: >-
        Liveness probe. Returns the running build version and the server
        timestamp.
      security: []
      responses:
        '200':
          description: Service is up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: true
                data:
                  version: 26.4.0
                  timestamp: '2026-05-11T08:00:00.000Z'
        '500':
          $ref: '#/components/responses/InternalError'
  /version.json:
    get:
      tags:
        - Identity
      operationId: getVersionJson
      summary: Build manifest
      description: >-
        Returns the contents of `version.json` if present (CI build manifest);
        otherwise an empty object.
      security: []
      responses:
        '200':
          description: Build manifest.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '500':
          $ref: '#/components/responses/InternalError'
  /auth/token:
    post:
      tags:
        - Auth
      operationId: createToken
      summary: Generate JWT access token
      description: |
        Exchanges your API Key for a short-lived **JWT access token**
        (`tokenType: Bearer`, TTL ≈ 1 hour).

        Send the API Key in **one** of these headers:

        - `x-api-key: <YOUR_API_KEY>`, or
        - `Authorization: ApiKey <YOUR_API_KEY>`.

        The response includes both the absolute expiration timestamp
        and the relative TTL in seconds.
      security:
        - ApiKey: []
        - ApiKeyAuth: []
      responses:
        '200':
          description: A new JWT access token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  accessToken:
                    type: string
                    description: JWT signed with the TRP service key.
                  expiresIn:
                    type: integer
                    description: TTL in seconds.
                    example: 3600
                  expiresAt:
                    type: string
                    format: date-time
                  tokenType:
                    type: string
                    const: Bearer
              example:
                accessToken: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9…
                expiresIn: 3600
                expiresAt: '2026-05-11T10:00:00.000Z'
                tokenType: Bearer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /travel-address/generate:
    post:
      tags:
        - Address
      operationId: generateTravelAddress
      summary: Generate Travel Address
      description: |
        Creates (or upserts) a beneficiary applicant on **your** VASP
        and returns a routable **Travel Address** that other VASPs
        can use to deliver IVMS101 beneficiary data back to you.

        The Travel Address encodes:

        - your VASP `domain`,
        - the beneficiary's `publicId`,
        - the lookup path `/transfers/inquiry/{beneficiaryPublicId}`,
        - the `t=i` query (inquiry routing hint).

        If an applicant with the same `externalId` or
        (`firstName`,`lastName`,`walletAddress`) tuple already
        exists, it is reused — Travel Addresses are stable per
        beneficiary.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TravelAddressGenerateRequest'
            examples:
              naturalPersonMinimal:
                summary: Natural person — minimal
                value:
                  personType: natural
                  firstName: Alice
                  lastName: Liddell
              naturalPersonFull:
                summary: Natural person — all accepted fields
                value:
                  personType: natural
                  firstName: Alice
                  lastName: Liddell
                  externalId: cust_123
                  walletAddress: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
                  isSelfHosted: false
              naturalSelfHosted:
                summary: Natural person — self-hosted wallet
                value:
                  personType: natural
                  firstName: Bob
                  lastName: Marley
                  externalId: cust_124
                  walletAddress: '0xabcdef1234567890abcdef1234567890abcdef12'
                  isSelfHosted: true
              legalEntityFull:
                summary: Legal entity — all accepted fields
                value:
                  personType: legal
                  legalName: Wonderland Holdings Ltd
                  firstName: Alice
                  lastName: Liddell
                  externalId: org_001
                  walletAddress: '0xfeedbeefcafebabe1234567890abcdef12345678'
                  isSelfHosted: false
      responses:
        '200':
          description: A unique Travel Address.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TravelAddressGenerateResponse'
              example:
                status: true
                data:
                  travelAddress: ta1qexamplebeneficiaryvasp1ahjk23
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /travel-address/decode:
    post:
      tags:
        - Address
      operationId: decodeTravelAddress
      summary: Decode Travel Address
      description: |
        Verifies and decodes a Travel Address back into its target
        URL. Use this client-side to preview where a transfer will
        be routed before initiating it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TravelAddressDecodeRequest'
            example:
              travelAddress: ta1qexamplebeneficiaryvasp1ahjk23
      responses:
        '200':
          description: Decoded target URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TravelAddressDecodeResponse'
              example:
                status: true
                data:
                  parsedUrl: >-
                    example.com/transfers/inquiry/0193abc4-1234-7890-abcd-ef1234567890?t=i
        '400':
          description: Invalid Travel Address format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                status: false
                errors:
                  - code: INVALID_TRAVEL_ADDRESS
                    message: Invalid travel address format
        '401':
          $ref: '#/components/responses/Unauthorized'
  /transfers/initiate:
    post:
      tags:
        - Transfer
      operationId: initiateTransfer
      summary: Initiate Travel Rule transfer
      description: |
        **Main Travel Rule operation.** Submits originator and
        beneficiary applicants, asset, amount, and optional
        routing info (`travelAddress` or `email`).

        ### Flow selection

        | Provided fields | Resulting `flow` |
        |-----------------|------------------|
        | `travelAddress` | `OPEN_VASP` |
        | only `email` (top-level or `beneficiary.email`) | `EMAIL` |

        ### What TRP does next

        - Persists the transfer with `APPROVED` originator status.
        - Resolves originator and beneficiary VASP IDs.
        - In `OPEN_VASP` flow, queues an `INQUIRY` callback to the
          beneficiary VASP endpoint (extracted from the Travel
          Address).
        - In `EMAIL` flow, sends a confirmation email and flips
          `isEmailDelivered` once Mailjet acks.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferInitiateRequest'
            examples:
              naturalToNaturalFull:
                summary: Natural → Natural — full payload (every accepted field)
                description: |
                  Demonstrates every field the backend accepts on
                  `originator` and `beneficiary` for the natural-person
                  case. Pare this down to the minimum your flow needs.
                value:
                  travelAddress: ta1qexamplebeneficiaryvasp1ahjk23
                  asset: BTC
                  amount: '0.05'
                  callback: https://my-vasp.com/trp/callbacks
                  dti: 4H95J0R2X
                  originator:
                    personType: natural
                    externalId: cust_42
                    firstName: Bob
                    lastName: Marley
                    middleName: Robert
                    dateOfBirth: '1985-02-06'
                    placeOfBirth: Kingston
                    addressType: HOME
                    streetName: Hope Road
                    buildingNumber: '56'
                    buildingName: Tuff Gong House
                    postCode: KIN 6
                    townName: Kingston
                    country: JM
                    countrySubDivision: Saint Andrew
                    nationalId: A12345678
                    nationalIdType: CCPT
                    countryOfIssue: JM
                    accountNumber: ACC-ORIG-001
                    walletAddress: bc1qoriginatoraddressxx
                    email: bob@example.com
                    isSelfHosted: false
                    vasp:
                      name: My VASP Inc.
                  beneficiary:
                    personType: natural
                    externalId: cust_77
                    firstName: Alice
                    lastName: Liddell
                    middleName: Pleasance
                    dateOfBirth: '1990-01-15'
                    placeOfBirth: London
                    addressType: HOME
                    streetName: Baker Street
                    buildingNumber: 221B
                    postCode: NW1 6XE
                    townName: London
                    country: GB
                    countrySubDivision: England
                    nationalId: PB1234567
                    nationalIdType: CCPT
                    countryOfIssue: GB
                    walletAddress: bc1qbeneficiaryaddressxx
                    email: alice@example.com
                    isSelfHosted: false
                    vasp:
                      name: Wonderland Exchange
              legalToLegalFull:
                summary: Legal → Legal — full payload (юр → юр)
                description: |
                  Both parties are legal entities. `legalName` is the
                  required field; LEI is conventionally carried in
                  `nationalId` with `nationalIdType: LEIX`.
                value:
                  travelAddress: ta1qexamplebeneficiaryvasp1ahjk23
                  asset: USDT
                  amount: '50000'
                  callback: https://my-vasp.com/trp/callbacks
                  dti: 4H95J0R2X
                  originator:
                    personType: legal
                    externalId: org_orig_1
                    legalName: Originator Holdings Ltd
                    firstName: Bob
                    lastName: Marley
                    addressType: BIZZ
                    streetName: King William St
                    buildingNumber: '1'
                    postCode: EC4N 7AF
                    townName: London
                    country: GB
                    nationalId: 529900T8BM49AURSDO55
                    nationalIdType: LEIX
                    countryOfIssue: GB
                    accountNumber: ACC-ORIG-CORP
                    walletAddress: 0xoriginatororgaddress01234567890abcdef0
                    email: ops@originator.com
                    isSelfHosted: false
                    vasp:
                      name: Originator VASP Ltd
                  beneficiary:
                    personType: legal
                    externalId: org_bene_1
                    legalName: Wonderland Holdings Ltd
                    addressType: BIZZ
                    streetName: Marina Boulevard
                    buildingNumber: '10'
                    postCode: '018983'
                    townName: Singapore
                    country: SG
                    nationalId: 254900EXAMPLEBENE7GH7
                    nationalIdType: LEIX
                    countryOfIssue: SG
                    walletAddress: 0xbeneficiaryorgaddress01234567890abcde
                    isSelfHosted: false
                    vasp:
                      name: Wonderland Exchange
              naturalToLegal:
                summary: Natural → Legal (физ → юр)
                description: >-
                  Mixed flow — individual sending funds to a corporate
                  beneficiary.
                value:
                  travelAddress: ta1qexamplebeneficiaryvasp1ahjk23
                  asset: ETH
                  amount: '12.5'
                  callback: https://my-vasp.com/trp/callbacks
                  originator:
                    personType: natural
                    firstName: Bob
                    lastName: Marley
                    dateOfBirth: '1985-02-06'
                    country: JM
                    walletAddress: 0xoriginatoraddress01234567890abcdef0abc
                    email: bob@example.com
                  beneficiary:
                    personType: legal
                    legalName: Wonderland Holdings Ltd
                    country: SG
                    nationalId: 254900EXAMPLEBENE7GH7
                    nationalIdType: LEIX
                    walletAddress: 0xbeneficiaryorgaddress01234567890abcde
                    vasp:
                      name: Wonderland Exchange
              emailFlowSelfHosted:
                summary: EMAIL flow (self-hosted natural beneficiary)
                description: >-
                  No Travel Address — TRP emails the beneficiary for
                  self-attestation.
                value:
                  asset: ETH
                  amount: '1.25'
                  callback: https://my-vasp.com/trp/callbacks
                  originator:
                    personType: natural
                    firstName: Bob
                    lastName: Marley
                    country: JM
                    walletAddress: 0xoriginatoraddress
                  beneficiary:
                    personType: natural
                    firstName: Alice
                    lastName: Liddell
                    email: alice@example.com
                    walletAddress: 0xbeneficiaryaddress
                    isSelfHosted: true
      responses:
        '200':
          description: Transfer accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferInitiateResponse'
              example:
                status: true
                data:
                  transferId: 0193abc4-1234-7890-abcd-ef1234567890
                  status: PENDING
                  createdAt: '2026-05-11T08:00:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /transfers/inquiry/{beneficiaryPublicId}:
    post:
      tags:
        - Compliance
      operationId: transferInquiry
      summary: Receive inquiry from another VASP
      description: |
        **Inbound** endpoint. Called by an *originator* VASP (via
        Travel Address routing) to ask whether our VASP holds the
        beneficiary identified by `beneficiaryPublicId`.

        If the beneficiary exists, TRP:

        1. creates / approves a corresponding `Transfer` record,
        2. queues an `INQUIRY_RESOLUTION` callback to the
           caller-provided `callback` URL with the beneficiary
           wallet address.

        Authentication is **not** required — request integrity is
        guaranteed by the originator's signed `x-data-integrity`
        header and by the inclusion of the encoded Travel Address.
      security: []
      parameters:
        - $ref: '#/components/parameters/BeneficiaryPublicId'
        - $ref: '#/components/parameters/ApiVersionHeader'
        - $ref: '#/components/parameters/RequestIdentifierHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferInquiryRequest'
            example:
              asset: BTC
              amount: '0.05'
              IVMS101:
                originator:
                  originatorPersons:
                    - naturalPerson:
                        name:
                          nameIdentifier:
                            - primaryIdentifier: Marley
                              secondaryIdentifier: Bob
                              nameIdentifierType: LEGL
                originatingVASP:
                  originatingVASP:
                    legalPerson:
                      name:
                        nameIdentifier:
                          - legalPersonName: Originator VASP Ltd
                            legalPersonNameIdentifierType: LEGL
                      nationalIdentification:
                        nationalIdentifier: 529900T8BM49AURSDO55
                        nationalIdentifierType: LEIX
              callback: https://originator-vasp.com/trp/inquiry-resolution?q=0193abc4-…
      responses:
        '200':
          description: Inquiry accepted; an `INQUIRY_RESOLUTION` callback will follow.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
              example:
                status: true
        '400':
          $ref: '#/components/responses/BadRequest'
  /transfers/inquiryResolution:
    post:
      tags:
        - Compliance
      operationId: transferInquiryResolution
      summary: Receive inquiry resolution
      description: |
        **Inbound** endpoint. Called by the *beneficiary* VASP to
        respond to a previously issued `INQUIRY` callback.

        The request locates the transfer by the `q` query
        parameter (transfer `publicId`), stores the approved
        wallet address against the beneficiary applicant, marks
        the transfer as `APPROVED`, and queues a
        `TRANSFER_RESOLUTION` callback to the originator VASP.
      security: []
      parameters:
        - $ref: '#/components/parameters/TransferQueryId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InquiryResolutionRequest'
            examples:
              plainAddress:
                summary: Plain wallet string
                value:
                  approved: bc1qbeneficiaryaddressxx
                  callback: https://originator-vasp.com/trp/transfer-resolution
              structured:
                summary: Structured object with callback override
                value:
                  approved:
                    address: bc1qbeneficiaryaddressxx
                    callback: bc1qbeneficiaryaddressxx
                  callback: https://originator-vasp.com/trp/transfer-resolution
      responses:
        '200':
          description: Resolution stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'
  /transfers/txId:
    post:
      tags:
        - Compliance
      operationId: transferTxId
      summary: Broadcast on-chain txId
      description: |
        Called by the **originator** VASP once the on-chain
        transaction has been broadcast. Stores `txId` on the
        transfer and forwards a `TRANSFER_CONFIRMATION` callback to
        the beneficiary's stored confirmation URL.
      parameters:
        - $ref: '#/components/parameters/TransferQueryId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferTxIdRequest'
            example:
              txId: 5b1c8f7d9a4e7c3b…
      responses:
        '200':
          description: TxId stored and confirmation queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Transfer or confirmation callback not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                transferMissing:
                  value:
                    status: false
                    errors:
                      - code: TRANSFER_NOT_FOUND
                        message: Transfer was not found
                callbackMissing:
                  value:
                    status: false
                    errors:
                      - code: CALLBACK_NOT_FOUND
                        message: Transfer confirmation callback was not found
  /transfers/confirmation:
    post:
      tags:
        - Compliance
      operationId: transferConfirmation
      summary: Receive transfer confirmation
      description: |
        **Inbound** endpoint that receives the final
        `TRANSFER_CONFIRMATION` callback. Persists the on-chain
        `txId` against the transfer.

        Also used as the landing endpoint for self-hosted (EMAIL
        flow) beneficiaries when they click the confirmation link.
      security: []
      parameters:
        - $ref: '#/components/parameters/TransferQueryId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferConfirmationRequest'
            example:
              txId: 5b1c8f7d9a4e7c3b…
      responses:
        '200':
          description: Confirmation stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'
  /transfers/{transferPublicId}/comments:
    get:
      tags:
        - Transfer
      operationId: listTransferComments
      summary: List comments on a transfer
      description: |
        Returns every comment attached to the transfer (originator
        and beneficiary VASPs share the same thread). Attachment
        downloads are signed, short-lived URLs.
      parameters:
        - $ref: '#/components/parameters/TransferPublicId'
      responses:
        '200':
          description: List of comments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: boolean
                    const: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TransferComment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
        - Transfer
      operationId: createTransferComment
      summary: Add a comment (with optional attachments)
      description: |
        Adds a comment to a transfer thread. Up to **5 attachments**
        per call (each ≤ 50 MB). Only images and PDFs are accepted —
        other MIME types are rejected with `Only images and PDF files
        are allowed`.
      parameters:
        - $ref: '#/components/parameters/TransferPublicId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TransferCommentCreateMultipart'
      responses:
        '200':
          description: Comment created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: boolean
                    const: true
                  data:
                    $ref: '#/components/schemas/TransferComment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /transfers/{transferPublicId}/comments/{commentId}:
    patch:
      tags:
        - Transfer
      operationId: updateTransferComment
      summary: Edit a comment
      description: |
        Edits a comment **you previously authored**. Other parties'
        comments can be read but not modified.
      parameters:
        - $ref: '#/components/parameters/TransferPublicId'
        - $ref: '#/components/parameters/CommentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferCommentUpdateRequest'
            example:
              text: Updated comment text.
      responses:
        '200':
          description: Comment updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: boolean
                    const: true
                  data:
                    $ref: '#/components/schemas/TransferComment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Only the comment author can edit this comment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                status: false
                errors:
                  - code: FORBIDDEN
                    message: Only the comment author can edit this comment
        '404':
          $ref: '#/components/responses/NotFound'
  /discovery/vasp:
    get:
      tags:
        - Discovery
      operationId: searchVasp
      summary: Search VASP directory
      description: |
        Returns up to **100** VASPs matching the supplied criteria
        (case-insensitive `contains` match, combined with **AND**).
        At least one search parameter is required.

        > Although this is `GET`, the search payload is read from
        > the **request body** (compatibility with the original TRP
        > registry — bodies on `GET` are tolerated).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VaspSearchRequest'
            examples:
              byDomain:
                summary: By domain
                value:
                  domain: example.com
              byLei:
                summary: By LEI
                value:
                  lei: 529900T8BM49AURSDO55
      responses:
        '200':
          description: Search results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      count:
                        type: integer
                      vasps:
                        type: array
                        items:
                          $ref: '#/components/schemas/Vasp'
        '400':
          description: No search parameters supplied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                status: false
                errors:
                  - code: INVALID_REQUEST
                    message: >-
                      At least one search parameter is required (domain, name,
                      email, commonName, directoryId, or lei)
        '500':
          $ref: '#/components/responses/InternalError'
