{
  "openapi": "3.0.3",
  "info": {
    "title": "VPNDetection API",
    "description": "The VPNDetection API: classify any IP address, and download the databases\nbehind the answers.\n\nSee https://docs.vpndetection.io for guides and https://github.com/vpndetection-io\nfor the official client libraries.\n",
    "contact": {
      "name": "VPNDetection Support",
      "email": "support@vpndetection.io",
      "url": "https://vpndetection.io"
    },
    "termsOfService": "https://vpndetection.io/terms",
    "version": "2026.09.15"
  },
  "servers": [
    {
      "url": "https://api.vpndetection.io",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Lookup",
      "description": "Classify an IP address.",
      "x-displayName": "Lookup"
    },
    {
      "name": "Database",
      "description": "List and download the licensed databases.",
      "x-displayName": "Database"
    },
    {
      "name": "Entitlement",
      "description": "What your key may spend, and what it has.",
      "x-displayName": "Entitlement"
    },
    {
      "name": "Account",
      "description": "Who the credential belongs to, and what it may do.",
      "x-displayName": "Account"
    },
    {
      "name": "Organization",
      "description": "The organization it is scoped to.",
      "x-displayName": "Organization"
    },
    {
      "name": "API keys",
      "description": "Creating, listing and rotating credentials.",
      "x-displayName": "API keys"
    },
    {
      "name": "Authorization",
      "description": "Obtaining and revoking tokens.",
      "x-displayName": "Authorization"
    }
  ],
  "paths": {
    "/{ip}": {
      "get": {
        "operationId": "lookupIp",
        "tags": [
          "Lookup"
        ],
        "summary": "Lookup",
        "description": "Answers what is known about a single IPv4 or IPv6 address. Which fields\ncome back is decided by the plan behind the presented key; with no key\nthe answer is `ip` and `is_vpn`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "ip",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The IPv4 or IPv6 address to classify.",
            "examples": {
              "ipv4": {
                "value": "1.1.1.1"
              },
              "ipv6": {
                "value": "2606:4700:4700::1111"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The classification. Present fields are those your plan includes; see\nthe tier table in the API description.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupResponse"
                },
                "examples": {
                  "free": {
                    "summary": "No API key, or a key on the free plan",
                    "value": {
                      "ip": "1.1.1.1",
                      "is_vpn": false
                    }
                  },
                  "starter": {
                    "summary": "A key on the starter plan",
                    "value": {
                      "ip": "1.1.1.1",
                      "is_vpn": false,
                      "is_hosting": true,
                      "is_relay": false,
                      "is_tor": false,
                      "is_cdn": false,
                      "vpn": {}
                    }
                  },
                  "scale": {
                    "summary": "A key on the scale plan, which adds the classification detail objects and the proxy FLAGS - the proxy detail stays on max",
                    "value": {
                      "ip": "1.1.1.1",
                      "is_vpn": false,
                      "is_hosting": true,
                      "is_relay": false,
                      "is_tor": false,
                      "is_cdn": false,
                      "is_resproxy": false,
                      "is_dcproxy": false,
                      "is_mobproxy": false,
                      "vpn": {},
                      "hosting": {
                        "provider": "cloudflare",
                        "confidence": "high",
                        "last_seen": "2026-09-02"
                      },
                      "relay": {},
                      "tor": {},
                      "cdn": {}
                    }
                  },
                  "max": {
                    "summary": "A key on the max plan, address found in the VPN dataset",
                    "value": {
                      "ip": "146.70.22.220",
                      "is_vpn": true,
                      "is_hosting": true,
                      "is_relay": false,
                      "is_tor": false,
                      "is_cdn": false,
                      "is_resproxy": false,
                      "is_dcproxy": false,
                      "is_mobproxy": false,
                      "vpn": {
                        "provider": "mullvad",
                        "last_seen": "2026-09-02",
                        "confidence": "high",
                        "method": "scan"
                      },
                      "hosting": {
                        "provider": "m247",
                        "confidence": "high",
                        "last_seen": "2026-09-02"
                      },
                      "relay": {},
                      "tor": {},
                      "cdn": {},
                      "resproxy": {},
                      "dcproxy": {},
                      "mobproxy": {}
                    }
                  },
                  "bogon": {
                    "summary": "A private address on the max plan, answered without a lookup",
                    "value": {
                      "ip": "192.168.1.1",
                      "is_vpn": false,
                      "is_hosting": false,
                      "is_relay": false,
                      "is_tor": false,
                      "is_cdn": false,
                      "is_resproxy": false,
                      "is_dcproxy": false,
                      "is_mobproxy": false,
                      "vpn": {},
                      "hosting": {},
                      "relay": {},
                      "tor": {},
                      "cdn": {},
                      "resproxy": {},
                      "dcproxy": {},
                      "mobproxy": {}
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The path segment is not a valid IP address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "not a valid IP address"
                }
              }
            }
          },
          "401": {
            "description": "The presented API key is unknown, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "error": "invalid API key"
                    }
                  },
                  "revoked": {
                    "value": {
                      "error": "API key revoked"
                    }
                  },
                  "expired": {
                    "value": {
                      "error": "API key expired"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key restricts which source addresses may use it, and this\nrequest did not come from one of them.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "source address not allowed for this API key"
                }
              }
            }
          },
          "429": {
            "description": "Either a transient rate limit or a spent allowance. `Retry-After` is\npresent for the former and absent for the latter, and is the only\nway to tell them apart.\n",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Present ONLY when the rejection\nis a transient rate limit; its absence means an allowance is\nspent and retrying will not help.\n",
                "required": false,
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "examples": {
                  "rateLimited": {
                    "summary": "Transient, sent with Retry-After",
                    "value": {
                      "error": "rate limit exceeded"
                    }
                  },
                  "keylessDailyLimit": {
                    "summary": "Keyless daily cap spent, no Retry-After",
                    "value": {
                      "error": "daily limit for unauthenticated requests exceeded; use an API key"
                    }
                  },
                  "quotaExceeded": {
                    "summary": "Plan allowance spent, no Retry-After",
                    "value": {
                      "error": "request allowance exceeded; raise or remove your overage limit"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "The VPN dataset could not be consulted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "lookup failed"
                }
              }
            }
          }
        },
        "security": [
          {},
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/myip": {
      "get": {
        "operationId": "lookupMyIp",
        "tags": [
          "Lookup"
        ],
        "summary": "My IP",
        "description": "Answers what is known about the address this request came from, which is\nthe same answer `GET /{ip}` gives for that address: the plan behind the\npresented key decides which fields come back, and the request counts\nagainst the same allowance.\n\nThe address is the one our edge observed, so a request through a proxy\nor a VPN reports the exit it left through rather than the machine that\nmade it. That is usually the point of asking.\n",
        "responses": {
          "200": {
            "description": "The classification of your own address. Present fields are those\nyour plan includes; see the tier table in the API description.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupResponse"
                },
                "examples": {
                  "free": {
                    "summary": "No API key, or a key on the free plan",
                    "value": {
                      "ip": "45.83.91.1",
                      "is_vpn": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The presented API key is unknown, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "error": "invalid API key"
                    }
                  },
                  "revoked": {
                    "value": {
                      "error": "API key revoked"
                    }
                  },
                  "expired": {
                    "value": {
                      "error": "API key expired"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key restricts which source addresses may use it, and this\nrequest did not come from one of them.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "source address not allowed for this API key"
                }
              }
            }
          },
          "429": {
            "description": "Either the request rate was too high, or an allowance is spent.\nThe two are told apart by the message, not the status.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "examples": {
                  "rateLimited": {
                    "value": {
                      "error": "rate limit exceeded"
                    }
                  },
                  "keylessDaily": {
                    "value": {
                      "error": "daily limit for unauthenticated requests exceeded; use an API key"
                    }
                  },
                  "quotaExceeded": {
                    "value": {
                      "error": "request allowance exceeded; raise or remove your overage limit"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "The VPN dataset could not be consulted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "lookup failed"
                }
              }
            }
          }
        },
        "security": [
          {},
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/batch": {
      "post": {
        "operationId": "lookupBatch",
        "tags": [
          "Lookup"
        ],
        "summary": "Batch",
        "description": "Answers up to 1000 addresses in one call. Each distinct string in `ips`\nis one lookup: it costs exactly what `GET /{ip}` costs for that address\nand comes back with exactly the fields that call would carry for your\nplan. Exact duplicates collapse to one entry and one lookup.\n\nBoth maps in the answer are keyed by the string you sent, so nothing has\nto be lined up by position; the `ip` inside each result is the\nnormalized form. An address that could not be answered sits in `errors`\nwith the status and message the single lookup would have given, and\nnever disturbs the others: a string that is not an address is a `400`\nthere, and an allowance that runs out part way through leaves the\nremaining entries as `429`s.\n\nThe call itself fails only for the reasons below, and a `429` on the\ncall always carries `Retry-After`: the batch is admitted or refused\nwhole by the rate limit, so a per-entry `429` is always a spent\nallowance and never a throttle.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchLookupRequest"
              },
              "example": {
                "ips": [
                  "1.1.1.1",
                  "2606:4700:4700:0:0:0:0:1111",
                  "192.168.1.1",
                  "not-an-ip"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per distinct input string, in `results` or in `errors`.\nPresent fields within a result are those your plan includes; see the\ntier table in the API description.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchLookupResponse"
                },
                "examples": {
                  "free": {
                    "summary": "No API key, or a key on the free plan",
                    "value": {
                      "results": {
                        "1.1.1.1": {
                          "ip": "1.1.1.1",
                          "is_vpn": false
                        },
                        "2606:4700:4700:0:0:0:0:1111": {
                          "ip": "2606:4700:4700::1111",
                          "is_vpn": false
                        },
                        "192.168.1.1": {
                          "ip": "192.168.1.1",
                          "is_vpn": false
                        }
                      },
                      "errors": {
                        "not-an-ip": {
                          "status": 400,
                          "error": "not a valid IP address"
                        }
                      }
                    }
                  },
                  "keylessDailyLimit": {
                    "summary": "No API key, and the daily allowance ran out after the first entry",
                    "value": {
                      "results": {
                        "1.1.1.1": {
                          "ip": "1.1.1.1",
                          "is_vpn": false
                        }
                      },
                      "errors": {
                        "8.8.8.8": {
                          "status": 429,
                          "error": "daily limit for unauthenticated requests exceeded; use an API key"
                        }
                      }
                    }
                  },
                  "quotaExceeded": {
                    "summary": "A key on the starter plan whose allowance ran out after the first entry",
                    "value": {
                      "results": {
                        "1.1.1.1": {
                          "ip": "1.1.1.1",
                          "is_vpn": false,
                          "is_hosting": true,
                          "is_relay": false,
                          "is_tor": false,
                          "is_cdn": false,
                          "vpn": {}
                        }
                      },
                      "errors": {
                        "8.8.8.8": {
                          "status": 429,
                          "error": "request allowance exceeded; raise or remove your overage limit"
                        }
                      }
                    }
                  },
                  "lookupFailed": {
                    "summary": "The VPN dataset could not be consulted, which fails every entry",
                    "value": {
                      "results": {},
                      "errors": {
                        "1.1.1.1": {
                          "status": 500,
                          "error": "lookup failed"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not a batch, or names too few or too many addresses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "examples": {
                  "malformed": {
                    "value": {
                      "error": "body must be a JSON object whose ips member is an array of strings"
                    }
                  },
                  "size": {
                    "value": {
                      "error": "ips must hold between 1 and 1000 addresses"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The presented API key is unknown, revoked, or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "error": "invalid API key"
                    }
                  },
                  "revoked": {
                    "value": {
                      "error": "API key revoked"
                    }
                  },
                  "expired": {
                    "value": {
                      "error": "API key expired"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key restricts which source addresses may use it, and this\nrequest did not come from one of them.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "source address not allowed for this API key"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than any batch of 1000 addresses can be.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "request body too large"
                }
              }
            }
          },
          "429": {
            "description": "The rate limit refused the whole call. Always transient and always\nwith `Retry-After`; a spent allowance is reported per entry in a\n`200` instead.\n",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying the call.",
                "required": true,
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupError"
                },
                "example": {
                  "error": "rate limit exceeded"
                }
              }
            }
          }
        },
        "security": [
          {},
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/database/list": {
      "get": {
        "operationId": "listDatabases",
        "tags": [
          "Database"
        ],
        "summary": "List",
        "description": "Every database this organization may SEE, with where its licence stands.\nNot just the ones you hold: a customer with one grant should be able to\ntell what else is published without asking. `standing` is the\ndifference - `licensed`, `expired`, or `unlicensed` for one never\nbought.\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "databases": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Database"
                      }
                    }
                  },
                  "required": [
                    "databases"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/database/download": {
      "get": {
        "operationId": "downloadDatabase",
        "tags": [
          "Database"
        ],
        "summary": "Download",
        "description": "Answers `302` with a time-limited URL pointing straight at object storage. Follow the redirect; the link authorizes the START of a transfer, so one already running is not interrupted when it lapses.\n",
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Dataset id, e.g. vpn_ip_extended_v1"
          },
          {
            "in": "query",
            "name": "format",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/DatabaseFormat"
            },
            "description": "Not every database is built in every format. The `_provider`\ncatalogues are keyed by provider id rather than by IP range, so no\nMMDB exists for them.\n"
          }
        ],
        "responses": {
          "302": {
            "description": "Follow the Location header to object storage",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The database is not built in the requested format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your organization holds no license for this database, or its term has ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such database",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Licensed, but today's copy has not been published yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/database/metadata": {
      "get": {
        "operationId": "databaseMetadata",
        "tags": [
          "Database"
        ],
        "summary": "Metadata",
        "description": "Poll this to decide whether today's build is worth fetching: it carries\n`updated` and `entries` without downloading anything.\n\nNo `format` parameter - one document describes every format the database\nis built in.\n",
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatabaseMetadata"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotLicensed"
          },
          "404": {
            "$ref": "#/components/responses/UnknownDataset"
          },
          "503": {
            "$ref": "#/components/responses/NotAvailable"
          }
        },
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/database/checksum": {
      "get": {
        "operationId": "databaseChecksum",
        "tags": [
          "Database"
        ],
        "summary": "Checksums",
        "description": "Checksums for one published file, so a download can be verified after it lands.\n",
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "format",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/DatabaseFormat"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "format": {
                      "type": "string"
                    },
                    "checksums": {
                      "$ref": "#/components/schemas/DbChecksums"
                    }
                  },
                  "required": [
                    "id",
                    "format",
                    "checksums"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The database is not built in the requested format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotLicensed"
          },
          "404": {
            "$ref": "#/components/responses/UnknownDataset"
          },
          "503": {
            "$ref": "#/components/responses/NotAvailable"
          }
        },
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/database/downloads": {
      "get": {
        "operationId": "listDownloads",
        "tags": [
          "Database"
        ],
        "summary": "History",
        "description": "Recent download attempts for this organization, newest first. Refusals are listed too, so a failed transfer can be accounted for.\n",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "downloads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Download"
                      }
                    }
                  },
                  "required": [
                    "downloads"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/entitlement": {
      "get": {
        "operationId": "myEntitlement",
        "tags": [
          "Entitlement"
        ],
        "summary": "Plan and usage",
        "description": "Answers what the presented key is, what plan is behind it, and what has\nbeen spent against that plan's allowance in the current window.\n",
        "responses": {
          "200": {
            "description": "The key's entitlements and consumption.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entitlement"
                },
                "examples": {
                  "paid": {
                    "summary": "A key on a paid plan, uncapped",
                    "value": {
                      "org_id": "85bb51e4-2eb6-4a31-8e4d-02ba8b98fe61",
                      "apikey": {
                        "id": "0ab424cc-7619-4dad-b027-afacdc2cedb0",
                        "expires": null,
                        "allowed_cidrs": []
                      },
                      "plan": {
                        "key": "max",
                        "tier": "max"
                      },
                      "usage": {
                        "requests": 580,
                        "quota": 5000000,
                        "hard_limit": null,
                        "window_start": "2026-09-04T07:00:00Z",
                        "window_end": "2026-10-04T07:00:00Z"
                      }
                    }
                  },
                  "free": {
                    "summary": "A key on the free plan. There is no subscription, so the window is the calendar month, and the allowance is a hard stop rather than a billable overage.",
                    "value": {
                      "org_id": "f32191d0-ef02-450e-a505-eb5814c35cab",
                      "apikey": {
                        "id": "10c2b437-3aa2-4a63-bd17-8e7c8c7f0def",
                        "expires": null,
                        "allowed_cidrs": []
                      },
                      "plan": {
                        "key": "free",
                        "tier": "free"
                      },
                      "usage": {
                        "requests": 545,
                        "quota": 50000,
                        "hard_limit": 50000,
                        "window_start": "2026-09-01T00:00:00Z",
                        "window_end": "2026-10-01T00:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No key was presented, or it is unknown, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementError"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "error": "an API key is required"
                    }
                  },
                  "invalid": {
                    "value": {
                      "error": "invalid API key"
                    }
                  },
                  "revoked": {
                    "value": {
                      "error": "API key revoked"
                    }
                  },
                  "expired": {
                    "value": {
                      "error": "API key expired"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key restricts which source addresses may use it, and this\nrequest did not come from one of them.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementError"
                },
                "example": {
                  "error": "source address not allowed for this API key"
                }
              }
            }
          },
          "503": {
            "description": "The usage counters could not be read. Reported rather than guessed\nat: the whole answer is the number, and a confident zero would read\nas \"you have used nothing\".\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntitlementError"
                },
                "example": {
                  "error": "usage is temporarily unavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/api/v1/iam/identity": {
      "get": {
        "operationId": "accountIdentity",
        "tags": [
          "Account"
        ],
        "summary": "Identity",
        "description": "The identity behind this credential: what it is, the organization it is\nscoped to, and the scopes it currently holds.\n\n`user` is present for an OAuth token and ABSENT for an API key, which\nhas an organization but no person behind it. `credential.kind` says\nwhich you are holding, so a client can branch without guessing from a\nmissing field.\n\nNarrower than what the console shows its own user on purpose: an\nintegration needs a name to display and an organization to address, not\na profile. The `scopes` array is what the credential may do RIGHT NOW,\nso a client can render its own capabilities rather than discovering\nthem from a 403.\n",
        "security": [
          {
            "bearerAuth": [
              "account.read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Identity"
                },
                "examples": {
                  "ok": {
                    "summary": "A token granted read access",
                    "value": {
                      "rc": "SUCCESS",
                      "user": {
                        "id": "3d86d1ef-f70b-4063-a3b8-f69605a8fc57",
                        "fullname": "Ada Lovelace",
                        "email": "ada@example.com"
                      },
                      "org": {
                        "id": "7b309476-0e70-4225-9a46-5e25650094dd"
                      },
                      "scopes": [
                        "account.read",
                        "apikeys.read"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          }
        }
      }
    },
    "/api/v1/iam/org": {
      "get": {
        "operationId": "accountOrg",
        "tags": [
          "Organization"
        ],
        "summary": "Organization",
        "description": "The organization this credential is scoped to.\n\nThere is no way to name a different one. A credential describes exactly\none organization, so an identifier in the path could only ever be your\nown or a refusal.\n",
        "security": [
          {
            "bearerAuth": [
              "org.read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountOrgWrap"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          }
        }
      }
    },
    "/api/v1/iam/org/members": {
      "get": {
        "operationId": "accountOrgMembers",
        "tags": [
          "Organization"
        ],
        "summary": "Members",
        "description": "Read-only. Adding or removing a member is an invitation flow with email\nin the middle rather than a POST, and modelling it as one here would\npromise something this API does not do.\n",
        "security": [
          {
            "bearerAuth": [
              "org.read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The members.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountRc"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          }
        }
      }
    },
    "/api/v1/iam/apikeys": {
      "get": {
        "operationId": "accountListApikeys",
        "tags": [
          "API keys"
        ],
        "summary": "List",
        "description": "Metadata only. A key's secret is never in a list - not here and not in\nthe console - because a list is the response that ends up in logs,\ncaches and support tickets.\n\n`retrievable` says whether the secret could still be read back at all.\nA key issued before this product stored secrets recoverably was never\nkept, so `reveal` will refuse it permanently; rotating produces one\nthat can be read.\n",
        "security": [
          {
            "bearerAuth": [
              "apikeys.read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The organization's keys, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApikeyList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          }
        }
      },
      "post": {
        "operationId": "accountCreateApikey",
        "tags": [
          "API keys"
        ],
        "summary": "Create",
        "description": "Creates a key and returns its secret.\n\nThis is the ONLY response that ever carries the secret, and only\nbecause this is the moment it comes into existence. Store it now.\n",
        "security": [
          {
            "bearerAuth": [
              "apikeys.manage"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountCreateApikeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new key, including its secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountCreatedApikey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AccountInvalid"
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          }
        }
      }
    },
    "/api/v1/iam/apikeys/{id}/rotate": {
      "post": {
        "operationId": "accountRotateApikey",
        "tags": [
          "API keys"
        ],
        "summary": "Rotate",
        "description": "Replaces the secret behind a key, keeping its id, name and settings.\nThe previous secret stops working immediately.\n",
        "security": [
          {
            "bearerAuth": [
              "apikeys.manage"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ApikeyIdParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The key, with its new secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountCreatedApikey"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          },
          "404": {
            "$ref": "#/components/responses/AccountNotFound"
          }
        }
      }
    },
    "/api/v1/iam/apikeys/{id}/revoke": {
      "post": {
        "operationId": "accountRevokeApikey",
        "tags": [
          "API keys"
        ],
        "summary": "Revoke",
        "description": "Stops the key working. Revocation is soft: the key stays listed with a\n`revoked_at`, because the organization still owns whatever it did while\nit was alive.\n",
        "security": [
          {
            "bearerAuth": [
              "apikeys.manage"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ApikeyIdParam"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountRc"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          },
          "404": {
            "$ref": "#/components/responses/AccountNotFound"
          }
        }
      }
    },
    "/api/v1/iam/apikeys/{id}/reveal": {
      "post": {
        "operationId": "accountRevealApikey",
        "tags": [
          "API keys"
        ],
        "summary": "Reveal",
        "description": "Returns an existing key's secret.\n\nIts own scope rather than part of `apikeys.manage`, because the two are\ndifferent acts: rotating replaces a secret you never see, while this\nhands one over.\n\nRefused with `NOT_RETRIEVABLE` for a key issued before this product\nstored secrets recoverably - that secret was never kept, so no retry\nwill ever produce it. Rotate the key instead.\n",
        "security": [
          {
            "bearerAuth": [
              "apikeys.reveal"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ApikeyIdParam"
          }
        ],
        "responses": {
          "200": {
            "description": "The key's secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountRevealedApikey"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/AccountUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountForbidden"
          },
          "404": {
            "description": "No such key in this organization, or its secret was never stored\nrecoverably (`NOT_RETRIEVABLE`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountRc"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "get": {
        "operationId": "oauthMetadata",
        "tags": [
          "Authorization"
        ],
        "summary": "Discovery",
        "description": "RFC 8414 authorization server metadata: the endpoints, the grant types\nand the scopes this server supports.\n\nRead this rather than hardcoding endpoints. It is also served at\n`/.well-known/openid-configuration`, identically, because several\nclients probe that path first.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "The server's metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthMetadata"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/device_authorization": {
      "post": {
        "operationId": "oauthDeviceAuthorization",
        "tags": [
          "Authorization"
        ],
        "summary": "Device authorization",
        "description": "Starts the device flow. Show the `user_code` to the person and send them\nto `verification_uri`; `verification_uri_complete` has the code already\nembedded, which is what to open if you can open a browser at all.\n\nThen poll `/oauth/token`, no faster than `interval` seconds.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/DeviceAuthorizationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A pending device authorization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthorization"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`, `invalid_client`, `unauthorized_client`, or\n`slow_down` when this address has started too many authorizations.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/authorize": {
      "get": {
        "operationId": "oauthAuthorize",
        "tags": [
          "Authorization"
        ],
        "summary": "Authorize",
        "description": "The browser entry point for the authorization-code flow. This is a\nredirect target, not something to call from code.\n\nAn unknown `client_id` or an unregistered `redirect_uri` is shown to the\nUSER and never redirected, because sending an error to an address we\nhave not verified belongs to you is how an open redirector works.\nEverything else comes back to your `redirect_uri` with `error`, your\n`state`, and `iss`.\n",
        "security": [],
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "code"
              ]
            }
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "S256"
              ]
            },
            "description": "S256 only. `plain` is refused rather than downgraded."
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Returned unchanged. Use it to bind the response to your request."
          },
          {
            "name": "resource",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "RFC 8707. What the token is FOR, so it cannot be replayed elsewhere."
          }
        ],
        "responses": {
          "302": {
            "description": "A redirect to the consent screen, or back to your redirect_uri with an error."
          },
          "400": {
            "description": "Shown to the user. An unverified redirect target is never sent an error."
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "operationId": "oauthToken",
        "tags": [
          "Authorization"
        ],
        "summary": "Token",
        "description": "Three grant types.\n\n`urn:ietf:params:oauth:grant-type:device_code` polls a device\nauthorization. Until the person approves it answers\n`authorization_pending`; poll faster than `interval` and it answers\n`slow_down`, which means widen your interval and keep it widened.\n\n`authorization_code` exchanges a code from `/oauth/authorize`, with the\n`code_verifier` matching the challenge you sent.\n\n`refresh_token` exchanges a refresh token. The presented token is\nconsumed whatever happens next, so store the new one before using it.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "An RFC 6749 error. `authorization_pending` and `slow_down` are\nnormal answers while polling a device authorization, not failures.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OauthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/revoke": {
      "post": {
        "operationId": "oauthRevoke",
        "tags": [
          "Authorization"
        ],
        "summary": "Revoke",
        "description": "RFC 7009. Always answers 200, including for a token that was never\nvalid - an endpoint that distinguished the two would be a way to test\nwhether a stolen string is a live credential.\n\nRevoking a REFRESH token ends the whole authorization and takes its\naccess tokens with it. Revoking an access token affects only that token.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/RevokeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Always, whatever was presented.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer <key>`."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "`X-Api-Key: <key>`."
      },
      "apiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "apikey",
        "description": "`?apikey=<key>`. Convenient in a browser; prefer a header elsewhere."
      }
    },
    "schemas": {
      "LookupResponse": {
        "type": "object",
        "description": "The union of every plan's answer. Only `ip` and `is_vpn` are guaranteed;\neach remaining field is present when your plan includes it, and absent\notherwise. A client that must work across plans should treat an absent\nflag as unknown rather than as `false`.\n",
        "required": [
          "ip",
          "is_vpn"
        ],
        "properties": {
          "ip": {
            "type": "string",
            "description": "The address that was looked up, normalized.",
            "example": "1.1.1.1"
          },
          "is_vpn": {
            "type": "boolean",
            "description": "Whether the address is VPN infrastructure. Keys off presence in the\nVPN dataset, so an unattributed range with no provider is still\n`true`.\n"
          },
          "is_hosting": {
            "type": "boolean",
            "description": "Whether the address belongs to a hosting or cloud provider. Starter and above."
          },
          "is_relay": {
            "type": "boolean",
            "description": "Whether the address is a privacy relay egress. Starter and above."
          },
          "is_tor": {
            "type": "boolean",
            "description": "Whether the address is a Tor node. Starter and above."
          },
          "is_cdn": {
            "type": "boolean",
            "description": "Whether the address belongs to a CDN. Starter and above."
          },
          "is_resproxy": {
            "type": "boolean",
            "description": "Whether the address was seen in a residential proxy pool. Scale and above."
          },
          "is_dcproxy": {
            "type": "boolean",
            "description": "Whether the address was seen in a datacenter proxy pool. Scale and above."
          },
          "is_mobproxy": {
            "type": "boolean",
            "description": "Whether the address was seen in a mobile proxy pool. Scale and above."
          },
          "vpn": {
            "allOf": [
              {
                "$ref": "#/components/schemas/VpnDetail"
              }
            ],
            "description": "Detail for `is_vpn`. Empty when `is_vpn` is false. Starter and above.\n"
          },
          "hosting": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClassDetail"
              }
            ],
            "description": "Detail for `is_hosting`. Empty when false. Scale and above."
          },
          "relay": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClassDetail"
              }
            ],
            "description": "Detail for `is_relay`. Empty when false. Scale and above."
          },
          "tor": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClassDetail"
              }
            ],
            "description": "Detail for `is_tor`. Empty when false. Scale and above. The tor\ndataset carries no provider, so `provider` is always an empty string.\n"
          },
          "cdn": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClassDetail"
              }
            ],
            "description": "Detail for `is_cdn`. Empty when false. Scale and above."
          },
          "resproxy": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ProxyDetail"
              }
            ],
            "description": "Detail for `is_resproxy`. Empty when false. Max only."
          },
          "dcproxy": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ProxyDetail"
              }
            ],
            "description": "Detail for `is_dcproxy`. Empty when false. Max only."
          },
          "mobproxy": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ProxyDetail"
              }
            ],
            "description": "Detail for `is_mobproxy`. Empty when false. Max only."
          }
        }
      },
      "BatchLookupRequest": {
        "type": "object",
        "required": [
          "ips"
        ],
        "properties": {
          "ips": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1000,
            "items": {
              "type": "string"
            },
            "description": "The addresses to classify, 1 to 1000 per call, counted before\nduplicates collapse. Each distinct string is one lookup.\n",
            "example": [
              "1.1.1.1",
              "2606:4700:4700::1111"
            ]
          }
        }
      },
      "BatchLookupResponse": {
        "type": "object",
        "required": [
          "results",
          "errors"
        ],
        "properties": {
          "results": {
            "type": "object",
            "description": "One answer per input string that was classified, keyed by the string\nas you sent it; the `ip` inside is the normalized form. Each value is\nexactly what `GET /{ip}` answers for your plan.\n",
            "additionalProperties": {
              "$ref": "#/components/schemas/LookupResponse"
            }
          },
          "errors": {
            "type": "object",
            "description": "One entry per input string that could not be classified, keyed the\nsame way. Empty when every entry was answered.\n",
            "additionalProperties": {
              "$ref": "#/components/schemas/BatchLookupError"
            }
          }
        }
      },
      "BatchLookupError": {
        "type": "object",
        "description": "Why one entry of a batch was not answered, as the single lookup would have reported it.",
        "required": [
          "status",
          "error"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "description": "The HTTP status `GET /{ip}` would have answered for this entry: `400`\nfor a string that is not an address, `429` for a spent allowance,\n`500` when the VPN dataset could not be consulted. A `429` here is\nnever a throttle; the whole call is refused instead.\n",
            "example": 400
          },
          "error": {
            "type": "string",
            "description": "The same message the single lookup carries for that status.",
            "example": "not a valid IP address"
          }
        }
      },
      "VpnDetail": {
        "type": "object",
        "description": "What is known about the VPN attribution. Every key is present when the\nobject is populated, empty values included; the object is `{}` when\n`is_vpn` is false. `confidence` and `method` are max only, so on a lower\nplan they are absent from a populated object rather than empty.\n",
        "properties": {
          "provider": {
            "type": "string",
            "description": "The VPN provider, or an empty string for an unattributed range.",
            "example": "mullvad"
          },
          "last_seen": {
            "type": "string",
            "format": "date",
            "description": "The most recent date this address was observed as VPN infrastructure.",
            "example": "2026-09-02"
          },
          "confidence": {
            "type": "string",
            "description": "How strongly the attribution is supported. Max only.",
            "example": "high"
          },
          "method": {
            "type": "string",
            "description": "The class of evidence the attribution rests on, one of four values.\nMax only.\n\n`scan` - we spoke the VPN protocol to the address ourselves and got a\nvalid server response. `scrape` - the operator published the address\nthrough its own API, client or configuration. `registry` - public\nregistration or naming records attribute it to the operator.\n`infer` - the address was extrapolated from confirmed neighbours in\nthe same block.\n",
            "example": "scan"
          }
        }
      },
      "ClassDetail": {
        "type": "object",
        "description": "The shared detail shape for the hosting, relay, tor and cdn datasets.\nEvery key is present when the object is populated; the object is `{}`\nwhen its flag is false.\n",
        "properties": {
          "provider": {
            "type": "string",
            "description": "The provider, or an empty string where the dataset has none.",
            "example": "m247"
          },
          "confidence": {
            "type": "string",
            "description": "How strongly the classification is supported.",
            "example": "high"
          },
          "last_seen": {
            "type": "string",
            "format": "date",
            "description": "The most recent date this address was observed in this dataset.",
            "example": "2026-09-02"
          }
        }
      },
      "ProxyDetail": {
        "type": "object",
        "description": "The shared detail shape for the residential, datacenter and mobile proxy\nfamilies, measured over a rolling 90 day window. Every key is present\nwhen the object is populated; the object is `{}` when its flag is false.\n",
        "properties": {
          "provider": {
            "type": "string",
            "description": "The proxy network, or an empty string where unattributed.",
            "example": "brightdata"
          },
          "first_seen": {
            "type": "string",
            "format": "date",
            "description": "The earliest date within the window this address was seen in the pool.",
            "example": "2026-06-14"
          },
          "last_seen": {
            "type": "string",
            "format": "date",
            "description": "The most recent date within the window this address was seen in the pool.",
            "example": "2026-09-02"
          },
          "hits": {
            "type": "integer",
            "description": "How many times the address was observed in the pool during the window.",
            "example": 42
          },
          "hits_days_pct": {
            "type": "integer",
            "description": "The share of days in the window on which the address was seen, as a\npercentage. A high value means a stable pool member rather than a\none-off sighting.\n",
            "example": 63
          },
          "providers_num": {
            "type": "integer",
            "description": "How many distinct proxy networks this address was seen in.",
            "example": 2
          }
        }
      },
      "LookupError": {
        "type": "object",
        "description": "Every non-2xx response carries this shape.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A human-readable explanation of the failure.",
            "example": "not a valid IP address"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "rc": {
            "type": "string"
          }
        },
        "required": [
          "rc"
        ]
      },
      "Standing": {
        "type": "string",
        "description": "Where your license for a database family stands today. `licensed` is a\nlive grant, `expired` one whose term has ended, and `unlicensed` a\ndatabase published but never bought.\n",
        "enum": [
          "expired",
          "licensed",
          "unlicensed"
        ]
      },
      "DatabaseFormat": {
        "type": "string",
        "description": "A file format a database version is published in.",
        "enum": [
          "csvgz",
          "mmdb"
        ]
      },
      "DatabaseFormatSize": {
        "type": "object",
        "properties": {
          "format": {
            "$ref": "#/components/schemas/DatabaseFormat"
          },
          "bytes": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "Size of the published file, or null when it has not been published\nyet. int64 because it is not hypothetical: resproxy_ip_14d's MMDB is\n4.58 GB, so a 32-bit field cannot carry the catalogue and `list`\nthrows for every caller rather than for that one entry.\n"
          }
        },
        "required": [
          "format",
          "bytes"
        ]
      },
      "Database": {
        "type": "object",
        "description": "One database FAMILY your organization is licensed for. A license covers\nthe family, while a download names a specific version, so the ids you\npass to the download and checksum endpoints come from `versions`.\n",
        "properties": {
          "base": {
            "type": "string",
            "description": "The database family, e.g. `vpn_ip`. What the license is held against.",
            "example": "vpn_ip"
          },
          "name": {
            "type": "string",
            "example": "VPN IP"
          },
          "summary": {
            "type": "string"
          },
          "license_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "evaluation",
              "standard",
              "redistribute",
              null
            ],
            "description": "What a license permits you to do with the data. Null for a family\nyou hold no license for, which is every one with standing\n`unlicensed`.\n"
          },
          "starts": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "A hard stop. Null when the license has no end date, which is the normal case for a rolling agreement, and when there is no license. A rolling license reports its turnover date in renews_at instead."
          },
          "renews_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a rolling license next renews. Null when the license has no defined term, when expires sets a hard stop instead, and when there is no license."
          },
          "notice_due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The last day notice of non-renewal can be given for the term ending at renews_at. Null whenever renews_at is, and when the agreement records no notice period."
          },
          "in_term": {
            "type": "boolean",
            "description": "False when the license has lapsed; downloads are refused."
          },
          "standing": {
            "$ref": "#/components/schemas/Standing"
          },
          "versions": {
            "type": "array",
            "description": "Every published version of this family. The `id` here is what the\ndownload and checksum endpoints take.\n",
            "items": {
              "$ref": "#/components/schemas/DatabaseVersion"
            }
          }
        },
        "required": [
          "base",
          "name",
          "summary",
          "license_type",
          "starts",
          "expires",
          "renews_at",
          "notice_due_at",
          "in_term",
          "standing",
          "versions"
        ]
      },
      "DatabaseVersion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The versioned database id, e.g. `vpn_ip_v1`. Pass this to download.",
            "example": "vpn_ip_v1"
          },
          "version": {
            "type": "integer",
            "example": 1
          },
          "summary": {
            "type": "string"
          },
          "formats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DatabaseFormatSize"
            }
          },
          "sample_formats": {
            "type": "array",
            "description": "The formats an evaluation sample is published in, if any.",
            "items": {
              "$ref": "#/components/schemas/DatabaseFormat"
            }
          }
        },
        "required": [
          "id",
          "version",
          "formats"
        ]
      },
      "Download": {
        "type": "object",
        "description": "One download ATTEMPT, refusals included - a denial is what answers \"it\nstopped working\", so they are listed rather than dropped.\n",
        "properties": {
          "dataset_id": {
            "type": "string"
          },
          "format": {
            "type": "string"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "ok",
              "unauthorized",
              "denied",
              "expired",
              "unknown",
              "unavailable"
            ]
          },
          "sample": {
            "type": "boolean",
            "description": "The evaluation sample rather than the database itself."
          },
          "bytes": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "Object size at redirect time, NOT bytes delivered: the transfer is a\npresigned redirect straight to object storage, so we never observe it.\nint64 for the same reason as DatabaseFormatSize.bytes - it is the\nsize of the same object.\n"
          },
          "http_status": {
            "type": "integer",
            "nullable": true
          },
          "apikey_id": {
            "type": "string",
            "nullable": true,
            "description": "The key that made the request. Null when the org acted through the\nconsole rather than through a key.\n"
          },
          "client_ip": {
            "type": "string",
            "nullable": true
          },
          "user_agent": {
            "type": "string",
            "nullable": true
          },
          "created": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "dataset_id",
          "format",
          "outcome",
          "sample",
          "bytes",
          "http_status",
          "apikey_id",
          "client_ip",
          "user_agent",
          "created"
        ]
      },
      "DbChecksums": {
        "type": "object",
        "description": "The published digests for one database file.",
        "properties": {
          "md5": {
            "type": "string"
          },
          "sha1": {
            "type": "string"
          },
          "sha256": {
            "type": "string"
          },
          "sha512": {
            "type": "string"
          }
        },
        "required": [
          "md5",
          "sha1",
          "sha256",
          "sha512"
        ]
      },
      "DatabaseMetadataColumn": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "type"
        ]
      },
      "DatabaseMetadata": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "update_freq": {
            "type": "string",
            "description": "How often a new build is published"
          },
          "updated": {
            "type": "string",
            "format": "date"
          },
          "entries": {
            "type": "integer",
            "description": "Row count in the current build"
          },
          "schema": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/DatabaseMetadataColumn"
              }
            },
            "description": "Columns, keyed by format"
          },
          "sample": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object"
              }
            },
            "description": "A few real rows, keyed by format"
          },
          "size": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Bytes per format"
          },
          "sample_size": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Bytes per format of the evaluation sample, where one is published"
          },
          "sample_entries": {
            "type": "integer",
            "description": "Row count in the evaluation sample"
          }
        },
        "required": [
          "id",
          "updated",
          "entries",
          "schema"
        ]
      },
      "Entitlement": {
        "type": "object",
        "required": [
          "org_id",
          "apikey",
          "plan",
          "usage"
        ],
        "properties": {
          "org_id": {
            "type": "string",
            "format": "uuid",
            "description": "The organization the key belongs to."
          },
          "apikey": {
            "$ref": "#/components/schemas/EntitlementApikey"
          },
          "plan": {
            "$ref": "#/components/schemas/EntitlementPlan"
          },
          "usage": {
            "$ref": "#/components/schemas/EntitlementUsage"
          }
        }
      },
      "EntitlementApikey": {
        "type": "object",
        "description": "The credential itself. The key is never echoed - only its id, which is\nwhat the console shows and what you can act on.\n",
        "required": [
          "id",
          "expires",
          "allowed_cidrs"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "expires": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null for a key with no end date, which is the normal case."
          },
          "allowed_cidrs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The source addresses this key may be used from. EMPTY means\nunrestricted, never \"deny all\".\n"
          }
        }
      },
      "EntitlementPlan": {
        "type": "object",
        "required": [
          "key",
          "tier"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The plan the organization is on.",
            "example": "max"
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "scale",
              "max"
            ],
            "description": "The field tier, which decides how much of a lookup answer comes\nback. What each tier includes is documented on the lookup endpoint\nrather than repeated here, so there is one place it can be wrong.\n"
          }
        }
      },
      "EntitlementUsage": {
        "type": "object",
        "required": [
          "requests",
          "quota",
          "hard_limit",
          "window_start",
          "window_end"
        ],
        "properties": {
          "requests": {
            "type": "integer",
            "format": "int64",
            "description": "Requests counted in the current window. The same number the lookup\nAPI gates on, and it can lag by a few seconds.\n"
          },
          "quota": {
            "type": "integer",
            "format": "int64",
            "description": "What the plan includes. Zero on a plan that includes none."
          },
          "hard_limit": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "Where we stop serving. NULL means never, which is the normal state\nof an uncapped paid plan and is not the same as zero. Above the\nquota and below this, requests are served and billed as overage.\n"
          },
          "window_start": {
            "type": "string",
            "format": "date-time",
            "description": "When the current allowance period began."
          },
          "window_end": {
            "type": "string",
            "format": "date-time",
            "description": "When the allowance next resets."
          }
        }
      },
      "EntitlementError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "AccountRc": {
        "type": "object",
        "properties": {
          "rc": {
            "type": "string",
            "description": "The outcome. `SUCCESS` on success; otherwise the reason."
          }
        },
        "required": [
          "rc"
        ]
      },
      "Identity": {
        "type": "object",
        "properties": {
          "rc": {
            "type": "string"
          },
          "user": {
            "$ref": "#/components/schemas/AccountUser"
          },
          "org": {
            "$ref": "#/components/schemas/AccountOrgRef"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What this credential may do right now."
          }
        },
        "required": [
          "rc",
          "user",
          "org",
          "scopes"
        ]
      },
      "AccountUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fullname": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          }
        },
        "required": [
          "id"
        ]
      },
      "AccountOrgRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          }
        },
        "required": [
          "id"
        ]
      },
      "AccountOrgWrap": {
        "type": "object",
        "properties": {
          "rc": {
            "type": "string"
          },
          "org": {
            "$ref": "#/components/schemas/AccountOrg"
          }
        },
        "required": [
          "rc",
          "org"
        ]
      },
      "AccountOrg": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "required": [
          "id"
        ]
      },
      "ApikeyList": {
        "type": "object",
        "properties": {
          "rc": {
            "type": "string"
          },
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApikeyDetail"
            }
          }
        },
        "required": [
          "rc",
          "keys"
        ]
      },
      "ApikeyDetail": {
        "description": "Key METADATA. Never the key itself.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "key_prefix": {
            "type": "string",
            "description": "The leading, non-secret part, so a key is recognisable without storing it."
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "expires": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "allowed_cidrs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Source-IP allowlist. EMPTY MEANS UNRESTRICTED, not deny-all."
          },
          "allowed_scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "retrievable": {
            "type": "boolean",
            "description": "Whether this key's secret can still be read back. False permanently for a key issued before secrets were stored recoverably."
          }
        },
        "required": [
          "id",
          "name",
          "key_prefix",
          "created"
        ]
      },
      "AccountCreateApikeyRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "A label you will recognise later. Shown wherever the key is listed."
          },
          "allowed_scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the new key may do. Omit for a key that carries no named scope, which is the safe default."
          }
        },
        "required": [
          "name"
        ]
      },
      "AccountCreatedApikey": {
        "type": "object",
        "properties": {
          "rc": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "description": "The secret. Returned once, here, and never again."
          },
          "key_prefix": {
            "type": "string"
          },
          "allowed_cidrs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "allowed_scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "rc",
          "id",
          "key"
        ]
      },
      "AccountRevealedApikey": {
        "type": "object",
        "properties": {
          "rc": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "description": "The secret."
          }
        },
        "required": [
          "rc",
          "key"
        ]
      },
      "OauthMetadata": {
        "type": "object",
        "properties": {
          "issuer": {
            "type": "string"
          },
          "authorization_endpoint": {
            "type": "string"
          },
          "token_endpoint": {
            "type": "string"
          },
          "device_authorization_endpoint": {
            "type": "string"
          },
          "revocation_endpoint": {
            "type": "string"
          },
          "scopes_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "response_types_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "grant_types_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "code_challenge_methods_supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "client_id_metadata_document_supported": {
            "type": "boolean",
            "description": "A client_id may be an https URL serving your client metadata."
          }
        },
        "required": [
          "issuer",
          "authorization_endpoint",
          "token_endpoint"
        ]
      },
      "DeviceAuthorizationRequest": {
        "type": "object",
        "properties": {
          "client_id": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "description": "Space-delimited. Anything your client is not registered for is dropped rather than refused."
          },
          "resource": {
            "type": "string",
            "description": "RFC 8707: what the token is for."
          }
        },
        "required": [
          "client_id"
        ]
      },
      "DeviceAuthorization": {
        "type": "object",
        "properties": {
          "device_code": {
            "type": "string",
            "description": "Yours. Poll with it; never show it to anyone."
          },
          "user_code": {
            "type": "string",
            "description": "Short and typable. This is what the person confirms."
          },
          "verification_uri": {
            "type": "string"
          },
          "verification_uri_complete": {
            "type": "string",
            "description": "The same page with the code already filled in."
          },
          "expires_in": {
            "type": "integer"
          },
          "interval": {
            "type": "integer",
            "description": "Seconds between polls."
          }
        },
        "required": [
          "device_code",
          "user_code",
          "verification_uri",
          "expires_in",
          "interval"
        ]
      },
      "TokenRequest": {
        "type": "object",
        "properties": {
          "grant_type": {
            "type": "string"
          },
          "client_id": {
            "type": "string"
          },
          "device_code": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "code_verifier": {
            "type": "string"
          },
          "redirect_uri": {
            "type": "string"
          },
          "refresh_token": {
            "type": "string"
          }
        },
        "required": [
          "grant_type",
          "client_id"
        ]
      },
      "TokenResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "type": "string"
          },
          "expires_in": {
            "type": "integer"
          },
          "refresh_token": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "description": "What was actually granted, which may be narrower than what was asked for."
          }
        },
        "required": [
          "access_token",
          "token_type",
          "expires_in"
        ]
      },
      "RevokeRequest": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          },
          "client_id": {
            "type": "string"
          }
        },
        "required": [
          "token"
        ]
      },
      "OauthError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "error_description": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid key, or the key lacks the db.download scope",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotLicensed": {
        "description": "Your organization holds no license for this database, or its term has ended",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnknownDataset": {
        "description": "No such database",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotAvailable": {
        "description": "Licensed, but today's copy has not been published yet",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AccountUnauthorized": {
        "description": "No credential was presented, or it is not valid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AccountRc"
            }
          }
        }
      },
      "AccountForbidden": {
        "description": "The credential does not hold the scope this endpoint requires. The\nmissing scope is deliberately not named.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AccountRc"
            }
          }
        }
      },
      "AccountNotFound": {
        "description": "No such key in this organization.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AccountRc"
            }
          }
        }
      },
      "AccountInvalid": {
        "description": "The request body is missing a required field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AccountRc"
            }
          }
        }
      }
    },
    "parameters": {
      "ApikeyIdParam": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The key's id, as returned by the list endpoint. Never the key itself.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    }
  },
  "x-tagGroups": [
    {
      "name": "VPNDetection IP API",
      "tags": [
        "Lookup"
      ]
    },
    {
      "name": "VPNDetection Database API",
      "tags": [
        "Database"
      ]
    },
    {
      "name": "VPNDetection Entitlement API",
      "tags": [
        "Entitlement"
      ]
    },
    {
      "name": "VPNDetection IAM API",
      "tags": [
        "Account",
        "Organization",
        "API keys"
      ]
    },
    {
      "name": "VPNDetection Authorization",
      "tags": [
        "Authorization"
      ]
    }
  ]
}
