openapi: 3.0.3
info:
  title: Realpad Accounting Software API
  version: "10"
  description: |
    WS-style API endpoints for synchronizing Incoming Payments from accounting software into Realpad CRM.

    ## Terminology
    In this documentation, "developer" refers to a **real estate developer** — the primary customer of Realpad. In our multi-tenant SaaS architecture, each real estate developer is a **tenant**. The terms "developer" and "tenant" are used interchangeably. A developer/tenant ID identifies the tenant and scopes all data access.

    A **Deal** is a sales transaction (reservation, contract) between a developer and a customer for a Unit (flat, parking spot, etc.). Deals have **Prescribed Payments** (expected payment schedule) and **Incoming Payments** (actual received payments). An **Unmatched Payment** is a received payment that could not be automatically matched to any Deal.

    ## Authentication
    All endpoints require `login` and `password` sent as form-encoded POST parameters. There is no Bearer token or API key mechanism. Credentials are issued per developer and use case (e.g., `acme-accounting` / `acme-sap-import`).

    ## Common Behavior
    - All endpoints accept **HTTP POST** only.
    - Parameters are form-encoded (`application/x-www-form-urlencoded`) for the active endpoint, or `multipart/form-data` for the deprecated file-upload endpoints.
    - Error responses are **plain text** with appropriate HTTP status codes.
    - Unexpected parameters cause HTTP 400 (unless the endpoint explicitly allows extras).
    - Rate-limited endpoints return HTTP 429 with a `Retry-After` header.

    ## Payment Matching Algorithm
    When a payment is submitted via `/add-incoming-payment`, the system attempts to match it to an existing Deal using the following cascade:

    1. **Prescribed Payment ID**: If `prescribedid` is provided and valid, the payment is matched to the Deal that owns that Prescribed Payment.
    2. **Variable symbol + Project**: If `symvar` and `projectid` are both provided, looks for a Deal with that pairing symbol in the given Project.
    3. **Variable symbol only**: If `symvar` is provided without `projectid`, looks for a Deal with that pairing symbol across all Projects.
    4. **Unmatched**: If none of the above yields a match (or `symvar` is empty), the payment is stored as an Unmatched Payment for manual resolution in the Realpad UI.

    When a Deal is found, the system also attempts to match the payment to a specific Prescribed Payment by amount (if `prescribedid` was not provided).

    ## De-duplication
    Before attempting to match, the system checks if a payment with the same external ID (`id` parameter) already exists — either as a matched Incoming Payment or an Unmatched Payment. If found, the existing payment's ID(s) are returned with HTTP 200, and no new payment is created. This makes the endpoint safe to call repeatedly with the same data.

    ## VAT Calculation
    - If `amountnovat` is provided, VAT is calculated as `amount - amountnovat`.
    - Otherwise, VAT is auto-calculated based on the matched Deal's VAT configuration.
    - VAT is only calculated when the payment is matched to a Deal.

    ## Response Format Notes
    The active `/add-incoming-payment` endpoint returns **plain text** responses (just an integer ID or IDs separated by newlines). The deprecated file-upload endpoints return plain-text summaries with a trailing `<!-- Time taken: X ms. -->` XML comment (server-side processing time, safe to ignore).

    **Note on Content-Type header:** The WS framework sends `Content-Type: application/xml;charset=UTF-8` for all responses, including plain-text ones. Integrators should parse the body as plain text regardless of this header.
  contact:
    name: Realpad Development Team
    url: https://dev.realpadsoftware.com
    email: support@realpadsoftware.com
servers:
  - url: https://test.realpad.eu/ws/v10
    description: Staging
  - url: https://cms.realpad.eu/ws/v10
    description: Production
  - url: https://crm-2.realpad.eu/ws/v10
    description: Production 2
tags:
  - name: Accounting
    description: Active endpoint for incoming payment synchronization from accounting software.
  - name: Accounting (Deprecated)
    description: Legacy file-upload endpoints for importing payments from specific accounting software formats (Pohoda, SAP). Use `/add-incoming-payment` instead.

