openapi: 3.0.3

info:
  title: RiskPulse API
  version: "1.3"
  description: |
    Geopolitical risk API for developers and compliance teams.
    Aggregates sanctions lists (EU, OFAC, UN, UK, Canada, Australia),
    travel advisories (BuZa, US State Dept, UK FCDO), political stability
    and corruption scores (World Bank), adverse media (GDELT), and armed
    conflict data (ACLED) into a single JSON response per country.

    ## Authentication
    All endpoints require an `X-Api-Key` header except the public endpoints listed below.

    **Public endpoints (no key required):**
    - `GET /api/v1/status`
    - `POST /api/v1/waitlist`
    - `GET /api/v1/risk/sectors`

    ## Tier limits
    | Tier       | Calls/month | Batch | History | CSDDD |
    |------------|-------------|-------|---------|-------|
    | Starter    | 500         | No    | No      | No    |
    | Growth     | 5,000       | Yes   | Yes     | Yes   |
    | Enterprise | Unlimited   | Yes   | Yes     | Yes   |

servers:
  - url: https://api.riskpulse.io
    description: Production
  - url: http://localhost:8080
    description: Local development

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key

  schemas:
    RiskProfile:
      type: object
      description: Full risk profile for a country
      properties:
        isoCode:
          type: string
          example: IR
          description: ISO 3166-1 alpha-2 country code
        countryName:
          type: string
          example: Iran
          description: Full country name in English
        aggregatedRisk:
          type: number
          format: float
          minimum: 1.0
          maximum: 10.0
          example: 8.4
          description: |
            Weighted risk score from 1.0 (lowest) to 10.0 (critical).
            Default formula: travel×40% + sanctions×35% + stability×25%.
            Optional components (media, corruption, conflict) reduce base weights proportionally.
        travelAdvisoryScore:
          type: integer
          minimum: 1
          maximum: 9
          example: 9
          description: |
            Blended travel advisory score from BuZa, US State Dept, and UK FCDO.
            1 = Safe, 3 = Caution, 6 = High risk, 9 = Do not travel.
        sanctionedEu:
          type: boolean
          example: true
          description: Country is on the EU Financial Sanctions list (FSF)
        sanctionedOfac:
          type: boolean
          example: true
          description: Country is on the US OFAC SDN list
        sanctionedUn:
          type: boolean
          example: true
          description: Country is on the UN Consolidated Sanctions list
        sanctionedUk:
          type: boolean
          example: true
          description: Country is on the UK FCDO Sanctions list (UKSL)
        sanctionedCa:
          type: boolean
          example: false
          description: Country is on Canada's SEMA sanctions list
        sanctionedAu:
          type: boolean
          example: false
          description: Country is on Australia's DFAT consolidated sanctions list
        stabilityScore:
          type: number
          format: float
          minimum: -2.5
          maximum: 2.5
          example: -1.8
          description: |
            World Bank Political Stability Index (PV.EST).
            Range: -2.5 (most unstable) to +2.5 (most stable).
        mediaSignal:
          type: number
          format: float
          minimum: 0.0
          maximum: 2.5
          nullable: true
          example: 1.4
          description: |
            GDELT adverse-media signal (0–2.5). Null until GdeltMediaJob has run.
            Only included in aggregated_risk when app.scoring.media-signal-enabled=true.
        corruptionScore:
          type: number
          format: float
          minimum: -2.5
          maximum: 2.5
          nullable: true
          example: -0.8
          description: |
            World Bank Control of Corruption indicator (CC.EST, -2.5 to +2.5).
            Null until CorruptionScoreJob has run.
            Only included in aggregated_risk when app.scoring.corruption-enabled=true.
        conflictSignal:
          type: number
          format: float
          minimum: 0.0
          maximum: 10.0
          nullable: true
          example: 2.1
          description: |
            ACLED armed conflict signal (0–10). Null until AcledConflictJob has run.
            Only included in aggregated_risk when app.scoring.conflict-signal-enabled=true.
        regionalWarning:
          type: string
          nullable: true
          example: rood
          description: Regional travel advisory color (rood/oranje/geel) when a country has a regional split
        regionalWarningArea:
          type: string
          nullable: true
          example: het oosten van Oekraïne
          description: Name of the region with elevated travel advisory
        sectorUsed:
          type: string
          nullable: true
          example: NGO
          description: Sector preset applied to this response (null when standard weights were used)
        lastUpdated:
          type: string
          format: date-time
          nullable: true
          example: "2026-03-22T02:05:00Z"
        sources:
          type: object
          description: Timestamp of last update per data source
          properties:
            buza:
              type: string
              format: date-time
              nullable: true
              example: "2026-03-22T06:00:00Z"
            stateDept:
              type: string
              format: date-time
              nullable: true
              example: "2026-03-22T06:30:00Z"
            fcdo:
              type: string
              format: date-time
              nullable: true
              example: "2026-03-22T06:45:00Z"
            euSanctions:
              type: string
              format: date-time
              nullable: true
              example: "2026-03-22T02:00:00Z"
            ofac:
              type: string
              format: date-time
              nullable: true
              example: "2026-03-22T02:30:00Z"
            worldBank:
              type: string
              format: date-time
              nullable: true
              example: "2026-03-01T03:00:00Z"

    ScoreResponse:
      type: object
      properties:
        isoCode:
          type: string
          example: IR
        score:
          type: number
          example: 8.4

    HistoryEntry:
      type: object
      properties:
        date:
          type: string
          format: date
          example: "2026-03-17"
        aggregatedRisk:
          type: number
          example: 8.2

    SectorPreset:
      type: object
      description: Sector-specific scoring weight preset
      properties:
        sector:
          type: string
          enum: [TECHNOLOGY, LOGISTICS, FINANCIAL, MANUFACTURING, ENERGY, NGO]
        travel:
          type: number
          format: float
          example: 0.60
        sanctions:
          type: number
          format: float
          example: 0.20
        stability:
          type: number
          format: float
          example: 0.20

    CsdddReport:
      type: object
      description: CSDDD-ready country risk assessment (EU Directive 2024/1760)
      properties:
        isoCode:
          type: string
          example: CN
        countryName:
          type: string
          example: China
        reportDate:
          type: string
          format: date
          example: "2026-03-22"
        directive:
          type: string
          example: "EU CSDDD 2024/1760"
        overallRisk:
          type: string
          enum: [NEGLIGIBLE, LOW, MEDIUM, HIGH, SEVERE]
          example: HIGH
        indicators:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/CsdddIndicator"
        assessmentNotes:
          type: string
        disclaimer:
          type: string

    CsdddIndicator:
      type: object
      properties:
        name:
          type: string
          example: "Travel Advisory Level"
        value:
          type: string
          example: "6"
          description: The raw value, or "DATA_UNAVAILABLE" when the data source has not run
        riskContribution:
          type: string
          example: "HIGH"
        source:
          type: string
          example: "BuZa / US State Dept / UK FCDO"

    Error:
      type: object
      properties:
        error:
          type: string
          example: Country not found

