{
  "components": {
    "schemas": {
      "netinfo.Confidence": {
        "enum": [
          "high",
          "medium",
          "low"
        ],
        "type": "string",
        "x-enum-varnames": [
          "ConfHigh",
          "ConfMedium",
          "ConfLow"
        ]
      },
      "netinfo.Result": {
        "properties": {
          "confidence": {
            "$ref": "#/components/schemas/netinfo.Confidence"
          },
          "confidencePct": {
            "type": "integer"
          },
          "provenance": {
            "description": "Provenance is the matched rule's CSV origin (seed / derived / dataset /\nvote-…), distinct from Source (which stays the coarse decision path:\nrule / cloud / heuristic). It lets a consumer tell a hand-verified rule\nfrom an ASdb-derived guess. Empty (omitted) for the cloud and heuristic\npaths, which have no rule origin.",
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "type": {
            "$ref": "#/components/schemas/netinfo.Type"
          }
        },
        "type": "object"
      },
      "netinfo.Type": {
        "enum": [
          "hosting",
          "datacenter",
          "mobile",
          "isp",
          "home",
          "business",
          "private",
          ""
        ],
        "type": "string",
        "x-enum-comments": {
          "TypePrivate": "non-global input (short-circuit)",
          "TypeUnknown": "global but undetermined"
        },
        "x-enum-descriptions": [
          "",
          "",
          "",
          "",
          "",
          "",
          "non-global input (short-circuit)",
          "global but undetermined"
        ],
        "x-enum-varnames": [
          "TypeHosting",
          "TypeDatacenter",
          "TypeMobile",
          "TypeISP",
          "TypeHome",
          "TypeBusiness",
          "TypePrivate",
          "TypeUnknown"
        ]
      },
      "rdap.ASNInfo": {
        "properties": {
          "autnumType": {
            "type": "string"
          },
          "contacts": {
            "$ref": "#/components/schemas/rdap.Contacts"
          },
          "country": {
            "type": "string"
          },
          "dataMayBeRedacted": {
            "type": "boolean"
          },
          "endAutnum": {
            "type": "integer"
          },
          "events": {
            "items": {
              "$ref": "#/components/schemas/rdap.Event"
            },
            "type": "array"
          },
          "name": {
            "type": "string"
          },
          "org": {
            "type": "string"
          },
          "redacted": {
            "items": {
              "$ref": "#/components/schemas/rdap.Redaction"
            },
            "type": "array"
          },
          "remarks": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "startAutnum": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "rdap.Contact": {
        "properties": {
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "org": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "rdap.Contacts": {
        "properties": {
          "abuse": {
            "$ref": "#/components/schemas/rdap.Contact"
          },
          "admin": {
            "$ref": "#/components/schemas/rdap.Contact"
          },
          "tech": {
            "$ref": "#/components/schemas/rdap.Contact"
          }
        },
        "type": "object"
      },
      "rdap.DomainInfo": {
        "properties": {
          "contacts": {
            "$ref": "#/components/schemas/rdap.Contacts"
          },
          "dataMayBeRedacted": {
            "type": "boolean"
          },
          "events": {
            "items": {
              "$ref": "#/components/schemas/rdap.Event"
            },
            "type": "array"
          },
          "ldhName": {
            "type": "string"
          },
          "nameservers": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "org": {
            "type": "string"
          },
          "redacted": {
            "items": {
              "$ref": "#/components/schemas/rdap.Redaction"
            },
            "type": "array"
          },
          "remarks": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "secureDNS": {
            "type": "boolean"
          },
          "status": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "rdap.Event": {
        "properties": {
          "action": {
            "type": "string"
          },
          "date": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "rdap.Info": {
        "properties": {
          "cidr": {
            "type": "string"
          },
          "contacts": {
            "$ref": "#/components/schemas/rdap.Contacts"
          },
          "country": {
            "type": "string"
          },
          "endAddress": {
            "type": "string"
          },
          "events": {
            "items": {
              "$ref": "#/components/schemas/rdap.Event"
            },
            "type": "array"
          },
          "handle": {
            "type": "string"
          },
          "ipVersion": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "org": {
            "description": "Org is a convenience: the first non-empty contact org, admin/tech\npreferred over abuse. Admin/tech identify the block's actual user (the\ndownstream service provider, e.g. RUVDS), whereas abuse often names the\nupstream AS holder that handles complaints for the whole AS (e.g. MTW);\npreferring admin/tech keeps this from shadowing the real provider.\nAbuse stays the fallback when no admin/tech org exists.",
            "type": "string"
          },
          "remarks": {
            "description": "filtered + truncated",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "startAddress": {
            "type": "string"
          },
          "status": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "type": {
            "description": "allocation type (DIRECT ALLOCATION, ASSIGNED PA, …), not usage type",
            "type": "string"
          }
        },
        "type": "object"
      },
      "rdap.Redaction": {
        "properties": {
          "method": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.EnrichError": {
        "properties": {
          "message": {
            "type": "string"
          },
          "source": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.Status": {
        "enum": [
          "ok",
          "partial",
          "error"
        ],
        "type": "string",
        "x-enum-comments": {
          "StatusError": "every attempted enrichment failed — nothing could be produced",
          "StatusOK": "every attempted enrichment succeeded (or nothing was attempted)",
          "StatusPartial": "at least one enrichment succeeded, at least one failed"
        },
        "x-enum-descriptions": [
          "every attempted enrichment succeeded (or nothing was attempted)",
          "at least one enrichment succeeded, at least one failed",
          "every attempted enrichment failed — nothing could be produced"
        ],
        "x-enum-varnames": [
          "StatusOK",
          "StatusPartial",
          "StatusError"
        ]
      },
      "web.anonTierDTO": {
        "properties": {
          "displayName": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "sets": {
            "items": {
              "$ref": "#/components/schemas/web.tierSetDTO"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.apiErrorBody": {
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.apiResponseV2": {
        "properties": {
          "appVersion": {
            "description": "AppVersion is the config-driven application version. Additive and\nomitempty — absent when the handler wasn't given a version.",
            "type": "string"
          },
          "cachedAt": {
            "type": "string"
          },
          "censorship": {
            "$ref": "#/components/schemas/web.censorshipV2"
          },
          "countReq": {
            "type": "integer"
          },
          "dns": {
            "$ref": "#/components/schemas/web.dnsV2"
          },
          "errors": {
            "items": {
              "$ref": "#/components/schemas/web.EnrichError"
            },
            "type": "array"
          },
          "flags": {
            "$ref": "#/components/schemas/web.flagsV2"
          },
          "fraud": {
            "$ref": "#/components/schemas/web.fraudV2"
          },
          "gated": {
            "description": "Gated lists metered group names (geo/network/security) an authed\ncaller's tier is not entitled to at all; their blocks stay null. Always\nempty for anonymous (entitled to all 3 groups, metered by anon defaults).",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "geo": {
            "$ref": "#/components/schemas/web.geoV2"
          },
          "ip": {
            "type": "string"
          },
          "limited": {
            "description": "Limited lists metered group names withheld for a RATE reason this\nrequest (over the per-unique-IP daily or per-minute budget, top-up\nempty, or a concurrent same-IP duplicate) — as opposed to Gated (not\nentitled at all). A non-empty Gated or Limited downgrades Status from\nok to partial (still HTTP 200).",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "network": {
            "$ref": "#/components/schemas/web.networkV2"
          },
          "plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/web.planV2"
              }
            ],
            "description": "Plan is the additive usage/entitlement summary: see planV2 and\nbuildPlan. omitempty (nil) when there is no token/group usage to report\n(a bare baseline request with nothing requested) — same additive posture\nas Tier/Gated/Limited above."
          },
          "privacy": {
            "$ref": "#/components/schemas/web.privacyV2"
          },
          "recon": {
            "allOf": [
              {
                "$ref": "#/components/schemas/web.reconV2"
              }
            ],
            "description": "Recon is the ADVISORY Dataplane.org active-recon/scanner-source signal\n— its OWN block, deliberately separate from Threat (never folds into\nthreat.isMalicious). Null unless the security group is populated AND\nthe signal was actually evaluated for the IP — see reconV2's doc\ncomment."
          },
          "schemaVersion": {
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/web.Status"
          },
          "threat": {
            "$ref": "#/components/schemas/web.threatV2"
          },
          "tier": {
            "description": "Tier/Gated/Limited are additive entitlement fields (omitempty — an\nolder client ignoring them sees no behavioral change).\nTier is the resolved caller tier (\"anonymous\"/\"free\"/\"pro\"/\"business\").",
            "type": "string"
          },
          "version": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.asnLookupResponse": {
        "properties": {
          "asn": {
            "type": "integer"
          },
          "data": {
            "$ref": "#/components/schemas/rdap.ASNInfo"
          }
        },
        "type": "object"
      },
      "web.asnPrefixesResponse": {
        "properties": {
          "asn": {
            "type": "integer"
          },
          "hasNext": {
            "type": "boolean"
          },
          "limited": {
            "type": "boolean"
          },
          "page": {
            "type": "integer"
          },
          "perPage": {
            "type": "integer"
          },
          "prefixes": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "total": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.bogonCheckReq": {
        "properties": {
          "ips": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.bogonCheckResp": {
        "properties": {
          "results": {
            "items": {
              "$ref": "#/components/schemas/web.bogonCheckResult"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.bogonCheckResult": {
        "properties": {
          "class": {
            "type": "string"
          },
          "ip": {
            "type": "string"
          },
          "unassigned": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "web.censorshipV2": {
        "properties": {
          "confirmedRate": {
            "description": "ConfirmedRate is the fraction of OONI measurements classified as\nconfirmed blocking over the rolling window, for the caller's ASN+country.",
            "type": "number"
          },
          "isCensored": {
            "description": "IsCensored reports whether ConfirmedRate exceeds the configured\nblocking threshold for the caller's ASN+country (OONI-derived).",
            "type": "boolean"
          },
          "sampleSize": {
            "description": "SampleSize is the number of OONI measurements aggregated into\nConfirmedRate.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.createTokenReq": {
        "properties": {
          "name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.createTokenResp": {
        "properties": {
          "createdAt": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "token": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.dnsV2": {
        "properties": {
          "hostnames": {
            "description": "Hostnames lists every hostname the PTR lookup returned (the rare\nmulti-PTR case); always a non-nil array, empty when PTR is empty.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "ptr": {
            "description": "PTR is the reverse-DNS (PTR record) hostname for the IP, empty when none\nresolved; baseline.",
            "type": "string"
          },
          "ptrFetchedAt": {
            "description": "PTRFetchedAt is the fetch time of the reverse-DNS lookup above, RFC3339.\nVerbose-only: nil (omitted) on a non-verbose request. See the doc on\nnetworkV2.RDAPFetchedAt/ASNFetchedAt for the nil-vs-empty-string rule.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.domainLookupResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/rdap.DomainInfo"
          },
          "domain": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.errorBody": {
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.featureSetDTO": {
        "properties": {
          "key": {
            "type": "string"
          },
          "labelKey": {
            "type": "string"
          },
          "metered": {
            "type": "boolean"
          },
          "topupable": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "web.flagsV2": {
        "properties": {
          "cloudProvider": {
            "type": "string"
          },
          "cloudRegion": {
            "type": "string"
          },
          "isAnycast": {
            "description": "IsAnycast/IsSatellite are IPFire libloc network-privacy classification\nfacts: topology/access-type signals, not privacy (the anonymous-proxy\nlibloc flag feeds privacy.isProxy/isAnonymous instead, see\nprivacyFromResponse). Sourced from Response.NetFlags (data.NetFlags), a\nbest-effort MariaDB longest-prefix lookup — both false when the\nresolver is disabled, has no covering prefix, or errored. No overlap\nwith cloudtor's isCloud/isTor above.",
            "type": "boolean"
          },
          "isBogon": {
            "type": "boolean"
          },
          "isCGNAT": {
            "type": "boolean"
          },
          "isCloud": {
            "description": "Cloud/Tor matcher signals. isCloud/isTor are always present;\ncloudProvider/cloudRegion are omitted when empty. isTor is independent of\nthe cloud fields (a Tor exit node is not \"cloud\").",
            "type": "boolean"
          },
          "isGlobal": {
            "type": "boolean"
          },
          "isLinkLocal": {
            "type": "boolean"
          },
          "isLoopback": {
            "type": "boolean"
          },
          "isMulticast": {
            "type": "boolean"
          },
          "isPrivate": {
            "type": "boolean"
          },
          "isSatellite": {
            "type": "boolean"
          },
          "isTor": {
            "type": "boolean"
          },
          "isUnassigned": {
            "description": "IsUnassigned is the Team Cymru fullbogons live unassigned-RIR-space\nflag: extends the static IsBogon flag above with a runtime-refreshed\ndelta — space that isn't in ipclass's curated reserved-prefix table (so\nIsBogon is false, IsGlobal true) but is nonetheless currently\nunassigned per Team Cymru's continuously updated fullbogons list. False\nwhen the matcher is disabled or has no covering prefix, same degrade\nposture as IsCloud/IsTor above.",
            "type": "boolean"
          },
          "isUnspecified": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "web.fraudV2": {
        "properties": {
          "level": {
            "description": "Level buckets Score into low/medium/high against the service's\nconfigured thresholds.",
            "type": "string"
          },
          "score": {
            "description": "Score is the 0-100 risk-score rollup: a weighted sum of the fired\nSignals, saturating at 100.",
            "type": "integer"
          },
          "signals": {
            "description": "Signals names every weighted input that fired (e.g. \"hosting\", \"vpn\",\n\"tor\", \"malicious\", \"censored\", \"ptrMissing\", \"bulkMail\"), so Score is\nexplainable; empty ([]) for a clean IP.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.geoAltV2": {
        "properties": {
          "action": {
            "description": "Action distinguishes a folded geo_history row from a live alternative\n(verbose-only): \"\" for a live alternative (current reading of a losing\nsource), \"superseded\"/\"deleted\" for a geo_history snapshot.",
            "type": "string"
          },
          "city": {
            "description": "City mirrors geoV2.City for this alternative source.",
            "type": "string"
          },
          "continent": {
            "description": "Continent mirrors geoV2.Continent for this alternative source.",
            "type": "string"
          },
          "countryCode": {
            "description": "CountryCode mirrors geoV2.CountryCode for this alternative source's reading.",
            "type": "string"
          },
          "countryName": {
            "description": "CountryName mirrors geoV2.CountryName for this alternative source.",
            "type": "string"
          },
          "createdDate": {
            "description": "CreatedDate is the alternative row's first_seen (creation), vs Date=last_seen.",
            "type": "string"
          },
          "date": {
            "description": "Date is this alternative's row last_seen, RFC3339 UTC — same\nformat/semantics as geoV2.Date but for the losing source.",
            "type": "string"
          },
          "isEu": {
            "description": "IsEu mirrors geoV2.IsEu for this alternative source's country.",
            "type": "boolean"
          },
          "lat": {
            "description": "Lat mirrors geoV2.Lat for this alternative source (same nil-vs-0.0 rule).",
            "type": "number"
          },
          "license": {
            "description": "License/URL mirror geoV2.License/URL for this alternative source's own\nprovenance — same verbose-only gating (buildAPIResponseV2 sets these\nonly inside the verbose branch, matched by index against\nr.GeoAlternatives; a non-verbose response leaves them empty/omitted, even\nthough the rest of this alternative's fields are already populated).",
            "type": "string"
          },
          "lon": {
            "description": "Lon mirrors geoV2.Lon for this alternative source.",
            "type": "number"
          },
          "postal": {
            "description": "Postal mirrors geoV2.Postal for this alternative source (always empty today).",
            "type": "string"
          },
          "region": {
            "description": "Region mirrors geoV2.Region for this alternative source.",
            "type": "string"
          },
          "retrievedDate": {
            "description": "RetrievedDate is when the continuous RIR crawler retrieved this\nalternative's source observation, RFC3339 UTC. Verbose-only and omitted\nfor non-RIR or legacy unmarked RIR rows.",
            "type": "string"
          },
          "source": {
            "description": "Source names this alternative's (losing) geo source (e.g. \"dbip-city\"\nor \"aws-geofeed\") — as opposed to the winning geoV2.Source.",
            "type": "string"
          },
          "timezone": {
            "description": "Timezone mirrors geoV2.Timezone for this alternative source.",
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.geoV2": {
        "properties": {
          "alternatives": {
            "description": "Alternatives are the losing geo sources' readings for this IP: each is\nthat source's most-specific covering location plus its own source name\nand date. Omitted when only one source covers the IP.",
            "items": {
              "$ref": "#/components/schemas/web.geoAltV2"
            },
            "type": "array"
          },
          "city": {
            "description": "City is the city name from the winning source; \"geo\" group.",
            "type": "string"
          },
          "cityNameLocal": {
            "description": "CityNameLocal is the city's Russian name from the offline GeoNames\ngazetteer — resolved from the winner's (countryCode, region, city)\ntext. Empty when the gazetteer has no match, no ru altname exists for\nthe matched city, or there was nothing to resolve (no country/city on\nthe winner). Part of the \"geo\" group like City itself — omitted on a\nbaseline country-only response.",
            "type": "string"
          },
          "confidence": {
            "description": "Confidence flags city-level geo precision as \"low\" for network types\nwhere the city reading is unreliable (by default, mobile/carrier\nranges, where the registered location often differs from the\nsubscriber's).",
            "type": "string"
          },
          "continent": {
            "description": "Continent is the two-letter continent code, re-derived alongside\nCountryName from the country table; baseline.",
            "type": "string"
          },
          "countryCode": {
            "description": "CountryCode is the ISO 3166-1 alpha-2 country code from the winning geo\nsource (DB-IP City Lite or an RFC 8805 geofeed); baseline (always\npresent whenever any geo data exists for the IP).",
            "type": "string"
          },
          "countryName": {
            "description": "CountryName is the country's display name, re-derived from the country\ntable for CountryCode (so a country-only override never carries a\nstale name); baseline.",
            "type": "string"
          },
          "date": {
            "description": "Date is the winning geo source's last-seen date, RFC3339 UTC\ndate-time — the same format as the top-level cachedAt. Omitted when\nunknown.",
            "type": "string"
          },
          "geonameId": {
            "description": "GeonameID is the matched GeoNames gazetteer entry's id — a stable\ncross-source city identifier distinct from City's raw text. Nilable:\nnil (JSON null/absent) means a resolve miss, never a fabricated 0.\nPart of the \"geo\" group like City itself.",
            "type": "integer"
          },
          "isEu": {
            "description": "IsEu reports whether CountryCode is an EU member state, re-derived from\nthe country table like CountryName/Continent; baseline.",
            "type": "boolean"
          },
          "lat": {
            "description": "Lat is the latitude in decimal degrees; nil means genuinely unknown,\ndistinct from a real 0.0 reading at the equator. \"geo\" group.",
            "type": "number"
          },
          "license": {
            "description": "License is the winning source's data license (e.g. \"CC BY 4.0\" for\ndbip-city) — provenance/attribution detail, not baseline. Present only\nalongside the extended geo fields above, on a verbose request. Omitted\nwhen the source has no recorded license.",
            "type": "string"
          },
          "lon": {
            "description": "Lon is the longitude in decimal degrees; same nil-vs-0.0 semantics as\nLat. \"geo\" group.",
            "type": "number"
          },
          "matchConfidence": {
            "description": "MatchConfidence is the match-quality signal for the\ncityNameLocal/geonameId resolve: \"high\" when country+admin1+name all\nmatched, \"low\" when it fell back to a country+name-only homonym pick.\nOmitted (not \"unknown\") on a resolve miss — a consumer must not read\nabsence as low confidence, only as \"no match at all\".\nDeliberately a SEPARATE field from Confidence above: Confidence is\nnet-type-driven geo precision (mobile/MVNO carrier heuristic),\nMatchConfidence is gazetteer string-match quality — unrelated signals,\nnever merged.",
            "type": "string"
          },
          "postal": {
            "description": "Postal is the postal/ZIP code; always empty today (DB-IP City Lite\ncarries no postal data) — reserved for a future source. \"geo\" group.",
            "type": "string"
          },
          "region": {
            "description": "Region is the state/province/administrative-region name from the\nwinning source; part of the \"geo\" group (entitlement.SetGeo) — empty\non a baseline country-only response.",
            "type": "string"
          },
          "regionMatchConfidence": {
            "description": "RegionMatchConfidence reports the stored raw-region to GeoNames admin1\nresolution quality. Omitted when the raw fallback is used.",
            "type": "string"
          },
          "retrievedDate": {
            "description": "RetrievedDate is when the continuous RIR crawler retrieved the winning\nsource observation, RFC3339 UTC. Verbose-only and omitted for non-RIR or\nlegacy unmarked RIR rows; this is deliberately distinct from Date\n(SCD-2 last_seen).",
            "type": "string"
          },
          "source": {
            "description": "Source is geo provenance: the name of the trust-ranked winning geo\nsource (e.g. \"dbip-city\" or \"aws-geofeed\"). Always populated when geo\nis present (the winner is whichever source the merge picked);\nomitempty is kept only for the degenerate case of an unnamed source.",
            "type": "string"
          },
          "timezone": {
            "description": "Timezone is the IANA zone name (e.g. \"Europe/Moscow\") for the resolved\nlocation; \"geo\" group.",
            "type": "string"
          },
          "url": {
            "description": "URL is the winning geo source's own reference link (its dataset\nhomepage or download page, e.g. \"https://db-ip.com/db/lite.php\" for\ndbip-city) — attribution/provenance detail alongside License, not a\nper-IP resource link. Same gating and omission rule as License.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.groupPlanV2": {
        "properties": {
          "perDay": {
            "allOf": [
              {
                "$ref": "#/components/schemas/web.rateWindowV2"
              }
            ],
            "description": "PerDay is the per-day unique-IP budget window for this metered group;\nnil when not checked this request (e.g. pre-empted by an exhausted\nPerMin) or unlimited."
          },
          "perMin": {
            "allOf": [
              {
                "$ref": "#/components/schemas/web.rateWindowV2"
              }
            ],
            "description": "PerMin is the per-minute unique-IP budget window for this metered\ngroup; nil when not checked this request or unlimited (no per_min\nconfigured for the tier+group)."
          },
          "topupRemaining": {
            "description": "TopupRemaining is the live top-up balance for this group, peeked (not\nspent); nil for an anonymous caller or a cold/never-spent key —\nabsence is not a zero-balance signal.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.networkV2": {
        "properties": {
          "allAsns": {
            "description": "AllASNs lists every ASN Team Cymru reports announcing this prefix\n(multi-origin); always a non-nil array, empty when there's only one/none.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "allocated": {
            "description": "Allocated is Prefix's allocation date from Team Cymru — an allocation\nTIMESTAMP, not a usage-type flag (see UsageHint for that).",
            "type": "string"
          },
          "asName": {
            "description": "ASName is the Team Cymru AS holder name — the coarse registered name,\ndistinct from the refined network.classification.provider.",
            "type": "string"
          },
          "asn": {
            "description": "ASN is the announcing Autonomous System number from Team Cymru (e.g.\n\"AS15169\"); baseline (always present for a routable IP with ASN data).",
            "type": "string"
          },
          "asnDomain": {
            "description": "AsnDomain is the offline iplocate org's domain, sibling to AsnOrg; nil\nunder the same conditions.",
            "type": "string"
          },
          "asnFetchedAt": {
            "description": "ASNFetchedAt is the fetch time of the Team Cymru ASN facts above (asn/\nallAsns/asName/prefix/registry/allocated/usageHint); see RDAPFetchedAt's\ndoc above for the nil-vs-empty-string rule. Verbose-only.",
            "type": "string"
          },
          "asnOrg": {
            "description": "AsnOrg/AsnDomain carry the offline iplocate org name/domain, populated\nfrom the MariaDB data resolver. nil/null when the resolver is off or the\nIP has no matching ASN block. These are distinct from the live Cymru\nfacts above (asn/asName/prefix/registry/allocated/usageHint).",
            "type": "string"
          },
          "classification": {
            "allOf": [
              {
                "$ref": "#/components/schemas/netinfo.Result"
              }
            ],
            "description": "Classification is the synthesized IP-type verdict (hosting/datacenter/\nresidential/mobile/…); always populated for a routable IP."
          },
          "prefix": {
            "description": "Prefix is the announced CIDR block covering the IP, from Team Cymru.",
            "type": "string"
          },
          "rdap": {
            "allOf": [
              {
                "$ref": "#/components/schemas/rdap.Info"
              }
            ],
            "description": "RDAP is a pointer so the block is JSON null when there is no RDAP data\nat all; when the \"network\" group isn't included in the response it\ninstead carries a trimmed {country, handle, name} subset (always\npresent at baseline). asnOrg/asnDomain above are nullable for the same\nnot-included reason."
          },
          "rdapFetchedAt": {
            "description": "RDAPFetchedAt/ASNFetchedAt are the fetch times of the RDAP registry record\nand the Team Cymru ASN facts above, RFC3339 (same format as the top-level\ncachedAt). Verbose-only: nil (omitted) on a non-verbose request.\n\nOn a verbose request the pointer distinguishes two states that both\nstringify to \"\": nil means the source was never resolved (disabled, or\ngenuinely no data), while a non-nil pointer to \"\" means the sibling data\n(rdap/asn) IS present but the store that served it can't report a fetch\ntime (e.g. a MariaDB-only deployment). Without this, a consumer can't\ntell \"no data\" from \"data, unknown age\".",
            "type": "string"
          },
          "registry": {
            "description": "Registry is the RIR that allocated Prefix (e.g. \"ripencc\", \"arin\"), from\nTeam Cymru.",
            "type": "string"
          },
          "usageHint": {
            "description": "UsageHint is Team Cymru's coarse allocation type (e.g. \"DIRECT\nALLOCATION\", \"ASSIGNED PA\"), not a usage-type classification of the IP\nitself.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.planV2": {
        "properties": {
          "groups": {
            "additionalProperties": {
              "$ref": "#/components/schemas/web.groupPlanV2"
            },
            "description": "Groups is keyed by metered group name (geo/network/security);\nrequest-scoped like limited[]/gated[] — a group this request's metering\npipeline never reached (not requested, gated, or concurrent-duplicate\nwithheld) has no entry, since there is no live-peek primitive for an\nunrequested group's cardinality without mutating its SET.",
            "type": "object"
          },
          "servedFromTopup": {
            "description": "ServedFromTopup lists metered groups whose per-day budget was exhausted\nthis request and were served from the persistent top-up bucket instead.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "tier": {
            "description": "Tier duplicates the top-level Tier field (self-contained for a consumer\nreading only the plan section, same as GET /api/v1/usage's own \"tier\").",
            "type": "string"
          },
          "tokenName": {
            "description": "TokenName is the API token's name, present when the caller\nauthenticated via an opaque API token; omitted for a session-JWT-authed\nor anonymous caller.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.privacyV2": {
        "properties": {
          "isAnonymous": {
            "description": "IsAnonymous is derived as IsTor || IsVPN || IsProxy; inherits IsTor's\ndegraded-false posture when the cloudtor matcher didn't run, and\nIsProxy's when the net-flags resolver didn't run/match.",
            "type": "boolean"
          },
          "isProxy": {
            "description": "IsProxy is the IPFire libloc \"is-anonymous-proxy\" network flag: false\nwhen the underlying resolver is disabled, has no covering data for the\naddress, or errored — same best-effort posture as IsVPN's rules signal.\nNo other proxy data source is wired.",
            "type": "boolean"
          },
          "isRelay": {
            "description": "IsRelay always reports false — no adopted data source (unused today).",
            "type": "boolean"
          },
          "isResidentialProxy": {
            "description": "IsResidentialProxy always reports false — no free data source exists\n(known gap).",
            "type": "boolean"
          },
          "isTor": {
            "description": "IsTor reuses the cloudtor Tor-exit match (flags.isTor); degraded to\nfalse (not an authoritative negative) when Tor detection isn't enabled\nserver-side.",
            "type": "boolean"
          },
          "isVpn": {
            "description": "IsVPN is the coarse VPN/hosting-posture signal: true only when\nnetwork.classification.source is \"rule\" (X4BNet-derived hosting/\ndatacenter rows) AND type is hosting or datacenter — excludes the\nweaker cloud- and heuristic-sourced hosting matches.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "web.rateWindowV2": {
        "properties": {
          "limit": {
            "description": "Limit is the configured per_day or per_min cap for this window\n(whichever plan.groups[\u003cgroup\u003e] entry it appears under).",
            "type": "integer"
          },
          "remaining": {
            "description": "Remaining is Limit minus the unique-IP count consumed so far this\nwindow, floored at 0.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.reconV2": {
        "properties": {
          "isScanner": {
            "description": "IsScanner is the advisory active-recon/brute-force verdict from the\nDataplane.org matcher; only present when recon was actually evaluated\n(this whole block is null otherwise). NOT threat.isMalicious.",
            "type": "boolean"
          },
          "lastSeen": {
            "description": "LastSeen is the most recent contributing signal's own observation\ntimestamp (RFC3339, same format as cachedAt/geo.date) — lets a\nconsumer judge how fresh the scanning activity is, since Dataplane's\nrecon feeds age out via a configured decay window rather than staying\n\"hot\" forever between refreshes. Omitted when unknown (e.g. IsScanner\nfalse, or every contributing row carried an unparseable timestamp).",
            "type": "string"
          },
          "sources": {
            "description": "Sources lists which Dataplane.org signal(s) recently observed the IP\n(e.g. \"sshpwauth\", \"telnetlogin\"); always a non-nil array, empty ([])\nfor a clean IP — mirrors threat.sources' convention.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.renameTokenReq": {
        "properties": {
          "name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.threatIOCsV2": {
        "properties": {
          "cidrs": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "domains": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "hashes": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "ips": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "urls": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.threatReportV2": {
        "properties": {
          "date": {
            "description": "Date is the event's date (MISP manifest date), RFC3339 UTC — same format\nas the top-level cachedAt/geo.date; empty when unknown.",
            "type": "string"
          },
          "info": {
            "description": "Info is the event's title/description (MISP Event.info).",
            "type": "string"
          },
          "iocs": {
            "allOf": [
              {
                "$ref": "#/components/schemas/web.threatIOCsV2"
              }
            ],
            "description": "IOCs groups the event's persisted indicators by type (threat_event_iocs)\nfor easy consumption; see threatIOCsV2."
          },
          "org": {
            "description": "Org is the reporting organization (MISP Orgc.name); may be empty if the\nsource event never carried one.",
            "type": "string"
          },
          "source": {
            "description": "Source is the threat-intel feed id that persisted this event (e.g.\n\"circl_osint\", \"botvrij_osint\") — the same source vocabulary as\nthreatV2.Sources, but per-event rather than per-IP.",
            "type": "string"
          },
          "threatLevel": {
            "description": "ThreatLevel is this specific event's MISP threat_level_id (1..4), nil\nwhen the event carries none — distinct from the aggregated\nthreatV2.ThreatLevel above (which folds in every matching entry, not\njust this one).",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.threatV2": {
        "properties": {
          "isMalicious": {
            "description": "IsMalicious is the aggregate malicious-IP verdict across the loaded\nthreat-intelligence feeds; only present when the security group was\nevaluated.",
            "type": "boolean"
          },
          "reports": {
            "description": "Reports is the verbose evidence-API payload: one entry per persisted\nMISP event backing this IP's match. Omitted (nil) for a non-verbose\ncaller, an unmatched/unevaluated IP, or when the evidence lookup\nerrored.",
            "items": {
              "$ref": "#/components/schemas/web.threatReportV2"
            },
            "type": "array"
          },
          "sources": {
            "description": "Sources lists which loaded threat-intel feeds flagged the IP; always a\nnon-nil array, empty ([]) for a clean IP.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "threatLevel": {
            "description": "ThreatLevel is the aggregated (most-severe/min) MISP threat_level_id\nacross every matching entry that carries one: 1=High, 2=Medium, 3=Low,\n4=Undefined. Nil (omitted) when no matching entry carries a level —\ne.g. a clean IP, or a match backed only by thin/index-only (non-MISP)\nsources. Gated identically to isMalicious/sources (the security group\nonly) — independent of verbose, unlike Reports below.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.tierCatalogDTO": {
        "properties": {
          "durationDays": {
            "type": "integer"
          },
          "id": {
            "description": "ID is the numeric tier id as a string, matching the stringified-int64\nconvention adminTierDTO.ID already uses — it lets the pricing page\nbuild a per-tier `topup_tier_\u003cid\u003e` deep-link instead of falling back to\nthe generic `/start=topup` action.",
            "type": "string"
          },
          "isDefault": {
            "type": "boolean"
          },
          "name": {
            "type": "string"
          },
          "price": {
            "type": "number"
          },
          "sets": {
            "items": {
              "$ref": "#/components/schemas/web.tierSetDTO"
            },
            "type": "array"
          },
          "verbose": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "web.tierSetDTO": {
        "properties": {
          "allowed": {
            "type": "boolean"
          },
          "featureSet": {
            "type": "string"
          },
          "perDay": {
            "type": "integer"
          },
          "perMin": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.tiersResp": {
        "properties": {
          "anonymous": {
            "$ref": "#/components/schemas/web.anonTierDTO"
          },
          "featureSets": {
            "description": "FeatureSets is the canonical ordered feature-set descriptor — base,\nthen the 3 metered/top-up-able groups — always populated (even when no\ntier catalog is wired) so the frontend has one place to read the\ngeo/network/security order+labels from.",
            "items": {
              "$ref": "#/components/schemas/web.featureSetDTO"
            },
            "type": "array"
          },
          "tiers": {
            "items": {
              "$ref": "#/components/schemas/web.tierCatalogDTO"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.tokenDTO": {
        "properties": {
          "createdAt": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "lastUsedAt": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "revokedAt": {
            "description": "RevokedAt is nil for an active token, else the RFC3339 revocation time\n(the list now includes revoked tokens too, marked via this field).\nNever exposes the hash or raw token.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.tokenListResp": {
        "properties": {
          "cap": {
            "type": "integer"
          },
          "tokens": {
            "items": {
              "$ref": "#/components/schemas/web.tokenDTO"
            },
            "type": "array"
          },
          "used": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.toolsASNRDAP": {
        "properties": {
          "country": {
            "type": "string"
          },
          "handle": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsASNResp": {
        "properties": {
          "allAsns": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "allocated": {
            "type": "string"
          },
          "asName": {
            "type": "string"
          },
          "asn": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "org": {
            "type": "string"
          },
          "prefix": {
            "type": "string"
          },
          "query": {
            "type": "string"
          },
          "queryType": {
            "type": "string"
          },
          "rdap": {
            "$ref": "#/components/schemas/web.toolsASNRDAP"
          },
          "registry": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsDNSRecord": {
        "properties": {
          "priority": {
            "type": "integer"
          },
          "value": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsDNSResp": {
        "properties": {
          "name": {
            "type": "string"
          },
          "records": {
            "items": {
              "$ref": "#/components/schemas/web.toolsDNSRecord"
            },
            "type": "array"
          },
          "truncated": {
            "type": "boolean"
          },
          "truncatedReason": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsHeadersAcceptLanguage": {
        "properties": {
          "lang": {
            "type": "string"
          },
          "q": {
            "type": "number"
          }
        },
        "type": "object"
      },
      "web.toolsHeadersClientHints": {
        "properties": {
          "mobile": {
            "type": "boolean"
          },
          "ua": {
            "type": "string"
          },
          "uaPlatform": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsHeadersParsed": {
        "properties": {
          "acceptEncoding": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "acceptLanguage": {
            "items": {
              "$ref": "#/components/schemas/web.toolsHeadersAcceptLanguage"
            },
            "type": "array"
          },
          "clientHints": {
            "$ref": "#/components/schemas/web.toolsHeadersClientHints"
          },
          "doNotTrack": {
            "type": "boolean"
          },
          "proxyHeaders": {
            "$ref": "#/components/schemas/web.toolsHeadersProxy"
          },
          "userAgent": {
            "$ref": "#/components/schemas/web.toolsHeadersUserAgent"
          }
        },
        "type": "object"
      },
      "web.toolsHeadersProxy": {
        "properties": {
          "present": {
            "type": "boolean"
          },
          "via": {
            "type": "string"
          },
          "xForwardedFor": {
            "type": "string"
          },
          "xRealIp": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsHeadersResp": {
        "properties": {
          "headers": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object"
          },
          "parsed": {
            "$ref": "#/components/schemas/web.toolsHeadersParsed"
          }
        },
        "type": "object"
      },
      "web.toolsHeadersUserAgent": {
        "properties": {
          "raw": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsMACForwardResp": {
        "properties": {
          "isLocallyAdministered": {
            "type": "boolean"
          },
          "isMulticast": {
            "type": "boolean"
          },
          "mac": {
            "type": "string"
          },
          "note": {
            "type": "string"
          },
          "query": {
            "type": "string"
          },
          "queryType": {
            "type": "string"
          },
          "vendor": {
            "$ref": "#/components/schemas/web.toolsMACVendor"
          }
        },
        "type": "object"
      },
      "web.toolsMACVendor": {
        "properties": {
          "block": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsMailDKIM": {
        "properties": {
          "found": {
            "type": "boolean"
          },
          "record": {
            "type": "string"
          },
          "selector": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsMailDMARC": {
        "properties": {
          "adkim": {
            "type": "string"
          },
          "aspf": {
            "type": "string"
          },
          "fo": {
            "type": "string"
          },
          "found": {
            "type": "boolean"
          },
          "malformed": {
            "type": "boolean"
          },
          "pct": {
            "type": "integer"
          },
          "policy": {
            "type": "string"
          },
          "record": {
            "type": "string"
          },
          "rua": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "ruf": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "subdomainPolicy": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsMailMX": {
        "properties": {
          "host": {
            "type": "string"
          },
          "priority": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.toolsMailResp": {
        "properties": {
          "dkim": {
            "$ref": "#/components/schemas/web.toolsMailDKIM"
          },
          "dmarc": {
            "$ref": "#/components/schemas/web.toolsMailDMARC"
          },
          "domain": {
            "type": "string"
          },
          "mx": {
            "items": {
              "$ref": "#/components/schemas/web.toolsMailMX"
            },
            "type": "array"
          },
          "spf": {
            "$ref": "#/components/schemas/web.toolsMailSPF"
          }
        },
        "type": "object"
      },
      "web.toolsMailSPF": {
        "properties": {
          "all": {
            "type": "string"
          },
          "found": {
            "type": "boolean"
          },
          "includes": {
            "items": {
              "$ref": "#/components/schemas/web.toolsMailSPFInclude"
            },
            "type": "array"
          },
          "ip4": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "ip6": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "lookupCount": {
            "type": "integer"
          },
          "multipleRecords": {
            "type": "boolean"
          },
          "record": {
            "type": "string"
          },
          "spfLookupLimitExceeded": {
            "type": "boolean"
          },
          "truncated": {
            "type": "boolean"
          },
          "truncatedReason": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsMailSPFInclude": {
        "properties": {
          "error": {
            "type": "string"
          },
          "host": {
            "type": "string"
          },
          "ip4": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "ip6": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "nestedIncludes": {
            "items": {
              "$ref": "#/components/schemas/web.toolsMailSPFInclude"
            },
            "type": "array"
          },
          "record": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.toolsReputationOwnResp": {
        "properties": {
          "ip": {
            "type": "string"
          },
          "isHosting": {
            "type": "boolean"
          },
          "isMalicious": {
            "type": "boolean"
          },
          "isScanner": {
            "type": "boolean"
          },
          "isTor": {
            "type": "boolean"
          },
          "isVpnOrProxy": {
            "type": "boolean"
          },
          "riskLevel": {
            "type": "string"
          },
          "riskScore": {
            "type": "integer"
          },
          "tier": {
            "type": "string"
          },
          "verdict": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "web.topupPriceDTO": {
        "properties": {
          "price": {
            "type": "number"
          },
          "qtyBucket": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.topupSetDTO": {
        "properties": {
          "featureSet": {
            "type": "string"
          },
          "prices": {
            "items": {
              "$ref": "#/components/schemas/web.topupPriceDTO"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.topupsResp": {
        "properties": {
          "topups": {
            "items": {
              "$ref": "#/components/schemas/web.topupSetDTO"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "web.usageResp": {
        "properties": {
          "sets": {
            "items": {
              "$ref": "#/components/schemas/web.usageSetDTO"
            },
            "type": "array"
          },
          "tier": {
            "type": "string"
          },
          "tierExpiresAt": {
            "type": "string"
          },
          "tokens": {
            "$ref": "#/components/schemas/web.usageTokensDTO"
          }
        },
        "type": "object"
      },
      "web.usageSetDTO": {
        "properties": {
          "allowed": {
            "type": "boolean"
          },
          "featureSet": {
            "type": "string"
          },
          "perDay": {
            "type": "integer"
          },
          "perMin": {
            "type": "integer"
          },
          "topupRemaining": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.usageTokensDTO": {
        "properties": {
          "cap": {
            "type": "integer"
          },
          "used": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "web.whoisErrorResponse": {
        "properties": {
          "error": {
            "$ref": "#/components/schemas/web.apiErrorBody"
          }
        },
        "type": "object"
      },
      "web.whoisResponse": {
        "properties": {
          "abuseHandle": {
            "type": "string"
          },
          "adminHandle": {
            "type": "string"
          },
          "allocationType": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "ip": {
            "type": "string"
          },
          "netName": {
            "type": "string"
          },
          "orgHandle": {
            "type": "string"
          },
          "orgName": {
            "type": "string"
          },
          "originAs": {
            "type": "string"
          },
          "prefix": {
            "type": "string"
          },
          "retrievedAt": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "stale": {
            "type": "boolean"
          },
          "status": {
            "type": "string"
          },
          "techHandle": {
            "type": "string"
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "BearerAuth": {
        "description": "JWT access token or an opaque API token: \"Authorization: Bearer \u003ctoken\u003e\".",
        "in": "header",
        "name": "Authorization",
        "type": "apiKey"
      }
    }
  },
  "info": {
    "contact": {},
    "description": "IP intelligence service. GET /api/v1/ip returns a versioned JSON envelope (apiResponseV2, schemaVersion 16) with baseline classification always present; the geo / network / security enrichment groups are opt-in via ?include= and metered per unique caller IP per day. An over-budget group is soft-withheld (named in limited[], status \"partial\", still HTTP 200) rather than rejected; abuse-rate limiting is enforced at the nginx layer, outside this contract. Auth, API tokens, the tier/pricing catalog, usage, and the admin cabinet live under /api/v1/auth, /api/v1/tokens, /api/v1/tiers, /api/v1/usage and /api/v1/admin.",
    "title": "IP lookUp API",
    "version": "1.0"
  },
  "openapi": "3.0.3",
  "paths": {
    "/api": {
      "get": {
        "description": "Identical to the current latest baseline endpoint (today GET /api/v1/ip); always mirrors the newest API version, so integrations that don't want to pin a version can call /api. Add enrichment groups with ?include=geo,network,security (or all); each is metered per unique caller IP per day and soft-withheld (limited[], status \"partial\") when over budget.",
        "parameters": [
          {
            "description": "IP to look up; defaults to the caller's own IP",
            "in": "query",
            "name": "ip",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated enrichment groups",
            "in": "query",
            "name": "include",
            "schema": {
              "enum": [
                "geo",
                "network",
                "security",
                "all"
              ],
              "type": "string"
            }
          },
          {
            "description": "Include verbose fields (authed + entitled only)",
            "in": "query",
            "name": "verbose",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiResponseV2"
                }
              }
            },
            "description": "OK"
          }
        },
        "summary": "Look up an IP (version-agnostic alias for the latest baseline endpoint)",
        "tags": [
          "ip"
        ]
      }
    },
    "/api/v1/asn/{asn}": {
      "get": {
        "description": "Returns privacy-safe RDAP metadata for an autnum. dataMayBeRedacted is always true because registrars may redact personal data; detailed RFC 9537 redacted markers remain visible. Prefixes remain available from the separate ASN prefix endpoint.",
        "parameters": [
          {
            "description": "ASN number",
            "in": "path",
            "name": "asn",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.asnLookupResponse"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Bad Request"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Forbidden"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "No RDAP delegation or record"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Network lookup limit reached"
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "RDAP client unavailable"
          }
        },
        "summary": "Look up an ASN through RDAP",
        "tags": [
          "rdap"
        ]
      }
    },
    "/api/v1/domain/{domain}": {
      "get": {
        "description": "Returns a privacy-safe RDAP projection. dataMayBeRedacted is always true because registrars may redact personal data; detailed RFC 9537 redacted markers remain visible. Only HTTPS RDAP delegation is used; no Whois port-43 fallback is attempted.",
        "parameters": [
          {
            "description": "Domain name",
            "in": "path",
            "name": "domain",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.domainLookupResponse"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Bad Request"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Forbidden"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "No RDAP delegation or record"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Network lookup limit reached"
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "RDAP client unavailable"
          }
        },
        "summary": "Look up a domain through RDAP",
        "tags": [
          "rdap"
        ]
      }
    },
    "/api/v1/ip": {
      "get": {
        "description": "Baseline classification is always returned (HTTP 200) and is never over-quota rate-limited. Add enrichment groups with ?include=geo,network,security (or all); each is metered per unique caller IP per day and soft-withheld (limited[], status \"partial\") when over budget.",
        "parameters": [
          {
            "description": "IP to look up; defaults to the caller's own IP",
            "in": "query",
            "name": "ip",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated enrichment groups",
            "in": "query",
            "name": "include",
            "schema": {
              "enum": [
                "geo",
                "network",
                "security",
                "all"
              ],
              "type": "string"
            }
          },
          {
            "description": "Include verbose fields (authed + entitled only)",
            "in": "query",
            "name": "verbose",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiResponseV2"
                }
              }
            },
            "description": "OK"
          }
        },
        "summary": "Look up an IP (baseline + any requested enrichment groups)",
        "tags": [
          "ip"
        ]
      }
    },
    "/api/v1/tiers": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.tiersResp"
                }
              }
            },
            "description": "OK"
          }
        },
        "summary": "Public tier / pricing catalog (read live from the tier schema)",
        "tags": [
          "tiers"
        ]
      }
    },
    "/api/v1/tokens": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.tokenListResp"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List the caller's API tokens (+ cap/used)",
        "tags": [
          "tokens"
        ]
      },
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/web.createTokenReq"
              }
            }
          },
          "description": "Token name",
          "required": true,
          "x-originalParamName": "body"
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.createTokenResp"
                }
              }
            },
            "description": "Created"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Token cap reached"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Token name already used by an active token"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create an API token (the plaintext secret is returned once)",
        "tags": [
          "tokens"
        ]
      }
    },
    "/api/v1/tokens/{id}": {
      "delete": {
        "parameters": [
          {
            "description": "Token ID",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Revoke an API token",
        "tags": [
          "tokens"
        ]
      },
      "patch": {
        "parameters": [
          {
            "description": "Token ID",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/web.renameTokenReq"
              }
            }
          },
          "description": "New name (1-64 chars after trimming)",
          "required": true,
          "x-originalParamName": "body"
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.tokenDTO"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Empty or over-long name"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Token belongs to another user"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Not Found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Token name already used by an active token"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Rename an API token (the secret is not touched)",
        "tags": [
          "tokens"
        ]
      }
    },
    "/api/v1/tokens/{id}/reissue": {
      "post": {
        "parameters": [
          {
            "description": "Token ID",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.createTokenResp"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Token cap reached, or token belongs to another user"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Not Found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Token name already used by an active token"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reissue an API token in place (new secret, same id/name)",
        "tags": [
          "tokens"
        ]
      }
    },
    "/api/v1/tools/asn": {
      "get": {
        "description": "Resolves AS-level and RDAP whois facts for a single `q` — a routable IP address (v4/v6) or an AS number (\"AS15169\" or a bare \"15169\"). For an IP: the live Team Cymru origin ASN/AS-name/announced-prefix (cached), the offline MariaDB org/domain match when the resolver is enabled, and the baseline RDAP subset (netname/handle/registrant country — the same subset /api/v1/ip's network.rdap carries unconditionally, never the full verbose record). For a bare AS number: only the AS holder name (no source IP means no offline org/domain match and no RDAP, which is IP-delegation-based). A disabled/unavailable dependency (ASN_ENABLED off, no RDAP client wired, MARIADB_HOST unset, or a Cymru NXDOMAIN/RDAP 404) leaves the corresponding fields absent — HTTP 200, never an error. For announced prefixes, use the separate `/api/v1/tools/asn/{asn}/prefixes` endpoint, which returns canonical supernetted IPv4/IPv6 prefixes subject to the caller's entitlement limit; that list is not embedded in this lookup response. Whois for a domain name remains outside this endpoint.",
        "parameters": [
          {
            "description": "IP address (v4/v6) or AS number (AS15169 or 15169)",
            "in": "query",
            "name": "q",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.toolsASNResp"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "invalid_request (q missing, or neither a valid IP nor an AS number)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "rate_limited"
          }
        },
        "summary": "ASN / RDAP whois lookup by IP or AS number",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/tools/asn/{asn}/prefixes": {
      "get": {
        "description": "Returns canonical, supernetted IPv4 and IPv6 prefixes. Anonymous callers receive the live anonymous entitlement window; authenticated callers use their live network tier limit, bounded by the server absolute ceiling.",
        "parameters": [
          {
            "description": "ASN number",
            "in": "path",
            "name": "asn",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "1-based page number",
            "in": "query",
            "name": "page",
            "schema": {
              "default": 1,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Entries per page",
            "in": "query",
            "name": "perPage",
            "schema": {
              "default": 100,
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.asnPrefixesResponse"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Bad Request"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Forbidden"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Too Many Requests"
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Service Unavailable"
          }
        },
        "summary": "List aggregated CIDR prefixes announced by an ASN",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/tools/bogon-check": {
      "post": {
        "description": "Classifies each submitted address as private/bogon/unassigned/global. private/bogon come from an offline static table (RFC 1918/1122/3927/5737/... and IPv6 equivalents); unassigned additionally checks the live Team Cymru fullbogons feed when enabled — a disabled/not-yet-warmed feed silently reports global rather than erroring. Carries ONLY the bogon/unassigned verdict, never geo/ASN/threat/fraud/recon/PTR enrichment. Rate-limited independently of the rest of the /api/v1/tools/* lane (its own narrower budget, not weight-proportional to batch size). The server does not log submitted addresses.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/web.bogonCheckReq"
              }
            }
          },
          "description": "Addresses to classify (not CIDR)",
          "required": true,
          "x-originalParamName": "body"
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.bogonCheckResp"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "invalid_request (empty ips) / invalid_ip (a submitted value doesn't parse) / batch_too_large (over TOOLS_BOGON_CHECK_MAX_BATCH)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "rate_limited"
          }
        },
        "summary": "Batch bogon/unassigned-space check",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/tools/dns": {
      "get": {
        "description": "Resolves DNS records for `name` under the given `type` (default A): A/AAAA/MX/TXT/NS/CNAME resolve a domain name via the standard resolver; PTR resolves reverse-DNS for an IP address (`name` must then be a valid IP, not a domain). SOA is not supported (400, explicit message) — no LookupSOA in the standard library and no DNS wire-protocol implementation in this service. `name` is syntactically validated (RFC 1035 label rules, ASCII/IDNA, ≤253 chars, no wildcard/empty/bare-root) BEFORE any resolver call. NXDOMAIN (no record) returns HTTP 200 with an empty records array — not an error; a genuine infra failure (timeout/other) returns 502/504. A response over 64 records or 16 KB aggregate, or a lookup-chain over TOOLS_DNS_MAX_LOOKUPS, is honestly truncated (`truncated:true` + `truncatedReason`), never silently cut. Cached under `toolsdns:\u003ctype\u003e:\u003cname\u003e` for a short TTL to avoid amplifying repeated queries toward third-party authoritative nameservers.",
        "parameters": [
          {
            "description": "Domain name (A/AAAA/MX/TXT/NS/CNAME) or IP address (PTR)",
            "in": "query",
            "name": "name",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Record type: A, AAAA, MX, TXT, NS, CNAME, PTR (default A)",
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.toolsDNSResp"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "invalid_request (name missing/invalid for the given type, or an unsupported type incl. SOA)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "rate_limited"
          },
          "502": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "dns_lookup_failed (infrastructure failure, not NXDOMAIN)"
          },
          "504": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "dns_lookup_failed (resolver timeout)"
          }
        },
        "summary": "DNS record / reverse-DNS (PTR) lookup",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/tools/headers": {
      "get": {
        "description": "Echoes the caller's OWN HTTP request headers back at them — reads http.Request.Header of THIS request only, no outbound network call, no cache (the response must reflect exactly this request). A fixed blacklist (Cookie, Authorization, Proxy-Authorization, X-Api-Key, plus any header name containing \"token\"/\"secret\"/\"key\" case-insensitive) is withheld from `headers` unconditionally — never present, never replaced with a redaction placeholder, however it arrived. Everything else is echoed as-is; a header sent with multiple values is comma-joined. `parsed` layers structured reads on top: Accept-Language as a (lang, q) list sorted descending by weight, Do-Not-Track presence, Sec-CH-UA client hints, the Accept-Encoding coding list, and X-Forwarded-For/Via/X-Real-IP presence — the tool page explains that these proxy headers may be added by site infrastructure (e.g. this service's own nginx front), not necessarily sent by the browser itself.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.toolsHeadersResp"
                }
              }
            },
            "description": "OK"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "rate_limited"
          }
        },
        "summary": "HTTP request header inspector",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/tools/mac": {
      "get": {
        "description": "Resolves `q` against the IEEE MAC-address-block registries (MA-L \"/24\" ex-OUI, MA-M \"/28\", MA-S \"/36\" — embedded in the binary, regenerated build-time by `make macvendor-gen`, never fetched live from IEEE at request time). Auto-detects direction: a MAC-shaped `q` (any of colon `AA:BB:CC:11:22:33`, hyphen `AA-BB-CC-11-22-33`, Cisco dot `AABB.CC11.2233`, or bare hex `AABBCC112233`, case-insensitive) resolves forward (`queryType:\"mac\"`) — the MOST SPECIFIC matching registry block wins (MA-S beats MA-M beats MA-L). Anything else resolves as a reverse, case-insensitive vendor-name substring search (`queryType:\"vendor\"`), capped at 50 rows with `truncated:true` when more existed. A MAC-shaped `q` of the wrong length or with non-hex characters is a 400, never silently misrouted to the reverse search. The U/L (locally-administered) and I/G (multicast) bits are always computed from the FIRST octet of the address itself, never from the table. **A locally-administered address (U/L bit set — e.g. a randomized Wi-Fi MAC) is by definition not an IEEE-issued OUI: the table is never consulted for it** — `vendor` is an explicit `null` and `note` explains why, distinct from a universally-administered address that's simply absent from the registry (`vendor:null`, no `note`).",
        "parameters": [
          {
            "description": "MAC address (colon/hyphen/cisco-dot/bare hex, case-insensitive) or a vendor-name substring",
            "in": "query",
            "name": "q",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.toolsMACForwardResp"
                }
              }
            },
            "description": "queryType==\\\"mac\\\" shape; queryType==\\\"vendor\\\" instead returns {query,queryType,matches[],truncated} — see toolsMACReverseResp"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "invalid_request (q missing, or MAC-shaped but wrong length / non-hex)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "rate_limited"
          }
        },
        "summary": "MAC address / OUI vendor lookup",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/tools/mail": {
      "get": {
        "description": "Fetches and parses `domain`'s email-authentication DNS records: MX (host+priority, sorted ascending), SPF (mechanisms/qualifiers, a recursively-walked `include:` chain with per-node ip4/ip6 facts, a `multipleRecords` flag when more than one v=spf1 TXT record is published, RFC 7208 §4.6.4's 10-lookup-mechanism count and `spfLookupLimitExceeded`), and DMARC (`_dmarc.\u003cdomain\u003e` TXT, parsed p/sp/pct/rua/ruf/adkim/aspf/fo tags, with a `malformed` flag distinct from no-record-at-all). DKIM is resolved ONLY when `selector` is passed — this service never guesses/enumerates selectors server-side (that stays a client-side hint list), so `dkim` is entirely absent from the response otherwise. A hard technical ceiling (TOOLS_MAIL_MAX_DNS_LOOKUPS, separate from and larger than the RFC-10 count) bounds the TOTAL outbound resolver calls one request may spend across MX+DMARC+SPF-walk+DKIM combined — hitting it degrades to a partial SPF result (`truncated:true` + an explicit \"cut by technical limit\" reason), never a silent unbounded walk toward third-party nameservers. Missing SPF/DMARC is a normal `found:false` result, HTTP 200, never an error — this endpoint reads DNS only, no SMTP/delivery probing. Cached under `toolsmail:\u003cdomain\u003e` (aggregate) and `toolsmail:dkim:\u003cdomain\u003e:\u003cselector\u003e` (DKIM, its own key) for a short TTL.",
        "parameters": [
          {
            "description": "Domain name to check",
            "in": "query",
            "name": "domain",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "DKIM selector — when omitted, the dkim field is entirely absent (no DNS probing)",
            "in": "query",
            "name": "selector",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.toolsMailResp"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "invalid_request (domain missing/invalid, or selector not a syntactically valid DNS label)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "rate_limited"
          }
        },
        "summary": "SPF/DKIM/DMARC/MX email-domain checker",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/tools/reputation": {
      "get": {
        "description": "Builds a reputation verdict for `ip` (default: the caller's own resolved IP) ENTIRELY from signals already computed for GET /api/v1/ip — privacy (VPN/proxy/Tor), threat-intel (malicious), the Dataplane.org recon signal (scanner) and the fraud/risk score — never a second data source, no forensic detail (threat sources/reports, recon sources/lastSeen, fraud signal breakdown, censorship are NEVER included, in either tier). Two DIFFERENT response shapes depending on whether `ip` (normalized) equals the caller's own resolved address: `tier: \"own\"` gets the FULL verdict (isVpnOrProxy, isTor, isHosting, isMalicious, isScanner, the numeric riskScore 0-100, and riskLevel); `tier: \"arbitrary\"` (any other address) gets ONLY riskLevel (a low/medium/high category, never the numeric score) and isMalicious — the detailed boolean breakdown and exact score for someone else's address are the paid API's product, not this free tool's. Both tiers always carry a derived `verdict` (clean/suspicious/high-risk), or `not-applicable` for a non-routable input (private/loopback/CGNAT/bogon) where privacy/fraud have no genuine signal. A disabled/unloaded threat, recon or fraud dependency degrades the corresponding field to absent/false — never a 500, never a fabricated verdict.",
        "parameters": [
          {
            "description": "IP address (v4/v6) to check; defaults to the caller's own resolved IP",
            "in": "query",
            "name": "ip",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.toolsReputationOwnResp"
                }
              }
            },
            "description": "shape when tier==\\\"own\\\"; tier==\\\"arbitrary\\\" omits riskScore/isVpnOrProxy/isTor/isHosting/isScanner entirely"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "invalid_request (ip is not a valid IP address)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.apiErrorBody"
                }
              }
            },
            "description": "rate_limited"
          }
        },
        "summary": "IP reputation/risk verdict (proxy/VPN/Tor/malicious/scanner)",
        "tags": [
          "tools"
        ]
      }
    },
    "/api/v1/topups": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.topupsResp"
                }
              }
            },
            "description": "OK"
          }
        },
        "summary": "Public top-up price grid (per feature-set × quantity bucket)",
        "tags": [
          "tiers"
        ]
      }
    },
    "/api/v1/usage": {
      "get": {
        "description": "Read-only: peeks the KeyDB quota balances without spending them.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.usageResp"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.errorBody"
                }
              }
            },
            "description": "Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "The caller's own tier, token cap/used, and per-group usage vs limit",
        "tags": [
          "usage"
        ]
      }
    },
    "/api/v1/whois": {
      "get": {
        "description": "Returns the latest non-APNIC MariaDB observation for a global IP. Corpus misses return not found; cached RDAP entries lack the provenance required to prove APNIC exclusion. Contact fields are opaque handles only; mailbox, phone, address, and other personal data are never returned. SetRDAP entitlement limits are applied only when data is delivered.",
        "parameters": [
          {
            "description": "Global IP-network address",
            "in": "query",
            "name": "ip",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.whoisResponse"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.whoisErrorResponse"
                }
              }
            },
            "description": "Invalid or non-global IP"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.whoisErrorResponse"
                }
              }
            },
            "description": "Invalid bearer credential"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.whoisErrorResponse"
                }
              }
            },
            "description": "SetRDAP is not entitled"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.whoisErrorResponse"
                }
              }
            },
            "description": "No safe observation"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.whoisErrorResponse"
                }
              }
            },
            "description": "SetRDAP limit reached"
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/web.whoisErrorResponse"
                }
              }
            },
            "description": "Internal lookup failure"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Look up a safe RIR network observation",
        "tags": [
          "whois"
        ]
      }
    }
  }
}