# ===========================================================================
# PATHS
# ===========================================================================
paths:

  # =========================================================================
  # ACCOUNTING
  # =========================================================================

  /add-incoming-payment:
    post:
      tags: [Accounting]
      summary: Add an incoming payment
      operationId: addIncomingPayment
      description: |
        Submits a single incoming payment for matching to a Deal or storage as an Unmatched Payment.

        See the **Payment Matching Algorithm** and **De-duplication** sections in the API description for detailed behavior.

        The response status code indicates the outcome:
        - **201 Created** — a new payment was created (either matched to a Deal or stored as Unmatched).
        - **200 OK** — a payment with the same external ID already exists (duplicate, no action taken).

        **Permission required:** `WRITE_FINANCIAL_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required:
                    - id
                    - timestamp
                    - amount
                  properties:
                    id:
                      type: string
                      description: |
                        External ID — a unique identifier from the accounting system. Used for de-duplication: if a payment with this ID already exists (matched or unmatched), the endpoint returns HTTP 200 with the existing ID(s) instead of creating a new payment.
                    projectid:
                      type: integer
                      description: Project ID to scope the Deal matching. If omitted, matching by variable symbol is attempted across all Projects.
                    timestamp:
                      type: string
                      format: date-time
                      description: "Payment date/time in ISO 8601 format with timezone offset (e.g., `2026-03-10T14:00:00+01:00`)."
                    accountno:
                      type: string
                      description: "Sender's bank account number."
                    accountbank:
                      type: string
                      description: "Sender's bank code. Combined with `accountno` as `accountno/accountbank` in the stored payment."
                    accountholder:
                      type: string
                      description: Name of the account holder.
                    amount:
                      type: string
                      description: "Payment amount including VAT. Decimal number with dot as decimal separator, no thousands separator (e.g., `150000.00`). Parsed locale-independently."
                    amountnovat:
                      type: string
                      description: "Payment amount excluding VAT. Same format as `amount`. If provided, VAT is calculated as `amount - amountnovat`. If omitted, VAT is auto-calculated from the matched Deal's configuration."
                    prescribedid:
                      type: integer
                      description: ID of a specific Prescribed Payment to match against. If provided and valid, the payment is matched to the Deal owning this Prescribed Payment, bypassing variable symbol matching.
                    symvar:
                      type: string
                      description: Variable symbol (pairing symbol) used to match the payment to a Deal. This is the primary matching field when `prescribedid` is not provided.
                    symconst:
                      type: string
                      description: Constant symbol (Czech/Slovak banking convention). Stored with the payment but not used for matching.
                    symspec:
                      type: string
                      description: Specific symbol (Czech/Slovak banking convention). Stored with the payment but not used for matching.
                    note:
                      type: string
                      description: Free-text note attached to the payment.
                    bankaccountid:
                      type: integer
                      description: "ID of the developer's Bank Account that received the payment. If the Bank Account is configured with \"all payments are custom changes\", the payment is automatically marked as a custom change payment."
                    customchangepayment:
                      type: string
                      description: "Set to `true` (or any truthy value) to mark this payment as a custom change payment. Also automatically set if the referenced Bank Account has that flag enabled."
            example:
              login: acme-accounting
              password: s3cret
              id: "PAY-2026-001"
              timestamp: "2026-03-10T14:00:00+01:00"
              amount: "150000.00"
              symvar: "12345"
              projectid: 3356887
      responses:
        '201':
          description: |
            Payment created successfully. The body contains the ID of the newly created payment record (plain-text integer).

            This status is returned both when the payment is matched to a Deal (stored as an Incoming Payment) and when it cannot be matched (stored as an Unmatched Payment).
          content:
            text/plain:
              schema:
                type: string
              example: "42"
        '200':
          description: |
            Duplicate detected — a payment with the same external ID (`id`) already exists. The body contains the existing payment ID(s), one per line.

            No new payment is created. This makes the endpoint idempotent with respect to the external ID.
          content:
            text/plain:
              schema:
                type: string
              examples:
                single:
                  summary: Single existing payment
                  value: "42\n"
                multiple:
                  summary: Multiple existing payments with same external ID
                  value: "42\n43\n"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # =========================================================================
  # ACCOUNTING (DEPRECATED)
  # =========================================================================

  /add-payments-pohoda:
    post:
      tags: [Accounting (Deprecated)]
      summary: Import payments from Pohoda XML export
      operationId: addPaymentsPohoda
      deprecated: true
      description: |
        **DEPRECATED: Use `/add-incoming-payment` instead.**

        Parses a Pohoda bank export XML file and attempts to match the payments contained within to Deals based on the variable symbol.

        The file must be uploaded as `multipart/form-data`. Optionally, a `projectid` can be provided to scope matching to a specific Project.

        **Permission required:** `WRITE_FINANCIAL_DATA`
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required:
                    - file
                  properties:
                    projectid:
                      type: integer
                      description: Project ID to scope payment matching. If omitted or invalid, matching is attempted across all Projects.
                    file:
                      type: string
                      format: binary
                      description: Pohoda bank export XML file.
      responses:
        '200':
          description: |
            Payments processed successfully. The body contains a plain-text summary of matched, unmatched, and skipped payments, followed by a `<!-- Time taken: X ms. -->` XML comment.

            Note: the `Content-Type` header is `application/xml` despite the body being primarily plain text.
          content:
            application/xml:
              schema:
                type: string
              example: |
                3 payments imported. 2 matched, 1 unmatched.
                <!-- Time taken: 142 ms. -->
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /add-payments-sap:
    post:
      tags: [Accounting (Deprecated)]
      summary: Import payments from SAP HTML export
      operationId: addPaymentsSap
      deprecated: true
      description: |
        **DEPRECATED: Use `/add-incoming-payment` instead.**

        Parses a SAP export HTML file and attempts to match the payments contained within to Deals based on the variable symbol.

        The file must be uploaded as `multipart/form-data`. No additional parameters are supported beyond authentication.

        **Permission required:** `WRITE_FINANCIAL_DATA`
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required:
                    - file
                  properties:
                    file:
                      type: string
                      format: binary
                      description: SAP export HTML file.
      responses:
        '200':
          description: |
            Payments processed successfully. The body contains a plain-text summary of matched, unmatched, and skipped payments, followed by a `<!-- Time taken: X ms. -->` XML comment.

            Note: the `Content-Type` header is `application/xml` despite the body being primarily plain text.
          content:
            application/xml:
              schema:
                type: string
              example: |
                5 payments imported. 3 matched, 2 unmatched.
                <!-- Time taken: 256 ms. -->
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

# ===========================================================================
# COMPONENTS
# ===========================================================================
components:
  schemas:
    AuthFields:
      type: object
      required:
        - login
        - password
      properties:
        login:
          type: string
          description: API username.
        password:
          type: string
          format: password
          description: API password.

  responses:
    Unauthorized:
      description: Authentication failed — invalid credentials, insufficient permissions, or IP/account restriction.
      content:
        text/plain:
          schema:
            type: string
          example: "Not authorized."

    BadRequest:
      description: Missing required parameters, invalid parameter format, unexpected parameters, or invalid IDs.
      content:
        text/plain:
          schema:
            type: string
          examples:
            missing:
              summary: Required parameter missing
              value: "Parameters missing."
            format:
              summary: Invalid parameter format
              value: "Incorrect parameter format."
            extra:
              summary: Unexpected parameters
              value: "Unexpected parameters found."
            invalidId:
              summary: Invalid object ID
              value: "ID does not refer to a valid object."

    Forbidden:
      description: User must accept the privacy/data processing consent in the Realpad web interface before calling the API.
      content:
        text/plain:
          schema:
            type: string
          example: >-
            Please log into Realpad web interface and provide your consent
            with us processing your private data.

    TooManyRequests:
      description: Rate limited. Wait for the duration indicated in the response body or the `Retry-After` header before retrying.
      content:
        text/plain:
          schema:
            type: string
          example: "You can call this endpoint again after 2026-03-10T14:35:00.000+01:00"
      headers:
        Retry-After:
          description: Seconds until the next allowed call.
          schema:
            type: integer

    ServerError:
      description: Internal server error.
      content:
        text/plain:
          schema:
            type: string
          example: "Server error, check logs."
