{
  "openapi": "3.1.0",
  "info": {
    "title": "Domain Armor Grade API",
    "version": "1.0.0",
    "description": "Read-only, passive security grade for a public domain. Passive check of public configuration only (DNS, email authentication records, HTTPS and HTTP response headers of the homepage). It is not a penetration test, does not test the application, and a good grade is not a security guarantee. Rate limit: 10 requests per minute per client. Results are cached per domain for up to 10 minutes. Example: curl -s \"https://getmirror.online/api/grade?domain=example.com\""
  },
  "servers": [
    {
      "url": "https://getmirror.online"
    }
  ],
  "paths": {
    "/api/grade": {
      "get": {
        "operationId": "getGrade",
        "summary": "Get the current grade and top issues for a public domain",
        "x-codeSamples": [
          {
            "lang": "shell",
            "source": "curl -s \"https://getmirror.online/api/grade?domain=example.com\""
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "description": "Public hostname, e.g. example.com. IP addresses, localhost and names resolving or redirecting to private/reserved addresses are rejected.",
            "schema": {
              "type": "string",
              "maxLength": 253,
              "example": "example.com"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Scan result",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed per window (60 s)."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Grade"
                },
                "example": {
                  "domain": "example.com",
                  "grade": "D",
                  "score": 64,
                  "scanned_at": "2026-11-05T14:00:00.000Z",
                  "scoring_version": 2,
                  "issues": [
                    {
                      "id": "hsts",
                      "title": "Tell browsers to always use the secure version",
                      "severity": "medium",
                      "status": "fail"
                    },
                    {
                      "id": "csp",
                      "title": "Limit what scripts your pages are allowed to run",
                      "severity": "medium",
                      "status": "fail"
                    }
                  ],
                  "what_to_do_first": [
                    {
                      "id": "hsts",
                      "title": "Tell browsers to always use the secure version",
                      "why": "Helps browsers use HTTPS on future visits after they have received the policy.",
                      "who_can_fix": "Your web host",
                      "verify": "curl -sIL https://example.com/ | grep -i \"Strict-Transport-Security\""
                    }
                  ],
                  "coverage": {
                    "state": "partial",
                    "assessed_weight": 95,
                    "eligible_weight": 100,
                    "ratio": 0.95
                  },
                  "excluded": [
                    {
                      "id": "dkim",
                      "title": "DKIM — digital signature on email",
                      "reason": "unverified"
                    }
                  ],
                  "report_url": "https://getmirror.online/report/example.com",
                  "methodology_url": "https://getmirror.online/methodology",
                  "limitations": "Passive check of public configuration only (DNS, email authentication records, HTTPS and HTTP response headers of the homepage). It is not a penetration test, does not test the application, and a good grade is not a security guarantee."
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid domain, or an IP literal",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_domain",
                    "message": "Not a valid public hostname."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Domain does not exist or has no DNS records",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "The domain does not exist or has no DNS records."
                  }
                }
              }
            }
          },
          "422": {
            "description": "Resolves or redirects to a private/reserved address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "private_address",
                    "message": "The domain resolves or redirects to a private, loopback or reserved address."
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "rate_limited",
                    "message": "Too many requests from this client. Try again later."
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed per window (60 s)."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window resets."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "502": {
            "description": "Public DNS lookups failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "scan_failed",
                    "message": "Public DNS lookups failed; the domain could not be checked right now."
                  }
                }
              }
            }
          },
          "504": {
            "description": "Scan timed out (25 s)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "timeout",
                    "message": "The scan did not finish in time."
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_domain",
                  "invalid_domain",
                  "ip_literal",
                  "private_address",
                  "not_found",
                  "rate_limited",
                  "scan_failed",
                  "timeout"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Grade": {
        "type": "object",
        "required": [
          "domain",
          "grade",
          "score",
          "scanned_at",
          "scoring_version",
          "issues",
          "what_to_do_first",
          "coverage",
          "excluded",
          "report_url",
          "methodology_url",
          "limitations"
        ],
        "properties": {
          "domain": {
            "type": "string"
          },
          "grade": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "A",
              "B",
              "C",
              "D",
              "F",
              null
            ],
            "description": "null when too little could be assessed."
          },
          "score": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "scanned_at": {
            "type": "string",
            "format": "date-time",
            "description": "UTC"
          },
          "scoring_version": {
            "type": "integer"
          },
          "issues": {
            "type": "array",
            "description": "Checks that failed or need attention.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "title",
                "severity",
                "status"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "severity": {
                  "type": "string",
                  "enum": [
                    "critical",
                    "high",
                    "medium",
                    "low",
                    "none"
                  ]
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "fail",
                    "warn"
                  ]
                }
              }
            }
          },
          "what_to_do_first": {
            "type": "array",
            "maxItems": 2,
            "items": {
              "type": "object",
              "required": [
                "id",
                "title",
                "why",
                "who_can_fix",
                "verify"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "why": {
                  "type": "string"
                },
                "who_can_fix": {
                  "type": "string"
                },
                "verify": {
                  "type": "string",
                  "description": "Shell command to re-check after fixing."
                }
              }
            }
          },
          "coverage": {
            "type": "object",
            "description": "Score denominator: assessed_weight. Unverified and not-applicable checks are excluded from it.",
            "required": [
              "state",
              "assessed_weight",
              "eligible_weight",
              "ratio"
            ],
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "full",
                  "partial",
                  "insufficient",
                  "na"
                ]
              },
              "assessed_weight": {
                "type": "integer"
              },
              "eligible_weight": {
                "type": "integer"
              },
              "ratio": {
                "type": [
                  "number",
                  "null"
                ],
                "minimum": 0,
                "maximum": 1
              }
            }
          },
          "excluded": {
            "type": "array",
            "description": "Weighted checks left out of the score. 'unverified' is not a failure (e.g. DKIM when no common selector is found).",
            "items": {
              "type": "object",
              "required": [
                "id",
                "title",
                "reason"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "unverified",
                    "not_applicable"
                  ]
                }
              }
            }
          },
          "report_url": {
            "type": "string",
            "format": "uri"
          },
          "methodology_url": {
            "type": "string",
            "format": "uri"
          },
          "limitations": {
            "type": "string"
          }
        }
      }
    }
  }
}