openapi: 3.1.0
info:
  title: "Savvy Rilla FX API"
  summary: "South Sudan foreign exchange market data and analytics API."
  description: "Savvy Rilla FX API provides South Sudanese Pound exchange-rate data, market summaries, structured commentary, API-key protected access, rate limiting, caching and developer tooling."
  version: 1.0.0
  contact:
    name: "Savvy Rilla"
    url: https://fx.savvyrilla.tech
    email: "admin@savvygorilla.tech"
  license:
    name: Proprietary
servers:
  - url: https://fx.savvyrilla.tech
    description: Production
  - url: http://localhost:3000
    description: "Local development"
tags:
  - name: Health
    description: "Operational health and service diagnostics."
  - name: Rates
    description: "Latest, recent and historical FX rates."
  - name: Currencies
    description: "Supported currency reference data."
  - name: "Market Summary"
    description: "Market analytics, conditions scoring, and structured commentary."
  - name: Export
    description: "Downloadable FX datasets."
security:
  - ApiKeyHeader:
[]
  - BearerApiKey:
[]
paths:
  /api/health:
    get:
      tags:
        - Health
      summary: "Health check"
      description: "Returns service health, environment and database connectivity status."
      security:
[]
      responses:
        200:
          description: "Service health response."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      status:
                        type: string
                        example: healthy
                      db:
                        type: boolean
                      service:
                        type: string
                        example: "Savvy Rilla FX API"
                      environment:
                        type: string
                        example: production
                      uptimeSeconds:
                        type: integer
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/rates/latest:
    get:
      tags:
        - Rates
      summary: "Latest FX rates"
      description: "Returns the latest SSP-per-unit rates for supported quote currencies."
      parameters:
        - name: base
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: SSP
          description: "Base currency code."
      responses:
        200:
          description: "Latest exchange rates."
          headers:
            ETag:
              schema:
                type: string
              description: "Entity tag for conditional requests."
            Last-Modified:
              schema:
                type: string
              description: "Last modification date for the response resource."
            X-Cache:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
              description: "Whether the response was served from the API cache."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - "$ref": "#/components/schemas/LatestRates"
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/rates/{quote}/latest:
    get:
      tags:
        - Rates
      summary: "Latest FX rate by quote"
      description: "Returns the latest rate for SSP or supplied base currency against the path quote currency."
      parameters:
        - name: quote
          in: path
          required: true
          schema:
            type: string
            minLength: 3
            maxLength: 3
            example: USD
          description: "Quote currency code."
        - name: base
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: SSP
          description: "Base currency code."
      responses:
        200:
          description: "Latest exchange rate."
          headers:
            ETag:
              schema:
                type: string
              description: "Entity tag for conditional requests."
            Last-Modified:
              schema:
                type: string
              description: "Last modification date for the response resource."
            X-Cache:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
              description: "Whether the response was served from the API cache."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - "$ref": "#/components/schemas/ExchangeRate"
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/rates/recent:
    get:
      tags:
        - Rates
      summary: "Recent FX rates"
      description: "Returns recent rates for a base currency, optionally restricted to the newest available date."
      parameters:
        - name: base
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: SSP
          description: "Base currency code."
        - name: quote
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: USD
          description: "Quote currency code."
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: "Maximum number of records to return."
        - name: latest_only
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: "When true, returns every rate from the newest available date. The limit parameter is ignored."
      responses:
        200:
          description: "Recent exchange rates."
          headers:
            ETag:
              schema:
                type: string
              description: "Entity tag for conditional requests."
            Last-Modified:
              schema:
                type: string
              description: "Last modification date for the response resource."
            X-Cache:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
              description: "Whether the response was served from the API cache."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - "$ref": "#/components/schemas/RateConvention"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          "$ref": "#/components/schemas/RecentRate"
                      meta:
                        type: object
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/rates/history:
    get:
      tags:
        - Rates
      summary: "Historical FX rates"
      description: "Returns historical chart-ready FX data."
      parameters:
        - name: base
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: SSP
          description: "Base currency code."
        - name: quote
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: USD
          description: "Quote currency code."
        - name: mode
          in: query
          required: false
          schema:
            type: string
            enum:
              - all
              - range
          description: "Use all to return full available history. Otherwise the route defaults to a 365-day window."
        - name: days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
          description: "Rolling window in days when mode is not all."
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 2
            maximum: 50000
            default: 20000
      responses:
        200:
          description: "Historical exchange rate points."
          headers:
            ETag:
              schema:
                type: string
              description: "Entity tag for conditional requests."
            Last-Modified:
              schema:
                type: string
              description: "Last modification date for the response resource."
            X-Cache:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
              description: "Whether the response was served from the API cache."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - "$ref": "#/components/schemas/RateConvention"
                  - type: object
                    properties:
                      points:
                        type: array
                        items:
                          "$ref": "#/components/schemas/ChartPoint"
                      pair:
                        type: string
                        example: SSP/USD
                        description: "Legacy storage-order pair retained for v1 compatibility. Use display_pair for conventional market notation."
                      base:
                        type: string
                        example: SSP
                      quote:
                        type: string
                        example: USD
                      meta:
                        type: object
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/currencies:
    get:
      tags:
        - Currencies
      summary: "Supported currencies"
      description: "Returns supported currency reference data."
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
        - name: active
          in: query
          required: false
          schema:
            type: boolean
            default: true
      responses:
        200:
          description: "Supported currencies."
          headers:
            ETag:
              schema:
                type: string
              description: "Entity tag for conditional requests."
            Last-Modified:
              schema:
                type: string
              description: "Last modification date for the response resource."
            X-Cache:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
              description: "Whether the response was served from the API cache."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          "$ref": "#/components/schemas/Currency"
                      meta:
                        type: object
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/summary/market:
    get:
      tags:
        - "Market Summary"
      summary: "Market summary"
      description: "Returns market summary, trend analysis and Market Health Score."
      parameters:
        - name: base
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: SSP
          description: "Base currency code."
        - name: quote
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: USD
          description: "Quote currency code."
      responses:
        200:
          description: "Market analysis summary."
          headers:
            ETag:
              schema:
                type: string
              description: "Entity tag for conditional requests."
            Last-Modified:
              schema:
                type: string
              description: "Last modification date for the response resource."
            X-Cache:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
              description: "Whether the response was served from the API cache."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - "$ref": "#/components/schemas/MarketSummary"
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/summary/insights:
    get:
      tags:
        - "Market Summary"
      summary: "Market insights"
      description: "Returns structured commentary and interpreted market insights."
      parameters:
        - name: base
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: SSP
          description: "Base currency code."
        - name: quote
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: USD
          description: "Quote currency code."
      responses:
        200:
          description: "Structured market insight response."
          headers:
            ETag:
              schema:
                type: string
              description: "Entity tag for conditional requests."
            Last-Modified:
              schema:
                type: string
              description: "Last modification date for the response resource."
            X-Cache:
              schema:
                type: string
                enum:
                  - HIT
                  - MISS
              description: "Whether the response was served from the API cache."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - "$ref": "#/components/schemas/RateConvention"
                  - type: object
                    properties:
                      pair:
                        type: string
                        example: USD/SSP
                      base:
                        type: string
                        example: SSP
                      quote:
                        type: string
                        example: USD
                      as_of_date:
                        type: string
                        format: date
                      commentary:
                        type: object
                      insights:
                        type: array
                        items:
                          type: string
                      marketHealth:
                        "$ref": "#/components/schemas/MarketHealth"
                      meta:
                        type: object
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
  /api/v1/export/rates:
    get:
      tags:
        - Export
      summary: "Export FX rates"
      description: "Exports FX rates as JSON or CSV depending on supported query parameters."
      parameters:
        - name: base
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: SSP
          description: "Base currency code."
        - name: quote
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
            default: USD
          description: "Quote currency code."
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum:
              - json
              - csv
            default: csv
      responses:
        200:
          description: "Exported rate data."
          content:
            application/json:
              schema:
                allOf:
                  - "$ref": "#/components/schemas/ApiEnvelope"
                  - "$ref": "#/components/schemas/RateConvention"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          "$ref": "#/components/schemas/RecentRate"
                      meta:
                        type: object
            text/csv:
              schema:
                type: string
        400:
          "$ref": "#/components/responses/BadRequest"
        401:
          "$ref": "#/components/responses/Unauthorized"
        429:
          "$ref": "#/components/responses/RateLimited"
        500:
          "$ref": "#/components/responses/ServerError"