paths:
  /api/v1/risk/sectors:
    get:
      summary: Sector weight presets
      description: Returns all available sector-specific scoring weight presets. No API key required.
      operationId: getSectors
      security: []
      tags: [Risk]
      responses:
        "200":
          description: List of sector presets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SectorPreset"

  /api/v1/risk:
    get:
      summary: All countries
      description: Returns risk profiles for all countries, sorted by risk (high to low).
      operationId: getAllRisks
      tags: [Risk]
      parameters:
        - name: minScore
          in: query
          schema:
            type: number
          description: Filter countries with risk score >= minScore
        - name: maxScore
          in: query
          schema:
            type: number
          description: Filter countries with risk score <= maxScore
        - name: sanctionedEu
          in: query
          schema:
            type: boolean
        - name: sanctionedOfac
          in: query
          schema:
            type: boolean
        - name: sanctionedUn
          in: query
          schema:
            type: boolean
        - name: sanctionedUk
          in: query
          schema:
            type: boolean
        - name: sanctionedCa
          in: query
          schema:
            type: boolean
        - name: sanctionedAu
          in: query
          schema:
            type: boolean
        - name: sort
          in: query
          schema:
            type: string
            enum: [risk_asc, risk_desc]
            default: risk_desc
        - name: sector
          in: query
          schema:
            type: string
            enum: [TECHNOLOGY, LOGISTICS, FINANCIAL, MANUFACTURING, ENERGY, NGO]
          description: Apply sector-specific weights to recalculate aggregatedRisk
      responses:
        "200":
          description: List of risk profiles
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RiskProfile"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/risk/{isoCode}:
    get:
      summary: Single country
      description: Returns the full risk profile for a single country.
      operationId: getRisk
      tags: [Risk]
      parameters:
        - name: isoCode
          in: path
          required: true
          description: ISO 3166-1 alpha-2 or alpha-3 country code (e.g. `IR`, `RU`, `NLD`)
          schema:
            type: string
            minLength: 2
            maxLength: 3
            example: IR
        - name: sector
          in: query
          schema:
            type: string
            enum: [TECHNOLOGY, LOGISTICS, FINANCIAL, MANUFACTURING, ENERGY, NGO]
          description: Apply sector-specific weights to recalculate aggregatedRisk
      responses:
        "200":
          description: Risk profile for the country
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RiskProfile"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Country not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/risk/{isoCode}/score:
    get:
      summary: Score only
      description: Returns only the aggregated risk score. Useful for polling or monitoring.
      operationId: getScore
      tags: [Risk]
      parameters:
        - name: isoCode
          in: path
          required: true
          schema:
            type: string
            example: IR
        - name: sector
          in: query
          schema:
            type: string
            enum: [TECHNOLOGY, LOGISTICS, FINANCIAL, MANUFACTURING, ENERGY, NGO]
      responses:
        "200":
          description: Risk score
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScoreResponse"
        "404":
          description: Country not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/risk/{isoCode}/breakdown:
    get:
      summary: Score breakdown
      description: |
        Returns a detailed breakdown of how each component contributed to the final score.
        **Requires Starter tier or higher.**
      operationId: getBreakdown
      tags: [Risk]
      parameters:
        - name: isoCode
          in: path
          required: true
          schema:
            type: string
            example: IR
        - name: sector
          in: query
          schema:
            type: string
            enum: [TECHNOLOGY, LOGISTICS, FINANCIAL, MANUFACTURING, ENERGY, NGO]
      responses:
        "200":
          description: Score breakdown
          content:
            application/json:
              schema:
                type: object
        "403":
          description: Tier too low (Starter or higher required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/risk/{isoCode}/csddd:
    get:
      summary: CSDDD compliance report
      description: |
        Returns a CSDDD-ready (EU Corporate Sustainability Due Diligence Directive 2024/1760)
        country risk assessment. Packages existing RiskPulse data as a structured compliance report
        with 6 indicators and an overall risk level.
        **Requires Growth tier or higher.**
      operationId: getCsddd
      tags: [Risk]
      parameters:
        - name: isoCode
          in: path
          required: true
          schema:
            type: string
            example: CN
      responses:
        "200":
          description: CSDDD compliance report
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CsdddReport"
        "403":
          description: Tier too low (Growth or higher required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Country not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/risk/{isoCode}/history:
    get:
      summary: Historical scores
      description: |
        Returns risk scores for the past 90 days, newest first.
        **Requires Growth tier or higher.**
      operationId: getHistory
      tags: [Risk]
      parameters:
        - name: isoCode
          in: path
          required: true
          schema:
            type: string
            example: IR
      responses:
        "200":
          description: List of historical scores (newest first)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/HistoryEntry"
        "403":
          description: Tier too low (Growth or higher required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/risk/batch:
    post:
      summary: Batch request
      description: |
        Fetch risk profiles for multiple countries at once (max 50).
        **Requires Growth tier or higher.**
      operationId: getBatch
      tags: [Risk]
      parameters:
        - name: sector
          in: query
          schema:
            type: string
            enum: [TECHNOLOGY, LOGISTICS, FINANCIAL, MANUFACTURING, ENERGY, NGO]
          description: Apply sector-specific weights to all countries in the batch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              maxItems: 50
              items:
                type: string
              example: ["IR", "RU", "SY", "KP"]
      responses:
        "200":
          description: List of risk profiles
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RiskProfile"
        "403":
          description: Tier too low (Growth or higher required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/portal/me:
    get:
      summary: Account info
      description: Returns information about the current account, including tier and quota usage.
      operationId: getMe
      tags: [Account]
      responses:
        "200":
          description: Account information
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  email:
                    type: string
                  tier:
                    type: string
                    enum: [FREE, STARTER, GROWTH, ENTERPRISE]
                  callsThisMonth:
                    type: integer
                  monthlyQuota:
                    type: integer
                  quotaResetAt:
                    type: string
                    format: date-time
                  webhookUrl:
                    type: string

  /api/v1/portal/rotate-key:
    post:
      summary: Rotate API key
      description: |
        Rotates the current API key. The old key is immediately invalidated.
        The new raw key is returned only once and never stored in plaintext.
      operationId: rotateKey
      tags: [Account]
      responses:
        "200":
          description: New API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKey:
                    type: string
                    example: or_live_abc123def456

  /api/v1/portal/webhook:
    put:
      summary: Set webhook URL
      description: |
        Set a webhook URL for risk score changes.
        **Requires Growth tier or higher.**
      operationId: updateWebhook
      tags: [Account]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                  example: "https://your-app.com/riskpulse-webhook"
      responses:
        "200":
          description: Webhook saved
        "403":
          description: Tier too low

  /api/v1/status:
    get:
      summary: Service status
      description: |
        Returns the sync status of all data sources and an overall operational/degraded flag.
        No API key required. Cached for 5 minutes (Cache-Control: max-age=300).
      operationId: getStatus
      security: []
      tags: [Status]
      responses:
        "200":
          description: Service status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [operational, degraded]
                  sources:
                    type: object
                  generatedAt:
                    type: string
                    format: date-time

tags:
  - name: Risk
    description: Country risk profiles and scoring
  - name: Account
    description: Account and quota management
  - name: Status
    description: Service health and data source sync status
