{
  "openapi": "3.1.0",
  "info": {
    "title": "Kongfinger API",
    "version": "1.1.0",
    "description": "Live scan of African newspaper front pages as a headline × term matrix, behind kongfinger.vercel.app, plus daily topic snapshots and a weekly directory reachability check. Counts are literal headline counts; starter-pack terms also match curated French, Arabic, Portuguese and Swahili equivalents and every hit reports the variant that matched. Unreachable sites are reported with their error; nothing is estimated. Keyless, CORS-enabled. By Saga Dog Corp · Research-driven AI from Miami.",
    "contact": {
      "name": "Saga Dog Corp",
      "url": "https://ecoquipr.com"
    }
  },
  "servers": [
    {
      "url": "https://kongfinger.vercel.app"
    }
  ],
  "paths": {
    "/api/scan": {
      "get": {
        "summary": "Scan a country's front pages now",
        "description": "Fetches up to 10 of the country's listed outlets in parallel (reachable at the last weekly check first, then 2021 listed-active), ~7 s timeout each. CDN-cached 15 minutes per URL (stale-while-revalidate 1 hour); 400s are no-store.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "Algeria",
                "Angola",
                "Benin",
                "Botswana",
                "Burkina Faso",
                "Burundi",
                "Cameroon",
                "Cape Verde Islands",
                "Central African Republic",
                "Chad",
                "Comoros",
                "Congo-Brazzaville",
                "C�te D'Ivoire",
                "Democratic Republic of Congo",
                "Djibouti",
                "Egypt",
                "Equatorial Guinea",
                "Eritrea",
                "Ethiopia",
                "Gabon",
                "Gambia",
                "Ghana",
                "Guinea",
                "Kenya",
                "Lesotho",
                "Liberia",
                "Libyian Arab Jamahiriya",
                "Madagascar",
                "Malawi",
                "Mali",
                "Mauritania",
                "Mauritius",
                "Morocco",
                "Mozambique",
                "Namibia",
                "Nigeria",
                "Rwanda",
                "R�union",
                "Senegal",
                "Seychelles",
                "Sierra Leone",
                "Somalia",
                "Somaliland",
                "South Africa",
                "South Sudan",
                "Sudan",
                "Swaziland",
                "Tanzania",
                "Togo",
                "Tunisia",
                "Uganda",
                "Western Sahara",
                "Zambia",
                "Zimbabwe"
              ]
            },
            "description": "Case-insensitive"
          },
          {
            "name": "terms",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "election,economy,health,security,climate,China,oil,court"
            },
            "description": "Comma-separated; each 2–30 chars after trimming, duplicates dropped, max 12 used. Matching ignores case and accents, normalizes Arabic letter forms, is whole-word plus up to 3 suffix letters, and allows attached Arabic prefixes (و ف ب ل ك ال). A term found in the starter pack (any of its 5 languages) also matches the rest of its translation row."
          },
          {
            "name": "literal",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1",
                "true",
                "false"
              ],
              "default": "0"
            },
            "description": "1/true: match each term only as typed (no translations)."
          }
        ],
        "responses": {
          "200": {
            "description": "Scan",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "country",
                    "terms",
                    "scanned",
                    "listed",
                    "reachable",
                    "totals",
                    "outlets",
                    "scanned_at",
                    "headlines",
                    "variant_totals",
                    "term_variants",
                    "literal"
                  ],
                  "properties": {
                    "country": {
                      "type": "string"
                    },
                    "terms": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "scanned": {
                      "type": "integer"
                    },
                    "listed": {
                      "type": "integer",
                      "description": "Outlets with a URL in the directory for this country"
                    },
                    "reachable": {
                      "type": "integer"
                    },
                    "totals": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "outlets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OutletScan"
                      }
                    },
                    "scanned_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "literal": {
                      "type": "boolean"
                    },
                    "headlines": {
                      "type": "integer",
                      "description": "Headlines read across reachable outlets"
                    },
                    "variant_totals": {
                      "type": "object",
                      "description": "Term → {'variant (lang)': matching headline count}",
                      "additionalProperties": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "integer"
                        }
                      }
                    },
                    "term_variants": {
                      "type": "object",
                      "description": "Term → its starter-pack topic (or null) and every variant searched",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "topic": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "variants": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "text": {
                                  "type": "string"
                                },
                                "lang": {
                                  "type": "string",
                                  "description": "en, fr, ar, pt, sw, a slash-joined list when the typed form is shared (e.g. en/fr), or 'as typed'"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "topics": {
                      "type": "array",
                      "maxItems": 20,
                      "description": "Auto-detected most frequent words/two-word phrases in this scan's headlines (stopwords for en/fr/ar/pt/sw and outlet/country names removed; ≥2 headlines)",
                      "items": {
                        "type": "object",
                        "properties": {
                          "phrase": {
                            "type": "string"
                          },
                          "key": {
                            "type": "string",
                            "description": "Normalized form"
                          },
                          "headlines": {
                            "type": "integer"
                          },
                          "outlets": {
                            "type": "integer"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "word",
                              "phrase"
                            ]
                          }
                        }
                      }
                    },
                    "directory_health": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "checked_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "source": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown or missing country",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/data/africa_press.json": {
      "get": {
        "summary": "Press directory (static)",
        "description": "1,187 outlets compiled in 2021. listed_active is the 2021 status, not a live check.",
        "responses": {
          "200": {
            "description": "Directory",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "outlets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "country": {
                            "type": "string"
                          },
                          "region": {
                            "type": "string"
                          },
                          "state": {
                            "type": "string"
                          },
                          "language": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "description": "Empty when none was listed"
                          },
                          "listed_active": {
                            "type": "boolean"
                          },
                          "notes": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/trends": {
      "get": {
        "summary": "Daily topic snapshots per country",
        "description": "One scan per country per day with the 12-topic multilingual starter pack (GitHub Actions, 06:17 UTC). Without `country`: latest day for every country. CDN-cached 1 hour.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "Algeria",
                "Angola",
                "Benin",
                "Botswana",
                "Burkina Faso",
                "Burundi",
                "Cameroon",
                "Cape Verde Islands",
                "Central African Republic",
                "Chad",
                "Comoros",
                "Congo-Brazzaville",
                "C�te D'Ivoire",
                "Democratic Republic of Congo",
                "Djibouti",
                "Egypt",
                "Equatorial Guinea",
                "Eritrea",
                "Ethiopia",
                "Gabon",
                "Gambia",
                "Ghana",
                "Guinea",
                "Kenya",
                "Lesotho",
                "Liberia",
                "Libyian Arab Jamahiriya",
                "Madagascar",
                "Malawi",
                "Mali",
                "Mauritania",
                "Mauritius",
                "Morocco",
                "Mozambique",
                "Namibia",
                "Nigeria",
                "Rwanda",
                "R�union",
                "Senegal",
                "Seychelles",
                "Sierra Leone",
                "Somalia",
                "Somaliland",
                "South Africa",
                "South Sudan",
                "Sudan",
                "Swaziland",
                "Tanzania",
                "Togo",
                "Tunisia",
                "Uganda",
                "Western Sahara",
                "Zambia",
                "Zimbabwe"
              ]
            },
            "description": "Case-insensitive; omit for the latest snapshot of all countries"
          }
        ],
        "responses": {
          "200": {
            "description": "Snapshots (empty arrays when none yet)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string",
                      "enum": [
                        "github",
                        "bundled",
                        "missing"
                      ],
                      "description": "github = latest committed data (owner set KF_GITHUB_TOKEN); bundled = the copy deployed with the site; missing = no data yet"
                    },
                    "updated_at": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "terms": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "days": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "country": {
                      "type": "string"
                    },
                    "series": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "headlines": {
                            "type": "integer"
                          },
                          "reachable": {
                            "type": "integer"
                          },
                          "scanned": {
                            "type": "integer"
                          },
                          "totals": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "integer"
                            }
                          },
                          "shares": {
                            "type": "object",
                            "description": "Term → % of that day's headlines (null when 0 headlines)",
                            "additionalProperties": {
                              "type": [
                                "number",
                                "null"
                              ]
                            }
                          },
                          "topics": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Top auto-detected words that day"
                          }
                        }
                      }
                    },
                    "latest": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "headlines": {
                            "type": "integer"
                          },
                          "reachable": {
                            "type": "integer"
                          },
                          "scanned": {
                            "type": "integer"
                          },
                          "totals": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "integer"
                            }
                          },
                          "shares": {
                            "type": "object",
                            "description": "Term → % of that day's headlines (null when 0 headlines)",
                            "additionalProperties": {
                              "type": [
                                "number",
                                "null"
                              ]
                            }
                          },
                          "topics": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Top auto-detected words that day"
                          }
                        }
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown country",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/reachability": {
      "get": {
        "summary": "Weekly directory reachability check",
        "description": "Latest weekly check of every outlet homepage (GitHub Actions, Mondays 05:23 UTC; 12 concurrent, 12 s timeout, identifying user agent). Replaces reliance on the 2021 'listed active' flag. CDN-cached 1 hour.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter checks to one country (case-insensitive)"
          }
        ],
        "responses": {
          "200": {
            "description": "Checks (empty when no check has run)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source": {
                      "type": "string",
                      "enum": [
                        "github",
                        "bundled",
                        "missing"
                      ],
                      "description": "github = latest committed data (owner set KF_GITHUB_TOKEN); bundled = the copy deployed with the site; missing = no data yet"
                    },
                    "checked_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "user_agent": {
                      "type": "string"
                    },
                    "outlets_with_url": {
                      "type": "integer"
                    },
                    "unique_urls": {
                      "type": "integer"
                    },
                    "reachable": {
                      "type": "integer"
                    },
                    "by_country": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "checked": {
                            "type": "integer"
                          },
                          "reachable": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "checks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/HealthCheck"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown country",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/data/africa-map.json": {
      "get": {
        "summary": "Africa map (GeoJSON)",
        "description": "Natural Earth 1:50m (public domain) via world-atlas 2.0.2 (ISC), Africa subset, coordinates rounded to 0.05°. properties.directory = matching directory country name. Cached 1 day.",
        "responses": {
          "200": {
            "description": "GeoJSON FeatureCollection"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "OutletScan": {
        "type": "object",
        "required": [
          "name",
          "url",
          "language",
          "listed_active",
          "ok",
          "ms"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "listed_active": {
            "type": "boolean"
          },
          "ok": {
            "type": "boolean"
          },
          "http": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status (also on errors when the site answered)"
          },
          "error": {
            "type": "string",
            "description": "When not ok: 'HTTP 403', 'URLError', 'TimeoutError'…"
          },
          "final_url": {
            "type": "string",
            "description": "After redirects; when ok"
          },
          "headlines": {
            "type": "integer",
            "description": "Headline links found; when ok"
          },
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Term → matching headline count; when ok"
          },
          "matched": {
            "type": "array",
            "maxItems": 25,
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "terms": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "variants": {
                  "type": "object",
                  "description": "Term → the variant that matched",
                  "additionalProperties": {
                    "type": "object",
                    "properties": {
                      "text": {
                        "type": "string"
                      },
                      "lang": {
                        "type": "string",
                        "description": "en, fr, ar, pt, sw, a slash-joined list when the typed form is shared (e.g. en/fr), or 'as typed'"
                      }
                    }
                  }
                }
              }
            }
          },
          "sample": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "First headlines found; when ok"
          },
          "ms": {
            "type": "integer"
          },
          "variant_counts": {
            "type": "object",
            "description": "Term → {'variant (lang)': count}; when ok",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "integer"
              }
            }
          },
          "health": {
            "$ref": "#/components/schemas/HealthCheck"
          }
        }
      },
      "HealthCheck": {
        "type": "object",
        "description": "One weekly reachability check of an outlet homepage (a single GET; never inferred)",
        "properties": {
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "reachable": {
            "type": "boolean",
            "description": "Answered with HTTP < 400"
          },
          "http": {
            "type": [
              "integer",
              "null"
            ]
          },
          "final_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "After redirects"
          },
          "redirected": {
            "type": "boolean"
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "'HTTP 404', 'timeout', 'DNS lookup failed', 'SSLEOFError'…"
          },
          "note": {
            "type": "string"
          },
          "ms": {
            "type": "integer"
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}