components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: "Savvy Rilla FX API key. Example: srfx_live_xxxxxxxxxxxxxxxxx."
    BearerApiKey:
      type: http
      scheme: bearer
      description: "Alternative API key transport using Authorization: Bearer <api-key>."
  schemas:
    ApiEnvelope:
      type: object
      required:
        - success
        - requestId
        - timestamp
        - version
        - durationMs
      properties:
        success:
          type: boolean
        requestId:
          type: string
        timestamp:
          type: string
          format: date-time
        version:
          type: string
          example: v1
        durationMs:
          type: number
    RateConvention:
      type: object
      required:
        - rate_convention
        - unit
      properties:
        rate_convention:
          type: string
          const: base_units_per_quote_unit
          description: "Each mid-rate value is the number of base-currency units equal to one quote-currency unit."
        display_pair:
          type: string
          example: USD/SSP
          description: "Conventional market display pair, when one quote currency is selected."
        unit:
          type: string
          example: "SSP per 1 USD"
    ErrorEnvelope:
      allOf:
        - "$ref": "#/components/schemas/ApiEnvelope"
        - type: object
          required:
            - error
          properties:
            success:
              type: boolean
              const: false
            error:
              type: object
              required:
                - code
                - message
              properties:
                code:
                  type: string
                  example: INVALID_CURRENCY
                message:
                  type: string
                  example: "base and quote must be valid 3-letter currency codes."
                details:
{}
          example:
            success: false
            requestId: req_01JFXAMPLE
            timestamp: 2026-06-29T12:00:00.000Z
            version: v1
            durationMs: 8
            error:
              code: INVALID_CURRENCY
              message: "base and quote must be valid 3-letter currency codes."
    ExchangeRate:
      allOf:
        - "$ref": "#/components/schemas/RateConvention"
        - type: object
          required:
            - pair
            - base
            - quote
            - as_of_date
            - mid_rate
          properties:
            pair:
              type: string
              example: SSP/USD
              description: "Legacy storage-order pair retained for v1 compatibility. Use display_pair for conventional market notation."
            base:
              type: string
              example: SSP
            quote:
              type: string
              example: USD
            as_of_date:
              type: string
              format: date
              example: 2026-06-29
            mid_rate:
              type: number
              example: 4824.005
            change_pct_vs_previous:
              type:
                - number
                - null
              example: 0.12
            is_official:
              type: boolean
            is_manual_override:
              type: boolean
          example:
            rate_convention: base_units_per_quote_unit
            display_pair: USD/SSP
            unit: "SSP per 1 USD"
            pair: SSP/USD
            base: SSP
            quote: USD
            as_of_date: 2026-07-14
            mid_rate: 4824.005
            change_pct_vs_previous: 0.3533
            is_official: true
            is_manual_override: false
    LatestRates:
      allOf:
        - "$ref": "#/components/schemas/RateConvention"
        - type: object
          required:
            - base
            - as_of_date
            - rates
          properties:
            base:
              type: string
              example: SSP
            as_of_date:
              type: string
              format: date
              example: 2026-07-14
            source:
              type: string
              example: fx_daily_rates
            rates:
              type: object
              additionalProperties:
                type: number
              example:
                KES: 37.3235
                USD: 4824.005
    RecentRate:
      type: object
      required:
        - id
        - as_of_date
        - base_currency
        - quote_currency
        - rate_mid
      properties:
        id:
          type: integer
        as_of_date:
          type: string
          format: date
        base_currency:
          type: string
          example: SSP
        quote_currency:
          type: string
          example: USD
        rate_mid:
          type: number
          example: 4824.005
        is_official:
          type:
            - boolean
            - null
        is_manual_override:
          type:
            - boolean
            - null
        source_id:
          type:
            - integer
            - null
    Currency:
      type: object
      required:
        - code
        - name
      properties:
        code:
          type: string
          example: USD
        name:
          type: string
          example: "United States Dollar"
        symbol:
          type:
            - string
            - null
          example: "$"
        decimals:
          type:
            - integer
            - null
          example: 2
        created_at:
          type:
            - string
            - null
          format: date-time
    ChartPoint:
      type: object
      required:
        - date
        - mid
      properties:
        date:
          type: string
          format: date
        mid:
          type: number
    MarketHealth:
      type: object
      required:
        - score
        - status
        - color
      properties:
        score:
          type: integer
          minimum: 0
          maximum: 100
          example: 84
        status:
          type: string
          example: Stable
        color:
          type: string
          example: green
    MarketSummary:
      allOf:
        - "$ref": "#/components/schemas/RateConvention"
        - type: object
          properties:
            pair:
              type: string
              example: USD/SSP
            base:
              type: string
              example: SSP
            quote:
              type: string
              example: USD
            as_of_date:
              type: string
              format: date
            mid_rate:
              type: number
              example: 4824.005
            change_pct_vs_previous:
              type:
                - number
                - null
              example: 0.3533
            trend:
              type: object
            volatility:
              type: object
            changes:
              type: object
            ranges:
              type: object
            averages:
              type: object
            observations:
              type: object
            signals:
              type: object
            marketHealth:
              "$ref": "#/components/schemas/MarketHealth"
            commentary:
              type: object
          example:
            rate_convention: base_units_per_quote_unit
            display_pair: USD/SSP
            unit: "SSP per 1 USD"
            base: SSP
            quote: USD
            pair: USD/SSP
            as_of_date: 2026-07-14
            mid_rate: 4824.005
            change_pct_vs_previous: 0.3533
            trend:
              window_days: 7
              label: Uptrend
              change_pct: 0.8983
            volatility:
              window_days: 30
              avg_daily_move_pct: 0.1734
              label: low
            marketHealth:
              score: 78
              status: Stable
              color: emerald
  responses:
    BadRequest:
      description: "Invalid request parameters."
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
    Unauthorized:
      description: "Missing or invalid API key."
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
    RateLimited:
      description: "Rate limit exceeded."
      headers:
        Retry-After:
          schema:
            type: integer
          description: "Seconds until the client should retry."
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
    ServerError:
      description: "Server-side error."
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorEnvelope"
