openapi: "3.0.3"
info:
  title: SGPayNowQR API
  version: "1.0"
  description: |
    Generate EMVCo-compliant Singapore PayNow QR codes.
    Supports UEN, mobile number, and VPA payment types.
    Version is in the URL path (/api/v1/).
  contact:
    name: SGPayNowQR
    url: https://www.sgpaynowqr.com
  x-sdk:
    typescript:
      package: "@alexhalfborg/sgpaynowqr"
      repository: https://github.com/alexhalfborg/sgpaynowqr-sdk
      install: npm install @alexhalfborg/sgpaynowqr

servers:
  - url: https://developers.sgpaynowqr.com/api/v1
    description: Production

security:
  - ApiKeyAuth: []

paths:
  /generate:
    post:
      summary: Generate a PayNow QR code
      operationId: generateQR
      description: |
        Generate an EMVCo-compliant PayNow QR code for Singapore payments.
        Supports UEN, mobile number, and VPA payment types.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateRequest"
            examples:
              uen:
                summary: UEN payment
                value:
                  payment_type: uen
                  uen: "201234567A"
                  merchant_name: "My Company"
                  amount: 25.00
                  reference: "INV001"
              mobile:
                summary: Mobile payment
                value:
                  payment_type: mobile
                  mobile_number: "+6591234567"
                  amount: 10.50
              vpa:
                summary: VPA payment
                value:
                  payment_type: vpa
                  vpa: "+6591234567#OCBC"
                  amount: 5.00
                  reference: "DONATION"
      responses:
        "200":
          description: QR code generated successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenerateResponse"
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              description: Monthly request limit for this account (shared across all its API keys)
            X-RateLimit-Remaining:
              schema:
                type: integer
              description: Remaining requests this month
            X-RateLimit-Reset:
              schema:
                type: string
                format: date-time
              description: When the rate limit resets (1st of next month, SGT)
            X-Request-Id:
              schema:
                type: string
              description: Unique request identifier
        "400":
          description: Validation error, invalid JSON, or invalid content type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                validation:
                  value:
                    success: false
                    error:
                      code: VALIDATION_ERROR
                      message: "Missing required fields for the selected payment_type"
                    meta:
                      api_version: v1
                      request_id: req_abc123def456
                invalid_json:
                  value:
                    success: false
                    error:
                      code: INVALID_JSON
                      message: "Malformed JSON body"
                    meta:
                      api_version: v1
                      request_id: req_abc123def456
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: "Invalid API key"
                meta:
                  api_version: v1
                  request_id: req_abc123def456
        "429":
          description: Monthly rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                success: false
                error:
                  code: RATE_LIMIT_EXCEEDED
                  message: "Monthly usage limit exceeded"
                meta:
                  api_version: v1
                  request_id: req_abc123def456
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    options:
      summary: CORS preflight
      operationId: generateQROptions
      security: []
      responses:
        "204":
          description: No content (CORS headers returned)

  /health:
    get:
      summary: Health check
      operationId: healthCheck
      description: Returns API health status. No authentication required.
      security: []
      responses:
        "200":
          description: Health status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"

  /usage:
    get:
      summary: Get quota usage
      operationId: getUsage
      description: |
        Returns the tier and quota usage for the account that owns the API key.
        Does not count toward the monthly quota, so it is safe to call to check
        that a key is valid.
      responses:
        "200":
          description: Usage for the current period
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key in format: sgpn_<32 hex characters>"

  schemas:
    GenerateRequest:
      type: object
      required:
        - payment_type
        - amount
      properties:
        payment_type:
          type: string
          enum: [uen, mobile, vpa]
          description: Type of PayNow payment
        uen:
          type: string
          maxLength: 25
          description: UEN number (required if payment_type is "uen")
        merchant_name:
          type: string
          maxLength: 25
          description: Merchant name (required if payment_type is "uen")
        mobile_number:
          type: string
          pattern: "^(\\+65)?[89]\\d{7}$"
          description: Singapore mobile number (required if payment_type is "mobile")
        vpa:
          type: string
          description: Virtual Payment Address (required if payment_type is "vpa")
        amount:
          type: number
          minimum: 0.01
          maximum: 999999.99
          description: Payment amount in SGD
        reference:
          type: string
          maxLength: 25
          pattern: "^[a-zA-Z0-9]*$"
          description: Optional alphanumeric reference
        expiry:
          type: string
          enum: ["1h", "2h", "6h", "12h", "24h", "none"]
          default: "24h"
          description: QR code expiry duration
        qr_color:
          type: string
          pattern: "^[0-9a-fA-F]{6}$"
          default: "7d1979"
          description: "QR code color as 6-digit hex (without #)"
        qr_size:
          type: integer
          enum: [200, 300, 400]
          default: 300
          description: QR code image size in pixels
        include_image:
          type: boolean
          default: true
          description: Whether to include base64-encoded PNG image in response

    GenerateResponse:
      type: object
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          properties:
            qr_string:
              type: string
              description: EMVCo-compliant QR code string
            qr_image_base64:
              type: string
              description: Base64-encoded PNG image (only if include_image=true)
            image_mime_type:
              type: string
              enum: ["image/png"]
              description: Image MIME type (only if include_image=true)
            image_url:
              type: string
              format: uri
              description: |
                Signed link to the QR code PNG, at the requested size and colour. Always returned.
                Useful where base64 is awkward, such as AI agents replying in chat or emails that
                link to the image. The link stops working if the payload, size or colour is changed.
            payment_type:
              type: string
              enum: [uen, mobile, vpa]
            amount:
              type: string
              description: Amount as string with 2 decimal places
            currency:
              type: string
              enum: [SGD]
            reference:
              type: string
              nullable: true
              description: Reference string or null if not provided
            expiry:
              type: string
              enum: ["1h", "2h", "6h", "12h", "24h", "none"]
          required:
            - qr_string
            - image_url
            - payment_type
            - amount
            - currency
            - reference
            - expiry
        meta:
          $ref: "#/components/schemas/Meta"

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - VALIDATION_ERROR
                - INVALID_JSON
                - INVALID_CONTENT_TYPE
                - UNAUTHORIZED
                - RATE_LIMIT_EXCEEDED
                - INTERNAL_ERROR
            message:
              type: string
          required:
            - code
            - message
        meta:
          type: object
          properties:
            api_version:
              type: string
            request_id:
              type: string

    UsageResponse:
      type: object
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          properties:
            tier:
              type: string
              enum: [free, starter, pro, enterprise]
            used:
              type: integer
              description: Successful requests this period, summed across all of the account's keys
            limit:
              type: integer
            remaining:
              type: integer
            period:
              type: string
              description: Quota period as YYYY-MM (Singapore time)
            reset:
              type: string
              format: date-time
              description: Start of the next period
          required: [tier, used, limit, remaining, period, reset]
        meta:
          type: object
          properties:
            api_version:
              type: string
            request_id:
              type: string

    HealthResponse:
      type: object
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          properties:
            status:
              type: string
              enum: [ok, degraded]
              description: '"ok" when all systems operational, "degraded" when database is unavailable'
            version:
              type: string
            timestamp:
              type: string
              format: date-time
          required:
            - status
            - version
            - timestamp
        meta:
          $ref: "#/components/schemas/Meta"

    Meta:
      type: object
      properties:
        api_version:
          type: string
          example: v1
        request_id:
          type: string
          example: req_abc123def456
        usage:
          type: object
          description: Only present on /generate responses
          properties:
            used:
              type: integer
              description: Requests used this month
            limit:
              type: integer
              description: Monthly limit for this tier
            period:
              type: string
              description: Current billing period (YYYY-MM)
