{
  "openapi": "3.1.0",
  "info": {
    "title": "whatsmy.fyi API",
    "version": "1.1.0",
    "description": "Free IP geolocation API powered by Cloudflare Workers. No third-party databases. No IP query logging. Sub-50ms latency globally.",
    "contact": {
      "name": "whatsmy.fyi",
      "url": "https://whatsmy.fyi",
      "email": "enterprise@whatsmy.fyi"
    },
    "license": {
      "name": "MIT",
      "url": "https://opensource.org/licenses/MIT"
    },
    "x-logo": {
      "url": "https://whatsmy.fyi/icon-512.png",
      "backgroundColor": "#0f172a"
    },
    "x-apisguru-categories": ["developer_tools", "open_data"],
    "x-origin": [
      { "format": "openapi", "version": "3.1", "url": "https://whatsmy.fyi/openapi.json" }
    ],
    "x-preferred": true
  },
  "servers": [
    {
      "url": "https://whatsmy.fyi",
      "description": "Production"
    }
  ],
  "security": [
    { "BearerAuth": [] }
  ],
  "tags": [
    {
      "name": "IP",
      "description": "IP address, geolocation, ASN/ISP, and connection metadata lookups."
    }
  ],
  "externalDocs": {
    "description": "API documentation",
    "url": "https://whatsmy.fyi/docs"
  },
  "paths": {
    "/api/v1/ip": {
      "get": {
        "operationId": "getIp",
        "summary": "Get IP metadata",
        "description": "Returns IP address, geolocation, and connection metadata for the requesting IP. The response shape depends on the API key's scope (ip, geo, or full).",
        "tags": ["IP"],
        "security": [
          { "BearerAuth": [] },
          { "ApiKeyQuery": [] }
        ],
        "parameters": [
          {
            "name": "key",
            "in": "query",
            "required": false,
            "description": "API key as query parameter (fallback to Authorization header)",
            "schema": { "type": "string", "example": "wmf_your_api_key" }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "Narrow the key's scope for this request only (e.g. a full-scope key requesting just the IP). Can never widen beyond the key's scope — broader values are ignored.",
            "schema": { "type": "string", "enum": ["ip", "geo", "full"] }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful IP lookup",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": { "type": "integer" },
                "description": "Daily request limit (10000 for free tier)"
              },
              "X-RateLimit-Remaining": {
                "schema": { "type": "integer" },
                "description": "Requests remaining today"
              },
              "X-RateLimit-Reset": {
                "schema": { "type": "integer" },
                "description": "Unix timestamp of next reset (midnight UTC)"
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IpResponse" },
                "examples": {
                  "geo": {
                    "summary": "Geo scope response",
                    "value": {
                      "status": "success",
                      "ip": "203.0.113.42",
                      "ipv6": null,
                      "city": "Amsterdam",
                      "region": "North Holland",
                      "region_code": "NH",
                      "country": "NL",
                      "country_name": "Netherlands",
                      "continent": "EU",
                      "latitude": 52.374,
                      "longitude": 4.8897,
                      "timezone": "Europe/Amsterdam",
                      "timezone_offset": 7200,
                      "postal_code": "1011",
                      "asn": 1101,
                      "org": "SURF B.V.",
                      "as": "AS1101 SURF B.V.",
                      "is_eu": true,
                      "currency": "EUR"
                    }
                  },
                  "full": {
                    "summary": "Full scope response",
                    "value": {
                      "status": "success",
                      "ip": "203.0.113.42",
                      "ipv6": null,
                      "city": "Amsterdam",
                      "region": "North Holland",
                      "region_code": "NH",
                      "country": "NL",
                      "country_name": "Netherlands",
                      "continent": "EU",
                      "latitude": 52.374,
                      "longitude": 4.8897,
                      "timezone": "Europe/Amsterdam",
                      "timezone_offset": 7200,
                      "postal_code": "1011",
                      "asn": 1101,
                      "org": "SURF B.V.",
                      "as": "AS1101 SURF B.V.",
                      "is_eu": true,
                      "currency": "EUR",
                      "http_protocol": "HTTP/3",
                      "tls_version": "TLSv1.3",
                      "tls_cipher": "AEAD-AES128-GCM-SHA256",
                      "rtt": 12,
                      "colo": "AMS",
                      "proxy": false,
                      "hosting": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "examples": {
                  "missing": {
                    "value": {
                      "error": "missing_api_key",
                      "message": "No API key provided. Use Authorization: Bearer <key> or ?key=<key>.",
                      "docs": "https://whatsmy.fyi/docs#authentication"
                    }
                  },
                  "invalid": {
                    "value": {
                      "error": "invalid_api_key",
                      "message": "The API key provided does not exist.",
                      "docs": "https://whatsmy.fyi/docs#authentication"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Inactive API key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": {
                  "error": "inactive_api_key",
                  "message": "This API key has been deactivated.",
                  "docs": "https://whatsmy.fyi/docs#authentication"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Two limits apply: the daily quota (10,000 requests/day on the free tier, resets at midnight UTC) and a burst limit (300 requests/minute per key, applies to all keys). Exceeding the burst limit automatically pauses the key; resume it from the dashboard.",
            "headers": {
              "Retry-After": {
                "schema": { "type": "integer" },
                "description": "Seconds until the limit resets (next midnight UTC for the daily quota, 60 for the burst limit)"
              },
              "X-RateLimit-Limit": {
                "schema": { "type": "integer" },
                "description": "The limit that was exceeded (10000 daily or 300 per minute)"
              },
              "X-RateLimit-Remaining": { "schema": { "type": "integer" } },
              "X-RateLimit-Reset": { "schema": { "type": "integer" } }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "examples": {
                  "daily": {
                    "summary": "Daily quota exceeded",
                    "value": {
                      "error": "rate_limit_exceeded",
                      "message": "You have reached the free tier limit of 10,000 requests/day. Resets at midnight UTC.",
                      "docs": "https://whatsmy.fyi/docs#rate-limits",
                      "enterprise": "https://whatsmy.fyi/enterprise"
                    }
                  },
                  "burst": {
                    "summary": "Burst limit exceeded — key auto-paused",
                    "value": {
                      "error": "rate_limit_exceeded",
                      "message": "Your API key has been automatically paused after exceeding the burst limit of 300 requests/minute. This usually indicates a bug in your code. Visit your dashboard to review and resume.",
                      "docs": "https://whatsmy.fyi/docs#rate-limits"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": {
                  "error": "internal_error",
                  "message": "An unexpected error occurred. Please try again.",
                  "docs": "https://whatsmy.fyi/docs#errors"
                }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "corsPreflightIp",
        "summary": "CORS preflight",
        "tags": ["IP"],
        "responses": {
          "204": { "description": "CORS preflight response" }
        }
      }
    },
    "/api/v1/ip/demo": {
      "get": {
        "operationId": "getIpDemo",
        "summary": "Get IP metadata (demo, no auth)",
        "description": "Demo version of the IP endpoint for trying the API without an account. No API key required. Returns a fixed field set (roughly the geo scope plus http_protocol, tls_version, rtt, and colo). Shares a single quota of 1,000 requests/day across all anonymous users — do not use in production.",
        "tags": ["IP"],
        "security": [],
        "responses": {
          "200": {
            "description": "Successful demo IP lookup",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IpResponse" }
              }
            }
          },
          "429": {
            "description": "Shared demo quota of 1,000 requests/day exhausted. Resets at midnight UTC. Create a free account for your own 10,000 requests/day key.",
            "headers": {
              "Retry-After": {
                "schema": { "type": "integer" },
                "description": "Seconds until midnight UTC"
              },
              "X-RateLimit-Limit": { "schema": { "type": "integer" } },
              "X-RateLimit-Remaining": { "schema": { "type": "integer" } },
              "X-RateLimit-Reset": { "schema": { "type": "integer" } }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": {
                  "error": "rate_limit_exceeded",
                  "message": "The shared demo endpoint has reached its daily limit of 1,000 requests. Create a free account to get your own API key with 10,000 requests/day.",
                  "docs": "https://whatsmy.fyi/docs#try-it",
                  "signup": "https://whatsmy.fyi/signup"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "corsPreflightIpDemo",
        "summary": "CORS preflight",
        "tags": ["IP"],
        "responses": {
          "204": { "description": "CORS preflight response" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Pass your API key as a Bearer token in the Authorization header."
      },
      "ApiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "key",
        "description": "Pass your API key as a ?key= query parameter (fallback)."
      }
    },
    "schemas": {
      "IpResponse": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status":          { "type": "string", "enum": ["success"] },
          "ip":              { "type": ["string", "null"], "description": "IPv4 address (null if IPv6-only)" },
          "ipv6":            { "type": ["string", "null"], "description": "IPv6 address (null if IPv4-only)" },
          "city":            { "type": ["string", "null"], "description": "City name (geo+)" },
          "region":          { "type": ["string", "null"], "description": "Region / state name (geo+)" },
          "region_code":     { "type": ["string", "null"], "description": "ISO 3166-2 code (geo+)" },
          "country":         { "type": ["string", "null"], "description": "ISO 3166-1 alpha-2 country code (geo+)" },
          "country_name":    { "type": ["string", "null"], "description": "Full country name in English (geo+)" },
          "continent":       { "type": ["string", "null"], "description": "Two-letter continent code (geo+)" },
          "latitude":        { "type": ["number", "null"], "description": "Latitude (geo+)" },
          "longitude":       { "type": ["number", "null"], "description": "Longitude (geo+)" },
          "timezone":        { "type": ["string", "null"], "description": "IANA timezone identifier (geo+)" },
          "timezone_offset": { "type": ["integer", "null"], "description": "UTC offset in seconds (geo+)" },
          "postal_code":     { "type": ["string", "null"], "description": "Postal code (geo+)" },
          "asn":             { "type": ["integer", "null"], "description": "Autonomous System Number (geo+)" },
          "org":             { "type": ["string", "null"], "description": "AS organization name / ISP (geo+)" },
          "as":              { "type": ["string", "null"], "description": "Formatted AS string, e.g. 'AS1101 SURF B.V.' (geo+)" },
          "is_eu":           { "type": ["boolean", "null"], "description": "Whether the IP is in the EU (geo+)" },
          "currency":        { "type": ["string", "null"], "description": "ISO 4217 currency code for the country (geo+)" },
          "http_protocol":   { "type": ["string", "null"], "description": "HTTP protocol version (full)" },
          "tls_version":     { "type": ["string", "null"], "description": "TLS version (full)" },
          "tls_cipher":      { "type": ["string", "null"], "description": "TLS cipher suite (full)" },
          "rtt":             { "type": ["integer", "null"], "description": "Round-trip time to Cloudflare edge in ms (full)" },
          "colo":            { "type": ["string", "null"], "description": "Cloudflare datacenter IATA code (full)" },
          "proxy":           { "type": ["boolean", "null"], "description": "Known VPN/proxy ASN (full)" },
          "hosting":         { "type": ["boolean", "null"], "description": "Known hosting/cloud ASN (full)" }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["error", "message", "docs"],
        "properties": {
          "error":      { "type": "string", "description": "Snake_case error code" },
          "message":    { "type": "string", "description": "Human-readable explanation" },
          "docs":       { "type": "string", "description": "Link to relevant docs section" },
          "enterprise": { "type": "string", "description": "Enterprise page URL (present on daily-quota 429 only)" },
          "signup":     { "type": "string", "description": "Signup page URL (present on demo-endpoint 429 only)" }
        }
      }
    }
  }
}
