{
  "openapi": "3.1.0",
  "info": {
    "title": "SaaS Ranking",
    "version": "1.1.0",
    "summary": "SaaS vendors measured on what they publish about how they handle your data.",
    "description": "Read only, no key, no rate limit beyond ordinary fair use.\n\nTHE ONE RULE THAT SHAPES EVERY FIELD: a negative result describes OUR measurement\nand is never a property of the vendor. There is no boolean value anywhere in this\ninterface. A result is \"found\", \"not_found\" or \"not_measured\", and \"not_found\"\nalways carries the addresses we checked and the date we checked them.\n\nA vendor measured on fewer than 9 of the 12 criteria\ngets no score and no rank: a score out of three measurements looks like a score out\nof twelve and is not one.\n\nIf you quote a figure, quote its snapshot id with it. Two figures are only\ncomparable when they carry the same id.",
    "license": {
      "name": "CC BY 4.0",
      "identifier": "CC-BY-4.0"
    },
    "contact": {
      "url": "https://saas-ranking.com/correction"
    }
  },
  "servers": [
    {
      "url": "https://saas-ranking.com"
    }
  ],
  "externalDocs": {
    "url": "https://saas-ranking.com/developers",
    "description": "The same interface explained for people."
  },
  "paths": {
    "/api/v1/criteria": {
      "get": {
        "summary": "What do you measure, and how is it weighted?",
        "description": "The full catalogue with the weight of each criterion. This is the same source the methodology page reads.",
        "operationId": "api_v1_criteria",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The answer, always carrying the snapshot it was rendered from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Antwort"
                }
              }
            }
          },
          "404": {
            "description": "No such category, vendor or detail. The body carries a stable error name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "405": {
            "description": "This interface is read only. Corrections go through the form at /correction so a human reads them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "503": {
            "description": "No measurement run has finished yet, so there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories": {
      "get": {
        "summary": "Which categories exist?",
        "description": "Every category we have measured at least once, with the number of vendors in it.",
        "operationId": "api_v1_categories",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The answer, always carrying the snapshot it was rendered from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Antwort"
                }
              }
            }
          },
          "404": {
            "description": "No such category, vendor or detail. The body carries a stable error name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "405": {
            "description": "This interface is read only. Corrections go through the form at /correction so a human reads them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "503": {
            "description": "No measurement run has finished yet, so there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories/{slug}": {
      "get": {
        "summary": "Who is in a category, and in what order?",
        "description": "The ranking of one category: rank, score, and how many criteria we could measure for each vendor.",
        "operationId": "api_v1_categories_slug",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The category slug, as listed by /api/v1/categories.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The answer, always carrying the snapshot it was rendered from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Antwort"
                }
              }
            }
          },
          "404": {
            "description": "No such category, vendor or detail. The body carries a stable error name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "405": {
            "description": "This interface is read only. Corrections go through the form at /correction so a human reads them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "503": {
            "description": "No measurement run has finished yet, so there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vendors/{domain}": {
      "get": {
        "summary": "What did you find at one vendor?",
        "description": "One vendor by domain, with every criterion, its source, the addresses we checked, and the quotes we read.",
        "operationId": "api_v1_vendors_domain",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The vendor domain, without www.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The answer, always carrying the snapshot it was rendered from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Antwort"
                }
              }
            }
          },
          "404": {
            "description": "No such category, vendor or detail. The body carries a stable error name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "405": {
            "description": "This interface is read only. Corrections go through the form at /correction so a human reads them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "503": {
            "description": "No measurement run has finished yet, so there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/details": {
      "get": {
        "summary": "What values did you read off their pages?",
        "description": "The catalogue of measured details: certifications named, hosting locations, uptime figures, subprocessors, third-party hosts.",
        "operationId": "api_v1_details",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The answer, always carrying the snapshot it was rendered from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Antwort"
                }
              }
            }
          },
          "404": {
            "description": "No such category, vendor or detail. The body carries a stable error name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "405": {
            "description": "This interface is read only. Corrections go through the form at /correction so a human reads them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "503": {
            "description": "No measurement run has finished yet, so there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/details/{key}": {
      "get": {
        "summary": "Which vendors name a given value?",
        "description": "One detail type with every value that has enough vendors to have a page.",
        "operationId": "api_v1_details_key",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "A detail key or its path, as listed by /api/v1/details.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The answer, always carrying the snapshot it was rendered from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Antwort"
                }
              }
            }
          },
          "404": {
            "description": "No such category, vendor or detail. The body carries a stable error name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "405": {
            "description": "This interface is read only. Corrections go through the form at /correction so a human reads them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          },
          "503": {
            "description": "No measurement run has finished yet, so there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fehler"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Antwort": {
        "type": "object",
        "description": "Every response carries these fields. What follows them depends on the endpoint.",
        "required": [
          "snapshot",
          "snapshot_run",
          "snapshot_results",
          "snapshot_measured_through",
          "license",
          "score_version",
          "classifier_version",
          "classifier_scope"
        ],
        "properties": {
          "snapshot": {
            "type": [
              "string",
              "null"
            ],
            "description": "The id of the data snapshot this response was rendered from, for example r42.71638. Two figures are only comparable when they carry the same id."
          },
          "snapshot_run": {
            "type": "integer",
            "description": "The highest completed measurement run that contributed to the snapshot."
          },
          "snapshot_results": {
            "type": "integer",
            "description": "How many results the snapshot holds. It moves when a partial run lands, even if the run number does not."
          },
          "snapshot_measured_through": {
            "type": [
              "string",
              "null"
            ],
            "description": "The newest measurement timestamp in the snapshot."
          },
          "license": {
            "type": [
              "string",
              "null"
            ],
            "description": "The licence the figures are published under: CC BY 4.0. Attribution is the link back."
          },
          "score_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "The version of the transparency score. It is frozen, and a new criterion does not change an existing figure."
          },
          "classifier_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "The current confidence classifier for stored certification and hosting quotes. It does not date or replace their original measurement method. Measured precision and recall are on /data/quality."
          },
          "classifier_scope": {
            "type": [
              "string",
              "null"
            ],
            "description": "current_quote_confidence: only the confidence of current stored quotes is re-evaluated. Original subjects, measurement dates, criteria, scores and historical series keep their recorded state."
          }
        }
      },
      "Fehler": {
        "type": "object",
        "description": "Every error carries a stable name a caller can react to, and a sentence for people.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable name. It does not change when the sentence is rewritten."
          },
          "message": {
            "type": "string",
            "description": "What happened, for a human reader."
          }
        }
      },
      "Ergebnis": {
        "type": "object",
        "description": "One criterion at one vendor. There is no boolean here, and that is deliberate.",
        "required": [
          "result",
          "source",
          "checked",
          "measured_on"
        ],
        "properties": {
          "result": {
            "type": "string",
            "enum": [
              "found",
              "not_found",
              "not_measured"
            ],
            "description": "found: we found it, and \"source\" carries the page. not_found: we reached at least one address and none of them carried it. \"checked\" lists them. not_measured: we reached nothing, or robots.txt asks us not to. This says something about US."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The page we found it on, or null."
          },
          "checked": {
            "type": "array",
            "description": "Every address we requested, with what came back. This is the whole finding behind \"not_found\".",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "outcome": {
                  "type": "string",
                  "description": "The HTTP status or the reason we could not read it."
                }
              }
            }
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why we could not measure it, when we could not."
          },
          "measured_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Detail": {
        "type": "object",
        "description": "A value we read off a vendor page, with the sentence it stands in. Never a verdict, and it enters no score.",
        "required": [
          "key",
          "value",
          "quote",
          "source",
          "subject",
          "confidence",
          "aggregated"
        ],
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "zertifikat",
              "datenregion",
              "sla_prozent",
              "subprozessor",
              "meldefrist_tage",
              "datenschutzkontakt_art",
              "drittanbieter",
              "hsts_frist",
              "hsts_option",
              "security_txt_ablauf"
            ]
          },
          "criterion": {
            "type": "string",
            "enum": [
              "dpa",
              "subprozessoren",
              "datenregion",
              "statusseite",
              "preise",
              "zertifizierungen",
              "sla",
              "transport",
              "tracker_vor_einwilligung",
              "security_txt",
              "subprozessor_meldung",
              "datenschutzkontakt"
            ]
          },
          "value": {
            "type": "string",
            "description": "The value itself, as stored."
          },
          "label": {
            "type": "string",
            "description": "The value as a reader knows it."
          },
          "quote": {
            "type": "string",
            "description": "The sentence we read it from, at most 200 characters, mailboxes masked."
          },
          "source": {
            "type": "string",
            "format": "uri"
          },
          "measured_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "subject": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "anbieter",
              "infrastruktur",
              "dienstleister",
              "erwaehnt",
              "unklar",
              null
            ],
            "description": "Whose statement the sentence is. Only \"anbieter\" is counted into any list: \"Our data centres are ISO 27001 certified\" is a correct quote and still not a statement about the vendor."
          },
          "subject_means": {
            "type": [
              "string",
              "null"
            ]
          },
          "confidence": {
            "type": "string",
            "enum": [
              "belegt",
              "schwach"
            ],
            "description": "\"schwach\" means the sentence we kept does not carry the finding on its own. The row stays on the vendor profile and counts in no list and no figure. Measured precision per extractor: https://saas-ranking.com/data/quality"
          },
          "confidence_means": {
            "type": [
              "string",
              "null"
            ]
          },
          "aggregated": {
            "type": "boolean",
            "description": "Whether this row counts into a value page. It is the only boolean in this interface, and it describes OUR handling, not the vendor."
          },
          "path": {
            "type": [
              "string",
              "null"
            ],
            "description": "The page that lists every vendor with this value, if it exists."
          }
        }
      }
    }
  },
  "x-versions": {
    "score_version": "v1",
    "classifier_version": "v3-2026-10-20",
    "classifier_scope": "current_quote_confidence",
    "description": "The score is frozen: a thirteenth criterion does not change a figure already published."
  }
}