{
  "openapi": "3.1.0",
  "info": {
    "title": "Tender Fraud Prevention SA — Public API",
    "version": "1.0.0",
    "summary": "Public, unauthenticated, anonymised JSON endpoints for South African tender-fraud intelligence.",
    "description": "The public data surface of Tender Fraud Prevention SA (TFPSA). Every endpoint here is unauthenticated, rate-limited, and returns only anonymised, POPIA-compliant aggregates or a single-domain recognition verdict. No endpoint returns personal information, an email address, a phone number, or names any person or organisation. Authenticated account endpoints are intentionally NOT described here. Informational only — not legal or financial advice.",
    "contact": {
      "name": "Tender Fraud Prevention SA",
      "url": "https://tenderfraud.co.za/contact-us"
    },
    "license": {
      "name": "Terms of use",
      "url": "https://tenderfraud.co.za/terms"
    }
  },
  "servers": [
    { "url": "https://tenderfraud.co.za", "description": "Production" }
  ],
  "externalDocs": {
    "description": "Human-readable API documentation",
    "url": "https://tenderfraud.co.za/api-docs"
  },
  "tags": [
    { "name": "domains", "description": "Government-domain recognition" },
    { "name": "intelligence", "description": "Anonymised fraud aggregates" },
    { "name": "platform", "description": "Platform metadata" }
  ],
  "paths": {
    "/api/public/domain-lookup": {
      "get": {
        "tags": ["domains"],
        "summary": "Recognise a government domain (query alias)",
        "description": "Reports whether a domain is a recognised SA government / state-owned-entity / municipal domain. Never enumerates or discloses the corpus. Rate limited to 30 requests/min per IP.",
        "operationId": "getDomainLookup",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "description": "The domain to classify (or any address's domain).",
            "schema": { "type": "string", "maxLength": 253 },
            "example": "example.gov.za"
          }
        ],
        "responses": {
          "200": {
            "description": "Classification result.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DomainLookupResult" },
                "example": {
                  "status": "recognized",
                  "message": "Recognized government domain. Recognition is not a guarantee of legitimacy — always verify through official channels before acting.",
                  "reviewAvailable": false
                }
              }
            }
          },
          "400": {
            "description": "Missing or non-plausible domain.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "description": "Rate limit exceeded (see Retry-After)." },
          "503": {
            "description": "Reference data temporarily unavailable — never a guessed verdict.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Unavailable" } } }
          }
        }
      },
      "post": {
        "tags": ["domains"],
        "summary": "Recognise a government domain (JSON body)",
        "description": "Identical recognition check to the GET alias, taking the domain in a JSON body. Rate limited to 30 requests/min per IP.",
        "operationId": "postDomainLookup",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["domain"],
                "properties": {
                  "domain": { "type": "string", "maxLength": 253, "example": "example.gov.za" }
                }
              },
              "example": { "domain": "example.gov.za" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Classification result.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DomainLookupResult" } } }
          },
          "400": {
            "description": "Missing or non-plausible domain.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "description": "Rate limit exceeded (see Retry-After)." },
          "503": {
            "description": "Reference data temporarily unavailable.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Unavailable" } } }
          }
        }
      }
    },
    "/api/public/fraud-map": {
      "get": {
        "tags": ["intelligence"],
        "summary": "Anonymised provincial fraud-activity map",
        "description": "Provincial fraud-activity aggregates behind the live map: per-province counts and severity, plus metro hotspots. Never names a domain, organisation or person. Rate limited to 60 requests/min per IP.",
        "operationId": "getFraudMap",
        "parameters": [
          {
            "name": "window",
            "in": "query",
            "required": false,
            "description": "Time window for the aggregates. Defaults to 24h.",
            "schema": { "type": "string", "enum": ["24h", "7d", "30d", "90d"], "default": "24h" }
          }
        ],
        "responses": {
          "200": {
            "description": "Anonymised provincial aggregates.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FraudMapOverview" } } }
          },
          "429": { "description": "Rate limit exceeded." },
          "500": {
            "description": "Aggregates could not be loaded.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/public/fraud-trend": {
      "get": {
        "tags": ["intelligence"],
        "summary": "Anonymised fraud-detection trend",
        "description": "Monthly fraud-detection trend (trailing months) plus current-month impact figures. Scalar aggregates only. Rate limited to 60 requests/min per IP.",
        "operationId": "getFraudTrend",
        "responses": {
          "200": {
            "description": "Trend series and current-month impact.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FraudTrend" } } }
          },
          "429": { "description": "Rate limit exceeded." },
          "500": {
            "description": "Trend could not be loaded.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/public/impact-stats": {
      "get": {
        "tags": ["intelligence"],
        "summary": "Aggregate platform impact counters",
        "description": "Aggregate impact counters: emails verified, fraud blocked, RFQs analysed, and the measured detection rate (null when no signal exists). Scalar counts only. Rate limited to 60 requests/min per IP.",
        "operationId": "getImpactStats",
        "responses": {
          "200": {
            "description": "Aggregate counters.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImpactStats" } } }
          },
          "429": { "description": "Rate limit exceeded." },
          "500": {
            "description": "Counters could not be loaded.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/public/changelog": {
      "get": {
        "tags": ["platform"],
        "summary": "Published release notes",
        "description": "Published product release notes as structured JSON. Rate limited to 60 requests/min per IP.",
        "operationId": "getPublicChangelog",
        "responses": {
          "200": {
            "description": "Published releases (may be an empty list).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChangelogResponse" } } }
          },
          "429": { "description": "Rate limit exceeded." }
        }
      }
    },
    "/api/public/notices": {
      "get": {
        "tags": ["platform"],
        "summary": "Published public notices",
        "description": "Currently-live (published and not retired) operator-authored public notices as structured JSON. Rate limited to 60 requests/min per IP.",
        "operationId": "getPublicNotices",
        "responses": {
          "200": {
            "description": "Published notices (may be an empty list).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicNoticesResponse" } } }
          },
          "429": { "description": "Rate limit exceeded." }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } }
      },
      "Unavailable": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["unavailable"] },
          "message": { "type": "string" }
        }
      },
      "DomainLookupResult": {
        "type": "object",
        "required": ["status", "message", "reviewAvailable"],
        "properties": {
          "status": { "type": "string", "enum": ["recognized", "unrecognized", "lookalike"] },
          "message": { "type": "string" },
          "reviewAvailable": { "type": "boolean" }
        }
      },
      "FraudMapProvince": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "example": "Gauteng" },
          "count": { "type": "integer" },
          "severity": { "type": "string", "enum": ["high", "medium", "low"] }
        }
      },
      "FraudMapHotspot": {
        "type": "object",
        "properties": {
          "province": { "type": "string", "example": "Gauteng" },
          "metro": { "type": "string", "example": "City of Johannesburg" },
          "reports": { "type": "integer" },
          "trend": { "type": "string", "enum": ["up", "down", "flat"] },
          "trendDelta": { "type": "string" }
        }
      },
      "FraudMapOverview": {
        "type": "object",
        "properties": {
          "window": { "type": "string", "enum": ["24h", "7d", "30d", "90d"] },
          "provinces": { "type": "array", "items": { "$ref": "#/components/schemas/FraudMapProvince" } },
          "hotspots": { "type": "array", "items": { "$ref": "#/components/schemas/FraudMapHotspot" } },
          "topSpoofedDomain": {
            "type": "null",
            "description": "Always null — the public map never names a domain or organisation."
          },
          "generatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "FraudTrendPoint": {
        "type": "object",
        "properties": {
          "month": { "type": "string", "example": "2026-07" },
          "label": { "type": "string", "example": "JUL" },
          "verified": { "type": "integer" },
          "fraud": { "type": "integer" }
        }
      },
      "FraudTrend": {
        "type": "object",
        "properties": {
          "trend": { "type": "array", "items": { "$ref": "#/components/schemas/FraudTrendPoint" } },
          "impact": {
            "type": "object",
            "properties": {
              "verificationsThisMonth": { "type": "integer" },
              "fraudThisMonth": { "type": "integer" },
              "verificationsMomChange": { "type": ["number", "null"] }
            }
          },
          "generatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "ImpactStats": {
        "type": "object",
        "properties": {
          "emailsVerified": { "type": "integer" },
          "fraudBlocked": { "type": "integer" },
          "rfqsAnalysed": { "type": "integer" },
          "detectionRate": { "type": ["number", "null"] },
          "generatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "ChangeEntry": {
        "type": "object",
        "properties": {
          "category": { "type": "string", "enum": ["Added", "Changed", "Fixed", "Security", "Deprecated"] },
          "body": { "type": "string" }
        }
      },
      "Release": {
        "type": "object",
        "properties": {
          "version": { "type": "string", "example": "1.4.0" },
          "title": { "type": "string" },
          "date": { "type": "string", "example": "2026-07-01" },
          "isLatest": { "type": "boolean" },
          "topics": { "type": "array", "items": { "type": "string" } },
          "entries": { "type": "array", "items": { "$ref": "#/components/schemas/ChangeEntry" } },
          "migrationNotes": { "type": ["string", "null"] },
          "fullNotesHref": { "type": ["string", "null"] },
          "apiDiffHref": { "type": ["string", "null"] },
          "githubHref": { "type": ["string", "null"] }
        }
      },
      "ChangelogResponse": {
        "type": "object",
        "properties": {
          "releases": { "type": "array", "items": { "$ref": "#/components/schemas/Release" } }
        }
      },
      "PublicNotice": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "type": { "type": "string", "enum": ["status", "maintenance", "changelog", "other"] },
          "title": { "type": "string" },
          "content": { "type": "string" },
          "publishedAt": { "type": "string" }
        }
      },
      "PublicNoticesResponse": {
        "type": "object",
        "properties": {
          "notices": { "type": "array", "items": { "$ref": "#/components/schemas/PublicNotice" } }
        }
      }
    }
  }
}
