{
  "openapi": "3.0.3",
  "info": {
    "title": "Mailganer Domain Risk API",
    "version": "0.6.0",
    "description": "Публичная оценка риска домена отправителя (preflight перед SMTP / trusted list).\n\nПубличный REST: `POST /tools/domain-risk/v1` (nginx → внутренний WP REST). Ответ — уровень `allow` / `limited` / `deny`, бинарный `risk` и уверенность — без findings, чеклиста DNS и объяснений.\n\nСерверный режим (#20944, #21407): `POST /tools/domain-risk/v1/service` или тот же публичный путь с заголовком `X-MG-Service-Token` — без SmartCaptcha, отдельная квота; в ответе дополнительно `decision` и `reason_codes`.\n\nBannedIP overlay (#20945): если A-запись домена в списке банов — `deny` с `BANNED_IP` / `BANNED_CIDR` (в сервисном ответе).\n\nParking/redirect overlay (#20946): `CLIENT_REDIRECT` / `PARKING_UNBOUND` (+ опц. `NO_MX`) при пробинге главной.\n\nUI: `/tools/domain-risk` (SmartCaptcha + квоты). На apex `/api/` занят личным кабинетом."
  },
  "servers": [
    {
      "url": "https://mailganer.com",
      "description": "Production"
    },
    {
      "url": "https://newland.mailganer.com",
      "description": "Staging (Basic Auth на HTML; REST без Basic Auth)"
    }
  ],
  "tags": [
    {
      "name": "domain-risk",
      "description": "Проверка домена (публичный UI)"
    },
    {
      "name": "domain-risk-service",
      "description": "Серверный вызов сендеров / gungo"
    },
    {
      "name": "system",
      "description": "Статус и квоты"
    }
  ],
  "paths": {
    "/tools/domain-risk/v1/status": {
      "get": {
        "tags": [
          "system"
        ],
        "summary": "Квоты и captcha",
        "operationId": "domainRiskStatus",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          }
        }
      }
    },
    "/tools/domain-risk/v1": {
      "post": {
        "tags": [
          "domain-risk"
        ],
        "summary": "Оценить риск домена",
        "description": "Синхронная express-проверка. При включённой captcha нужен токен SmartCaptcha (`X-Captcha-Token` или поле `captchaToken`), **кроме** валидного `X-MG-Service-Token` (тогда капча не нужна, квота сервисная, в ответе есть `decision` / `reason_codes`).\n\n`level`: `allow` | `limited` | `deny` | `unknown`.\n`risk`: `true` при limited/deny | `false` при allow | `null` при unknown (совместимость).\n`confidence`: 0–100 — уверенность; `null` при UNKNOWN.\n`result`: `OK` | `NOT_OK` | `UNKNOWN` (служебный статус проверки).\n\nHTTP 200 — оценка выполнена. HTTP 503 — `UNKNOWN`.",
        "operationId": "domainRiskVerify",
        "parameters": [
          {
            "name": "X-Captcha-Token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Токен SmartCaptcha (если captcha обязательна и нет сервисного токена)"
          },
          {
            "name": "X-MG-Service-Token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Сервисный shared secret (сендеры / gungo). При совпадении с DOMAIN_RISK_SERVICE_TOKEN — без captcha, отдельная квота, поля decision/reason_codes"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              },
              "example": {
                "domain": "example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Оценка выполнена",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                },
                "examples": {
                  "no_risk": {
                    "summary": "Рисков нет (allow)",
                    "value": {
                      "status": "ok",
                      "request_id": "dvr_a8e5c98ccfd248218dc5723f",
                      "domain": "mailganer.com",
                      "result": "OK",
                      "level": "allow",
                      "risk": false,
                      "confidence": 99,
                      "timing_ms": 412.7,
                      "dailyRemaining": 19,
                      "dailyLimit": 20
                    }
                  },
                  "limited": {
                    "summary": "Есть сомнения (limited)",
                    "value": {
                      "status": "ok",
                      "request_id": "dvr_b1a2c3d4e5f6789012345678",
                      "domain": "bigmoonparty.ru",
                      "result": "OK",
                      "level": "limited",
                      "risk": true,
                      "confidence": 58,
                      "timing_ms": 520.1,
                      "dailyRemaining": 18,
                      "dailyLimit": 20
                    }
                  },
                  "has_risk": {
                    "summary": "Риск есть (deny)",
                    "value": {
                      "status": "ok",
                      "request_id": "dvr_c9cdec3b34a58cd518000e9d",
                      "domain": "mercadoflags.com",
                      "result": "NOT_OK",
                      "level": "deny",
                      "risk": true,
                      "confidence": 70,
                      "timing_ms": 497.7,
                      "dailyRemaining": 17,
                      "dailyLimit": 20
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Некорректный домен",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "Укажите домен вида example.com"
                }
              }
            }
          },
          "403": {
            "description": "Captcha не пройдена",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Дневной лимит IP или глобальный лимит",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Сервис не настроен или результат UNKNOWN",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/VerifyResponse"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/tools/domain-risk/v1/service": {
      "post": {
        "tags": [
          "domain-risk-service"
        ],
        "summary": "Оценить риск (серверный вызов)",
        "description": "Тот же express-прогон, что у публичного `POST /tools/domain-risk/v1`, но:\n\n- обязателен `X-MG-Service-Token`;\n- nginx allowlist IP сендеров;\n- без SmartCaptcha;\n- отдельная суточная квота `DOMAIN_RISK_SERVICE_DAILY_LIMIT`;\n- в ответе всегда `decision` и `reason_codes`.",
        "operationId": "domainRiskVerifyService",
        "parameters": [
          {
            "name": "X-MG-Service-Token",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Shared secret из DOMAIN_RISK_SERVICE_TOKEN"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceVerifyRequest"
              },
              "example": {
                "domain": "example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Оценка выполнена",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceVerifyResponse"
                },
                "example": {
                  "status": "ok",
                  "request_id": "dvr_c9cdec3b34a58cd518000e9d",
                  "domain": "mercadoflags.com",
                  "result": "NOT_OK",
                  "level": "deny",
                  "risk": true,
                  "confidence": 70,
                  "decision": "DENY",
                  "reason_codes": [
                    "SPF_INVALID",
                    "REGISTRY_UNVERIFIED"
                  ],
                  "timing_ms": 497.7,
                  "dailyRemaining": 49999,
                  "dailyLimit": 50000
                }
              }
            }
          },
          "401": {
            "description": "Нет или неверный сервисный токен",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "IP не в nginx allowlist",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Сервисный дневной лимит исчерпан",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Сервис не настроен или результат UNKNOWN",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ServiceVerifyResponse"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "VerifyRequest": {
        "type": "object",
        "required": [
          "domain"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "description": "Домен отправителя (без схемы и пути)",
            "example": "example.com",
            "maxLength": 253
          },
          "captchaToken": {
            "type": "string",
            "description": "Токен SmartCaptcha (альтернатива заголовку X-Captcha-Token)"
          },
          "website": {
            "type": "string",
            "description": "Honeypot — должно быть пустым"
          }
        }
      },
      "ServiceVerifyRequest": {
        "type": "object",
        "required": [
          "domain"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "description": "Домен отправителя (без схемы и пути)",
            "example": "example.com",
            "maxLength": 253
          }
        }
      },
      "VerifyResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "example": "ok"
          },
          "request_id": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "result": {
            "type": "string",
            "enum": [
              "OK",
              "NOT_OK",
              "UNKNOWN"
            ]
          },
          "level": {
            "type": "string",
            "enum": [
              "allow",
              "limited",
              "deny",
              "unknown"
            ],
            "description": "Публичный вердикт: allow — рисков нет; limited — есть сомнения; deny — риск есть; unknown — не удалось проверить"
          },
          "risk": {
            "type": "boolean",
            "nullable": true,
            "description": "true при limited/deny; false при allow; null при unknown (совместимость)"
          },
          "confidence": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "description": "Уверенность в утверждении risk, %"
          },
          "timing_ms": {
            "type": "number",
            "nullable": true
          },
          "dailyRemaining": {
            "type": "integer"
          },
          "dailyLimit": {
            "type": "integer"
          },
          "decision": {
            "type": "string",
            "description": "Только при сервисном токене: ALLOW | LIMITED | DENY | UNKNOWN"
          },
              "reason_codes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Только при сервисном токене — коды причин upstream и overlay (BANNED_IP, BANNED_CIDR, CLIENT_REDIRECT, PARKING_UNBOUND, NO_MX, …)"
              }
        }
      },
      "ServiceVerifyResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/VerifyResponse"
          },
          {
            "type": "object",
            "required": [
              "decision",
              "reason_codes"
            ],
            "properties": {
              "decision": {
                "type": "string",
                "enum": [
                  "ALLOW",
                  "LIMITED",
                  "DENY",
                  "UNKNOWN"
                ]
              },
              "reason_codes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "StatusResponse": {
        "type": "object",
        "properties": {
          "configured": {
            "type": "boolean"
          },
          "captchaRequired": {
            "type": "boolean"
          },
          "captchaMode": {
            "type": "string",
            "enum": [
              "invisible",
              "visible",
              "off"
            ]
          },
          "siteKey": {
            "type": "string"
          },
          "dailyLimit": {
            "type": "integer"
          },
          "dailyRemaining": {
            "type": "integer"
          },
          "globalDailyRemaining": {
            "type": "integer"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    }
  }
}
