{
  "openapi": "3.1.0",
  "info": {
    "title": "SkanQRCode API",
    "version": "1.7.0",
    "description": "Classifies URLs and IP addresses as malicious, suspicious or not_malicious.\n\nSkanQRCode combines several threat-intelligence sources, IP-range classification, an IP-association store, file-sharing awareness and a weighted heuristic model to answer one question: is this URL or IP address safe to let a user visit or receive? It always returns a verdict within a fixed time budget. This document names neither the detection engine nor any data source.\n\nEvery operation needs an API key (`Authorization: Bearer ...`) except `GET /health` and `GET /openapi.json`."
  },
  "servers": [
    {
      "url": "https://api.skanqrcode.com",
      "description": "The only API host. There is no staging host; sandbox access is an `sk_test_` key on this same host."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "check",
      "description": "Real-time URL and IP classification."
    },
    {
      "name": "usage",
      "description": "Usage and quota reporting."
    },
    {
      "name": "lists",
      "description": "Per-tenant allow and block lists."
    },
    {
      "name": "meta",
      "description": "Service metadata; no API key required."
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_live_... / sk_test_...",
        "description": "API key issued at tenant creation, shown once, sent as `Authorization: Bearer sk_live_<id>_<secret>` or `sk_test_<id>_<secret>`. The prefix is set by plan, not chosen by the caller: free (sandbox) tenants receive an `sk_test_` key only; paid tenants receive an `sk_live_` key. Required on every operation except GET /health and GET /openapi.json. Never pass the key in a query string."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request failed validation (malformed JSON, bad field, unparseable target, or a body over 8 KB).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "invalidTarget": {
                "summary": "Unparseable target",
                "value": {
                  "error": {
                    "code": "invalid_request",
                    "message": "The target could not be parsed as a URL or IP address.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The API key is missing, malformed, unknown or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "unauthorized": {
                "summary": "Bad key",
                "value": {
                  "error": {
                    "code": "unauthorized",
                    "message": "The API key is missing, invalid, or has been revoked.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The tenant is suspended for non-payment.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "suspended": {
                "summary": "Suspended",
                "value": {
                  "error": {
                    "code": "payment_required",
                    "message": "This account is suspended. Update your billing details to continue.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key may not perform this operation: it lacks the required scope (`forbidden`), or the plan does not include the feature (`plan_feature_unavailable`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "scope": {
                "summary": "Missing scope",
                "value": {
                  "error": {
                    "code": "forbidden",
                    "message": "This key is not allowed to perform this operation.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              },
              "plan": {
                "summary": "Feature not in plan",
                "value": {
                  "error": {
                    "code": "plan_feature_unavailable",
                    "message": "This feature is not included in your plan.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist for this tenant.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "notFound": {
                "summary": "Unknown entry",
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "The requested resource was not found.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "The per-minute rate limit (`rate_limited`) or the monthly quota (`quota_exceeded`) was exceeded. Blocked requests are not billed. Back off for `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Limit": {
            "description": "The limit that was exceeded: requests per minute for `rate_limited`, requests per month for `quota_exceeded`.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window (0 when limited).",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the limit resets.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "rateLimited": {
                "summary": "Per-minute limit",
                "value": {
                  "error": {
                    "code": "rate_limited",
                    "message": "Too many requests. Retry after the interval in Retry-After.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              },
              "quotaExceeded": {
                "summary": "Monthly quota",
                "value": {
                  "error": {
                    "code": "quota_exceeded",
                    "message": "Monthly request quota exhausted for this plan.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      },
      "AuthUnavailable": {
        "description": "Authentication is temporarily unavailable for a key that has not been seen recently. Retry shortly.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "unavailable": {
                "summary": "Backend unavailable",
                "value": {
                  "error": {
                    "code": "auth_unavailable",
                    "message": "Authentication is temporarily unavailable. Retry shortly.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected failure. Never contains stack traces, internal hostnames or upstream error bodies.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "internal": {
                "summary": "Unexpected error",
                "value": {
                  "error": {
                    "code": "internal",
                    "message": "An unexpected error occurred.",
                    "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "plan_feature_unavailable",
                  "payment_required",
                  "rate_limited",
                  "quota_exceeded",
                  "auth_unavailable",
                  "internal"
                ],
                "description": "Stable, closed error code. Branch on this, never on `message`."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation."
              },
              "requestId": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message",
              "requestId"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false
      },
      "CheckResponse": {
        "type": "object",
        "properties": {
          "verdict": {
            "type": "string",
            "enum": [
              "not_malicious",
              "suspicious",
              "malicious"
            ],
            "description": "The classification. See `action` for what to do about it: they are not the same field."
          },
          "action": {
            "type": "string",
            "enum": [
              "allow",
              "warn",
              "block"
            ],
            "description": "The recommended action for the caller. Fixed mapping, identical on every plan: not_malicious -> allow, suspicious -> warn, malicious -> block."
          },
          "mode": {
            "type": "string",
            "enum": [
              "url",
              "ip"
            ],
            "description": "Whether the target was evaluated as a URL or as an IP address."
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "BRAND_LOOKALIKE_DOMAIN",
                "BRAND_IMPERSONATION",
                "DECEPTIVE_URL_USERINFO",
                "BRAND_NAME_IN_DOMAIN",
                "IP_ADDRESS_HOST",
                "MIXED_SCRIPT_DOMAIN",
                "HOSTED_ON_MALICIOUS_IP",
                "NEWLY_REGISTERED_DOMAIN",
                "HOSTED_FORM_ON_SHARED_PLATFORM",
                "MALICIOUS_IP_ASSOCIATION",
                "MALICIOUS_HOSTING_NEIGHBORHOOD",
                "FILE_SHARING_HOST",
                "SUSPICIOUS_CONTENT_OWNER",
                "EXECUTABLE_PAYLOAD",
                "HIGH_RISK_TLD",
                "RANDOMIZED_DOMAIN_NAME",
                "EXCESSIVE_SUBDOMAINS",
                "CREDENTIAL_LURE_KEYWORDS",
                "CLOUD_HOSTED_IP",
                "INSECURE_CREDENTIAL_PAGE",
                "URL_SHORTENER_REDIRECT",
                "POPULAR_DOMAIN",
                "ACTIVE_THREAT_FEED_MATCH",
                "KNOWN_MALWARE_MATCH",
                "KNOWN_PHISHING_MATCH",
                "UNWANTED_SOFTWARE_MATCH",
                "THREAT_INTEL_UNCONFIRMED",
                "SUSPICIOUS_IP_ASSOCIATION",
                "SHARED_INFRASTRUCTURE_IP",
                "NON_PUBLIC_IP",
                "NO_THREAT_INTEL",
                "UNRESOLVED_REDIRECT",
                "UNSUPPORTED_SCHEME",
                "BLOCK_LIST_MATCH",
                "ALLOW_LIST_CONFLICT",
                "ALLOW_LIST_MATCH",
                "ADMINISTRATIVE_OVERRIDE",
                "EVALUATION_ERROR"
              ]
            },
            "description": "Reason codes that contributed to the verdict. Codes name the category of evidence only, never a data source."
          },
          "finalUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL a shortened link resolved to, when a redirect was followed; otherwise null."
          },
          "cached": {
            "type": "boolean",
            "description": "True only when the result came from this tenant's own per-user cache, which requires `userId`. Best-effort: false when the per-user cache can't answer within a few milliseconds of where the request is served (for example, far from its replicas). The verdict is the same either way."
          },
          "executionTimeMs": {
            "type": "integer",
            "minimum": 0,
            "description": "Server-side processing time of the evaluation in milliseconds."
          },
          "environment": {
            "type": "string",
            "enum": [
              "sandbox",
              "production"
            ],
            "description": "Derived from the calling key's plan, never from the request."
          },
          "licensedForProduction": {
            "type": "boolean",
            "description": "False on the free sandbox plan. Static plan metadata: it never changes `verdict` or `action`, but sandbox results must not be relied on for production enforcement."
          },
          "requestId": {
            "type": "string",
            "description": "Identifier to quote when contacting support."
          },
          "related": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RelatedHost"
            },
            "maxItems": 20,
            "description": "IP mode only: up to 20 recently associated hosts, most recent first."
          }
        },
        "required": [
          "verdict",
          "action",
          "mode",
          "reasons",
          "finalUrl",
          "cached",
          "executionTimeMs",
          "environment",
          "licensedForProduction",
          "requestId"
        ],
        "additionalProperties": false
      },
      "RelatedHost": {
        "type": "object",
        "properties": {
          "host": {
            "type": "string"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "not_malicious",
              "suspicious",
              "malicious"
            ]
          }
        },
        "required": [
          "host",
          "verdict"
        ],
        "additionalProperties": false,
        "description": "A host recently associated with the checked IP address by public threat analysis."
      },
      "CheckRequest": {
        "type": "object",
        "properties": {
          "target": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096,
            "description": "A URL or an IP address (v4 or v6) to classify. Decimal, hexadecimal and octal IPv4 forms are treated as IP addresses. Up to 4096 characters.",
            "example": "https://example.com/login"
          },
          "userId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Optional opaque identifier of the end user on whose behalf the check runs. It enables per-user result caching. It is hashed before use and never stored or logged in clear.",
            "example": "user-4821"
          }
        },
        "required": [
          "target"
        ],
        "additionalProperties": false
      },
      "UsageResponse": {
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "month": {
            "type": "string",
            "pattern": "^\\d{4}-(0[1-9]|1[0-2])$",
            "description": "The calendar month (UTC) this response covers."
          },
          "monthlyQuota": {
            "type": "integer",
            "description": "Requests included in the plan for a month."
          },
          "totalRequests": {
            "type": "integer",
            "description": "Requests counted against the monthly quota so far, combined across every API key, client IP and access surface."
          },
          "availableRequests": {
            "type": "integer",
            "description": "max(monthlyQuota - totalRequests, 0). On plans that bill overage, reaching zero means the next request is billed as overage rather than blocked."
          }
        },
        "required": [
          "tenantId",
          "month",
          "monthlyQuota",
          "totalRequests",
          "availableRequests"
        ],
        "additionalProperties": false
      },
      "HourlyUsageResponse": {
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "month": {
            "type": "string",
            "pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
          },
          "rpmLimit": {
            "type": "integer",
            "description": "The plan's requests-per-minute limit."
          },
          "hourlyCapacity": {
            "type": "integer",
            "description": "rpmLimit * 60: the most requests one hour can hold at the per-minute limit. A reading aid only, not a separate quota; limits are enforced per minute."
          },
          "hours": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HourlyUsageEntry"
            }
          }
        },
        "required": [
          "tenantId",
          "month",
          "rpmLimit",
          "hourlyCapacity",
          "hours"
        ],
        "additionalProperties": false
      },
      "HourlyUsageEntry": {
        "type": "object",
        "properties": {
          "hour": {
            "type": "string",
            "description": "Start of the hour (UTC), RFC 3339."
          },
          "mode": {
            "type": "string",
            "enum": [
              "url",
              "ip"
            ]
          },
          "total": {
            "type": "integer",
            "description": "Requests served in this hour."
          },
          "capacityUsedPercent": {
            "type": "number",
            "description": "total / hourlyCapacity * 100, one decimal. Near 100 means this hour ran at the per-minute rate limit."
          },
          "blockedRpm": {
            "type": "integer",
            "description": "Requests blocked in this hour for exceeding the per-minute rate limit."
          },
          "blockedQuota": {
            "type": "integer",
            "description": "Requests blocked in this hour for exceeding the monthly quota."
          },
          "malicious": {
            "type": "integer"
          },
          "suspicious": {
            "type": "integer"
          },
          "notMalicious": {
            "type": "integer"
          },
          "cached": {
            "type": "integer"
          }
        },
        "required": [
          "hour",
          "mode",
          "total",
          "capacityUsedPercent",
          "blockedRpm",
          "blockedQuota",
          "malicious",
          "suspicious",
          "notMalicious",
          "cached"
        ],
        "additionalProperties": false
      },
      "ListEntriesResponse": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListEntry"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "entries",
          "nextCursor"
        ],
        "additionalProperties": false
      },
      "ListEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "matchType": {
            "type": "string",
            "enum": [
              "url",
              "host",
              "domain",
              "ip"
            ],
            "description": "What the entry matches: an exact URL, a hostname, a registrable domain (including its subdomains), or an IP address."
          },
          "value": {
            "type": "string",
            "description": "The host, domain or IP as entered. For URL entries only the registrable domain and a short hash hint are shown."
          },
          "createdAt": {
            "type": "string",
            "description": "RFC 3339, UTC."
          }
        },
        "required": [
          "id",
          "matchType",
          "value",
          "createdAt"
        ],
        "additionalProperties": false
      },
      "AddListEntryRequest": {
        "type": "object",
        "properties": {
          "matchType": {
            "type": "string",
            "enum": [
              "url",
              "host",
              "domain",
              "ip"
            ],
            "description": "What the entry matches: an exact URL, a hostname, a registrable domain (including its subdomains), or an IP address."
          },
          "value": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "example": "malicious-example.com"
          }
        },
        "required": [
          "matchType",
          "value"
        ],
        "additionalProperties": false
      },
      "HealthResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          }
        },
        "required": [
          "status"
        ],
        "additionalProperties": false
      }
    },
    "parameters": {}
  },
  "paths": {
    "/v1/check": {
      "post": {
        "operationId": "checkUrl",
        "tags": [
          "check"
        ],
        "summary": "Classify a URL or IP address",
        "description": "Classifies one URL or IP address as `malicious`, `suspicious` or `not_malicious` and recommends an `action` (`block`, `warn` or `allow`). Call it before letting a user follow a link, open a QR-code destination or accept a connection from an IP you do not already trust. Consumes one unit of the tenant's monthly quota per successful call (cached results included); unparseable input (400) and blocked requests (429) are not billed. Quota and the per-minute limit are counted per tenant across all keys and IPs. It always returns a verdict. Sandbox (`sk_test_`) keys return `environment: sandbox` and `licensedForProduction: false`; use those results for integration testing only.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "examples": {
                "url": {
                  "summary": "A URL",
                  "value": {
                    "target": "https://example.com/login"
                  }
                },
                "ipWithUser": {
                  "summary": "An IP address, with a per-user cache identifier",
                  "value": {
                    "target": "203.0.113.42",
                    "userId": "user-4821"
                  }
                }
              },
              "schema": {
                "$ref": "#/components/schemas/CheckRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A verdict was produced.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              },
              "Server-Timing": {
                "description": "Per-stage timings (auth, rl, quota, pipeline).",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "description": "The plan's requests-per-minute limit.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "clean": {
                    "summary": "Clean result, served from this tenant's cache",
                    "value": {
                      "verdict": "not_malicious",
                      "action": "allow",
                      "mode": "url",
                      "reasons": [
                        "POPULAR_DOMAIN"
                      ],
                      "finalUrl": null,
                      "cached": true,
                      "executionTimeMs": 2,
                      "environment": "production",
                      "licensedForProduction": true,
                      "requestId": "req_01j9z8q7y8k3f5a1b2c3d4e5f6"
                    }
                  },
                  "malicious": {
                    "summary": "Confirmed malicious",
                    "value": {
                      "verdict": "malicious",
                      "action": "block",
                      "mode": "url",
                      "reasons": [
                        "ACTIVE_THREAT_FEED_MATCH",
                        "KNOWN_MALWARE_MATCH"
                      ],
                      "finalUrl": null,
                      "cached": false,
                      "executionTimeMs": 38,
                      "environment": "production",
                      "licensedForProduction": true,
                      "requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
                    }
                  },
                  "sandbox": {
                    "summary": "Sandbox key, suspicious result",
                    "value": {
                      "verdict": "suspicious",
                      "action": "warn",
                      "mode": "url",
                      "reasons": [
                        "BRAND_LOOKALIKE_DOMAIN"
                      ],
                      "finalUrl": null,
                      "cached": false,
                      "executionTimeMs": 47,
                      "environment": "sandbox",
                      "licensedForProduction": false,
                      "requestId": "req_01j9z8qf2e5t9m1n2p3q4r5s6t"
                    }
                  },
                  "ip": {
                    "summary": "IP address with related hosts",
                    "value": {
                      "verdict": "malicious",
                      "action": "block",
                      "mode": "ip",
                      "reasons": [
                        "MALICIOUS_IP_ASSOCIATION"
                      ],
                      "finalUrl": null,
                      "cached": false,
                      "executionTimeMs": 14,
                      "environment": "production",
                      "licensedForProduction": true,
                      "requestId": "req_01j9z8qg7h2j4k6m8n0p1q2r3s",
                      "related": [
                        {
                          "host": "login-verify.example",
                          "verdict": "malicious"
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/CheckResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "usage"
        ],
        "summary": "Read this month's usage and remaining quota",
        "description": "Returns the monthly usage summary for the calling tenant: the plan quota, the requests counted so far and the requests still available. Defaults to the current month (UTC); pass `month=YYYY-MM` for another. Figures come from the billing aggregates and can lag real time by up to about an hour, so use this for reporting, not for deciding whether the next call will succeed. For an hour-by-hour view use `getUsageHourly`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-(0[1-9]|1[0-2])$",
              "description": "Calendar month (UTC) as YYYY-MM. Defaults to the current month.",
              "example": "2026-09"
            },
            "required": false,
            "description": "Calendar month (UTC) as YYYY-MM. Defaults to the current month.",
            "name": "month",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Monthly usage summary.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "tenantId": "ten_01j9z2k3f5g6h7a1b2c3d4e5f6",
                      "month": "2026-09",
                      "monthlyQuota": 50000,
                      "totalRequests": 42817,
                      "availableRequests": 7183
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/v1/usage/hourly": {
      "get": {
        "operationId": "getUsageHourly",
        "tags": [
          "usage"
        ],
        "summary": "Read the hourly usage breakdown with rate-limit utilization",
        "description": "Returns hour-by-hour usage for one month (default: the current month, UTC) so you can see when traffic happened and how close it came to the per-minute rate limit. Each hour reports `capacityUsedPercent` against `hourlyCapacity` (the plan's requests-per-minute limit times 60) and how many requests were blocked in that hour for exceeding the per-minute limit or the monthly quota. `hourlyCapacity` is a reading aid: no hourly quota exists, limits are enforced per minute.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-(0[1-9]|1[0-2])$",
              "description": "Calendar month (UTC) as YYYY-MM. Defaults to the current month.",
              "example": "2026-09"
            },
            "required": false,
            "description": "Calendar month (UTC) as YYYY-MM. Defaults to the current month.",
            "name": "month",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Hourly usage for the month.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "tenantId": "ten_01j9z2k3f5g6h7a1b2c3d4e5f6",
                      "month": "2026-09",
                      "rpmLimit": 30,
                      "hourlyCapacity": 1800,
                      "hours": [
                        {
                          "hour": "2026-09-01T00:00:00Z",
                          "mode": "url",
                          "total": 104,
                          "capacityUsedPercent": 5.8,
                          "blockedRpm": 0,
                          "blockedQuota": 0,
                          "malicious": 2,
                          "suspicious": 3,
                          "notMalicious": 99,
                          "cached": 81
                        },
                        {
                          "hour": "2026-09-01T09:00:00Z",
                          "mode": "url",
                          "total": 1766,
                          "capacityUsedPercent": 98.1,
                          "blockedRpm": 34,
                          "blockedQuota": 0,
                          "malicious": 5,
                          "suspicious": 40,
                          "notMalicious": 1721,
                          "cached": 1200
                        }
                      ]
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/HourlyUsageResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/v1/allow-list": {
      "get": {
        "operationId": "listAllowList",
        "tags": [
          "lists"
        ],
        "summary": "List allow list entries",
        "description": "Lists this tenant's allow list entries, newest first. Pro and Business plans only. Entries are private to the tenant.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "description": "Page size, 1-500 (default 100)."
            },
            "required": false,
            "description": "Page size, 1-500 (default 100).",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 256,
              "description": "`nextCursor` from the previous page."
            },
            "required": false,
            "description": "`nextCursor` from the previous page.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of entries.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "entries": [
                        {
                          "id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
                          "matchType": "domain",
                          "value": "malicious-example.com",
                          "createdAt": "2026-09-01T09:30:00Z"
                        }
                      ],
                      "nextCursor": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ListEntriesResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "post": {
        "operationId": "addAllowListEntry",
        "tags": [
          "lists"
        ],
        "summary": "Add a allow list entry",
        "description": "Adds a URL, host, domain or IP to the allow list. An allowed URL, host, domain or IP is returned as `not_malicious` unless it is confirmed malicious, in which case it is returned as `suspicious` (`ALLOW_LIST_CONFLICT`). Adding an entry that already exists returns the existing entry. Requires a key with the `admin` scope. Pro and Business plans only. Takes effect within about a minute.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "examples": {
                "domain": {
                  "summary": "A registrable domain",
                  "value": {
                    "matchType": "domain",
                    "value": "malicious-example.com"
                  }
                },
                "url": {
                  "summary": "An exact URL",
                  "value": {
                    "matchType": "url",
                    "value": "https://shop.example.com/pay?id=7"
                  }
                }
              },
              "schema": {
                "$ref": "#/components/schemas/AddListEntryRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The entry already existed.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
                      "matchType": "domain",
                      "value": "malicious-example.com",
                      "createdAt": "2026-09-01T09:30:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ListEntry"
                }
              }
            }
          },
          "201": {
            "description": "The entry was created.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
                      "matchType": "domain",
                      "value": "malicious-example.com",
                      "createdAt": "2026-09-01T09:30:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ListEntry"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/v1/allow-list/{entryId}": {
      "delete": {
        "operationId": "deleteAllowListEntry",
        "tags": [
          "lists"
        ],
        "summary": "Remove a allow list entry",
        "description": "Removes one allow list entry by id. Requires a key with the `admin` scope. Pro and Business plans only.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64,
              "description": "Entry id returned by the list endpoints."
            },
            "required": true,
            "description": "Entry id returned by the list endpoints.",
            "name": "entryId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "The entry was removed.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/v1/block-list": {
      "get": {
        "operationId": "listBlockList",
        "tags": [
          "lists"
        ],
        "summary": "List block list entries",
        "description": "Lists this tenant's block list entries, newest first. Available on every plan. Entries are private to the tenant.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "description": "Page size, 1-500 (default 100)."
            },
            "required": false,
            "description": "Page size, 1-500 (default 100).",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 256,
              "description": "`nextCursor` from the previous page."
            },
            "required": false,
            "description": "`nextCursor` from the previous page.",
            "name": "cursor",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of entries.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "entries": [
                        {
                          "id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
                          "matchType": "domain",
                          "value": "malicious-example.com",
                          "createdAt": "2026-09-01T09:30:00Z"
                        }
                      ],
                      "nextCursor": null
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ListEntriesResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "post": {
        "operationId": "addBlockListEntry",
        "tags": [
          "lists"
        ],
        "summary": "Add a block list entry",
        "description": "Adds a URL, host, domain or IP to the block list. A blocked URL, host, domain or IP is always returned as `malicious` (`BLOCK_LIST_MATCH`). Adding an entry that already exists returns the existing entry. Requires a key with the `admin` scope. Available on every plan. Takes effect within about a minute.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "examples": {
                "domain": {
                  "summary": "A registrable domain",
                  "value": {
                    "matchType": "domain",
                    "value": "malicious-example.com"
                  }
                },
                "url": {
                  "summary": "An exact URL",
                  "value": {
                    "matchType": "url",
                    "value": "https://shop.example.com/pay?id=7"
                  }
                }
              },
              "schema": {
                "$ref": "#/components/schemas/AddListEntryRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The entry already existed.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
                      "matchType": "domain",
                      "value": "malicious-example.com",
                      "createdAt": "2026-09-01T09:30:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ListEntry"
                }
              }
            }
          },
          "201": {
            "description": "The entry was created.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
                      "matchType": "domain",
                      "value": "malicious-example.com",
                      "createdAt": "2026-09-01T09:30:00Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/ListEntry"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/v1/block-list/{entryId}": {
      "delete": {
        "operationId": "deleteBlockListEntry",
        "tags": [
          "lists"
        ],
        "summary": "Remove a block list entry",
        "description": "Removes one block list entry by id. Requires a key with the `admin` scope. Available on every plan.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64,
              "description": "Entry id returned by the list endpoints."
            },
            "required": true,
            "description": "Entry id returned by the list endpoints.",
            "name": "entryId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "The entry was removed.",
            "headers": {
              "X-Request-Id": {
                "description": "Identifier to quote when contacting support.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "meta"
        ],
        "summary": "Service liveness",
        "description": "Lightweight liveness check. No API key required. It does not report the freshness of the data behind the verdicts.",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "status": "ok"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "tags": [
          "meta"
        ],
        "summary": "This specification as JSON",
        "description": "Returns this OpenAPI 3.1 document. No API key required; the contract itself is not sensitive.",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "examples": {
                  "default": {
                    "value": {
                      "openapi": "3.1.0",
                      "info": {
                        "title": "SkanQRCode API",
                        "version": "1.7.0"
                      }
                    }
                  }
                },
                "schema": {
                  "type": "object",
                  "properties": {},
                  "additionalProperties": {},
                  "description": "An OpenAPI 3.1 document."
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {}
}
