openapi: 3.1.0
info:
  title: JOD Payment Processing API
  version: 1.0.0
  description: Synchronous committed transfers, immutable balanced ledger, JWT scopes
    and account authorization.
servers:
- url: https://payments.example.invalid
paths:
  /api/v1/payments:
    post:
      operationId: submitPayment
      summary: Atomically transfer JOD with exactly-once financial effect per paymentId
      security:
      - bearerAuth: []
      description: Requires payments:submit and source ownership or explicit debit
        grant. Identical terminal replay returns original receipt/rejection; different
        instruction conflicts. Responses follow Accept JSON/XML. Only purpose and
        notes are trimmed. Old instructions may be replayed after the time window
        if already recorded.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Payment'
            example:
              paymentId: d92192df-f79a-4553-8120-5660d2eb1294
              fromAccount: ACC-1001
              toAccount: ACC-2002
              amount: '125.750'
              date: '2026-10-28T13:30:00+03:00'
              purpose: Invoice payment INV-2048
              notes: October services
          application/xml:
            schema:
              $ref: '#/components/schemas/Payment'
            example: <?xml version="1.0"?><payment><paymentId>d92192df-f79a-4553-8120-5660d2eb1294</paymentId><fromAccount>ACC-1001</fromAccount><toAccount>ACC-2002</toAccount><amount>125.750</amount><date>2026-10-28T13:30:00+03:00</date><purpose>Invoice
              payment INV-2048</purpose><notes>October services</notes></payment>
      responses:
        '400': &id003
          description: MALFORMED_MESSAGE or VALIDATION_FAILED
          headers: &id002
            X-Correlation-ID: &id001
              description: Server-generated request correlation ID
              schema:
                type: string
                format: uuid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '401': &id004
          description: UNAUTHENTICATED
          headers:
            X-Correlation-ID: *id001
            WWW-Authenticate:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '403': &id005
          description: FORBIDDEN
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '404': &id006
          description: NOT_FOUND or ACCOUNT_NOT_FOUND when safe to disclose
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '406': &id007
          description: NOT_ACCEPTABLE
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '409': &id008
          description: ACCOUNT_NOT_ACTIVE, INSUFFICIENT_FUNDS, IDEMPOTENCY_CONFLICT,
            PAYMENT_LIMIT_EXCEEDED
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '413': &id009
          description: PAYLOAD_TOO_LARGE
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '415': &id010
          description: UNSUPPORTED_MEDIA_TYPE
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '429': &id011
          description: RATE_LIMITED
          headers:
            X-Correlation-ID: *id001
            Retry-After:
              schema:
                type: integer
              description: Suggested retry delay in seconds
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '500': &id012
          description: INTERNAL_ERROR
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '503': &id013
          description: TEMPORARILY_UNAVAILABLE; query or retry original ID
          headers:
            X-Correlation-ID: *id001
            Retry-After:
              schema:
                type: integer
              description: Suggested retry delay in seconds
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                $ref: '#/components/schemas/Error'
        '201':
          description: First committed transfer
          headers:
            X-Correlation-ID: *id001
            Location:
              schema:
                type: string
              description: /api/v1/payments/{paymentId}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Receipt'
            application/xml:
              schema:
                $ref: '#/components/schemas/Receipt'
        '200':
          description: Identical completed replay, original receipt
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Receipt'
            application/xml:
              schema:
                $ref: '#/components/schemas/Receipt'
  /api/v1/payments/{paymentId}:
    get:
      operationId: getPayment
      summary: Requires payments:read; unrelated or unknown payments return 404
      security:
      - bearerAuth: []
      parameters:
      - name: paymentId
        in: path
        required: true
        schema:
          type: string
      responses:
        '400': *id003
        '401': *id004
        '403': *id005
        '404': *id006
        '406': *id007
        '409': *id008
        '413': *id009
        '415': *id010
        '429': *id011
        '500': *id012
        '503': *id013
        '200':
          description: Authorized persisted result
          headers: *id002
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/Receipt'
                - $ref: '#/components/schemas/Error'
            application/xml:
              schema:
                oneOf:
                - $ref: '#/components/schemas/Receipt'
                - $ref: '#/components/schemas/Error'
  /api/v1/accounts/{accountId}/balance:
    get:
      operationId: getBalance
      summary: Requires payments:read and account ownership/debit authorization
      security:
      - bearerAuth: []
      parameters:
      - name: accountId
        in: path
        required: true
        schema:
          type: string
      responses:
        '400': *id003
        '401': *id004
        '403': *id005
        '404': *id006
        '406': *id007
        '409': *id008
        '413': *id009
        '415': *id010
        '429': *id011
        '500': *id012
        '503': *id013
        '200':
          description: Authorized persisted result
          headers: *id002
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Balance'
            application/xml:
              schema:
                $ref: '#/components/schemas/Balance'
  /api/v1/admin/reconciliation:
    get:
      operationId: reconcile
      summary: Requires payments:admin; repeatable-read ledger/balance consistency
        report
      security:
      - bearerAuth: []
      responses:
        '400': *id003
        '401': *id004
        '403': *id005
        '404': *id006
        '406': *id007
        '409': *id008
        '413': *id009
        '415': *id010
        '429': *id011
        '500': *id012
        '503': *id013
        '200':
          description: issues array with payment and account inconsistencies; empty
            when consistent
  /health/live:
    get:
      responses:
        '200':
          description: Process is alive
  /health/ready:
    get:
      responses:
        '200':
          description: PostgreSQL reachable
        '503':
          description: PostgreSQL unavailable
  /metrics:
    get:
      summary: Requires payments:admin; Prometheus metrics
      security:
      - bearerAuth: []
      responses:
        '200':
          description: Prometheus exposition
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT RS256
  schemas:
    Payment:
      type: object
      additionalProperties: false
      required:
      - paymentId
      - fromAccount
      - toAccount
      - amount
      - date
      - purpose
      properties:
        paymentId:
          type: string
          format: uuid
        fromAccount:
          type: string
          maxLength: 100
        toAccount:
          type: string
          maxLength: 100
        amount:
          oneOf:
          - type: number
            exclusiveMinimum: 0
            maximum: 10000
            multipleOf: 0.001
          - type: string
            pattern: ^[0-9]+(\.[0-9]{1,3})?$
        date:
          type: string
          format: date-time
        purpose:
          type: string
          minLength: 1
          maxLength: 255
        notes:
          type: string
          maxLength: 1000
      xml:
        name: payment
    Receipt:
      type: object
      required:
      - paymentId
      - status
      - fromAccount
      - toAccount
      - amount
      - currency
      - processedAt
      properties:
        paymentId:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - COMPLETED
        fromAccount:
          type: string
        toAccount:
          type: string
        amount:
          type: string
          pattern: ^[0-9]+\.[0-9]{3}$
        currency:
          type: string
          enum:
          - JOD
        processedAt:
          type: string
          format: date-time
      xml:
        name: response
    Error:
      type: object
      required:
      - status
      - code
      - message
      - issues
      - correlationId
      - timestamp
      properties:
        paymentId:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - REJECTED
          - ERROR
        code:
          type: string
        message:
          type: string
        issues:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              code:
                type: string
              message:
                type: string
        correlationId:
          type: string
          format: uuid
        timestamp:
          type: string
          format: date-time
      xml:
        name: response
    Balance:
      type: object
      properties:
        accountId:
          type: string
        currency:
          type: string
          enum:
          - JOD
        balance:
          type: string
          pattern: ^[0-9]+\.[0-9]{3}$
