openapi: 3.1.0

info:
  title: IP Address API
  summary: Public IP, geolocation, and request inspection API.
  description: |
    Free API that returns information about the calling client: public IP address,
    ASN, geolocation, timezone, user-agent, the HTTP headers sent by the client,
    and connection security details (TLS, bot detection).

    Every endpoint supports `GET`, `POST`, and `HEAD`. POST bodies are ignored —
    POST is provided purely for clients that cannot issue a GET.

    ## Content negotiation

    The same path returns different formats depending on the request:

    - **`text/plain`** — default.
    - **`application/json`** — returned when any of these are true:
      - `Accept` header contains `application/json` or `*/json`
      - `Content-Type` header contains `application/json`
      - `?json=1` or `?format=json` query parameter is present
    - **`text/html`** — only on `/`, when `Accept` contains `text/html` (returns
      the human-readable documentation page instead of the IP).
    - **`text/markdown`** — only on `/`, when `Accept` contains `text/markdown`
      (or `?format=md` / `?md=1`). Returns the raw Markdown README.

    ## Response headers (always present)

    Every response carries:

    - `x-ipapp-ip` — the detected public IP (or `Unknown` / empty)
    - `x-ipapp-ip-version` — `4`, `6`, or empty

    Per-endpoint `x-ipapp-*` headers are listed in each response section. These
    make the API usable over `HEAD` requests (no response body needed).

    ## Rate limiting and geo-restriction

    Excessive requests are rejected with HTTP `429`; Cloudflare's edge may
    additionally return a non-standard `1015` block page. Certain countries are
    blocked at the edge due to historic abuse.
  version: "2026.01.15"
  contact:
    name: Fili
    url: https://fili.com/?ref=ip.app
  license:
    name: Use at your own risk — see disclaimer
    url: https://fili.com/d/?ref=ip.app

servers:
  - url: https://ip.app

tags:
  - name: ip
    description: Caller IP address
  - name: network
    description: Network-level info about the caller (ASN, location, timezone)
  - name: request
    description: Information extracted from the request itself (headers, user-agent)
  - name: security
    description: Connection security and bot detection
  - name: geolocation
    description: Geolocation endpoints (Mozilla Location Service-compatible)

paths:
  /:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [ip]
      summary: Caller IP address
      description: |
        Returns the public IP address of the requesting client.

        When called with `Accept: text/html`, returns the HTML documentation page
        instead of the IP.
      responses:
        '200':
          description: IP returned (or docs page for HTML requests).
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
          content:
            text/plain:
              schema: { type: string, example: "1.1.1.1" }
            application/json:
              schema: { $ref: '#/components/schemas/IpResponse' }
            text/html:
              schema: { type: string, description: Documentation page. }
            text/markdown:
              schema: { type: string, description: Raw Markdown README. }
        '429': { $ref: '#/components/responses/RateLimited' }
        '405': { $ref: '#/components/responses/MethodNotAllowed' }
    post:
      tags: [ip]
      summary: Caller IP address (POST; body ignored)
      description: Same as `GET /`. Any POST body is ignored.
      responses:
        '200':
          description: IP returned.
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
          content:
            text/plain: { schema: { type: string } }
            application/json: { schema: { $ref: '#/components/schemas/IpResponse' } }
    head:
      tags: [ip]
      summary: IP in response headers only (no body)
      responses:
        '200':
          description: No body; IP in `x-ipapp-ip` header.
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }

  /asn:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [network]
      summary: ASN of the caller
      description: |
        Returns the Autonomous System Number associated with the caller's IP,
        plus the AS organization name when available.

        Text format: `AS15169: GOOGLE` — or empty if unavailable.
      responses:
        '200':
          description: ASN returned (or empty when unknown).
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
            x-ipapp-asn:
              description: ASN with `AS` prefix, e.g. `AS15169`. Empty if unknown.
              schema: { type: string, example: "AS15169" }
          content:
            text/plain:
              schema: { type: string, example: "AS15169: GOOGLE" }
            application/json:
              schema: { $ref: '#/components/schemas/AsnResponse' }
    post: { $ref: '#/paths/~1asn/get' }
    head:
      tags: [network]
      summary: ASN in response headers only
      responses:
        '200':
          description: No body; ASN in `x-ipapp-asn` header.
          headers:
            x-ipapp-asn:
              schema: { type: string }

  /headers:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [request]
      summary: HTTP headers sent by the caller
      description: |
        Returns the HTTP headers the client sent with its request. Cloudflare-injected
        headers (`cf-*`), `x-forwarded-*`, common proxy/tracing headers (B3, Datadog,
        Envoy, Amazon, etc.), and `x-real-ip` are filtered out.

        On `HEAD` responses, each client header is echoed back as a response header
        prefixed with `x-ipapp-header-` (lowercased name).
      responses:
        '200':
          description: Map of the caller's request headers.
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
          content:
            text/plain:
              schema: { type: string, example: "user-agent: curl/8.5.0\naccept: */*" }
            application/json:
              schema: { $ref: '#/components/schemas/HeadersResponse' }
    post: { $ref: '#/paths/~1headers/get' }
    head:
      tags: [request]
      summary: Headers echoed via `x-ipapp-header-*` response headers
      responses:
        '200':
          description: No body; each client header echoed as `x-ipapp-header-<name>`.

  /loc:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [network]
      summary: Caller IP geolocation
      description: |
        City, region, country, postal code, continent, EU membership, and
        approximate coordinates, derived from Cloudflare's IP geolocation.
      responses:
        '200':
          description: Location fields (values may be empty when unknown).
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
            x-ipapp-loc-city: { schema: { type: string } }
            x-ipapp-loc-region: { schema: { type: string } }
            x-ipapp-loc-country: { schema: { type: string, description: ISO 3166-1 alpha-2 code. } }
            x-ipapp-loc-postal_code: { schema: { type: string } }
            x-ipapp-loc-continent: { schema: { type: string } }
            x-ipapp-loc-is_eu_country: { schema: { type: string, enum: ["true", "false"] } }
            x-ipapp-loc-latitude: { schema: { type: string } }
            x-ipapp-loc-longitude: { schema: { type: string } }
          content:
            text/plain:
              schema: { type: string }
            application/json:
              schema: { $ref: '#/components/schemas/LocationResponse' }
    post: { $ref: '#/paths/~1loc/get' }
    head:
      tags: [network]
      summary: Location in response headers only
      responses:
        '200':
          description: No body; location in `x-ipapp-loc-*` headers.

  /sec:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [security]
      summary: Connection security and bot detection
      description: |
        Information about the TLS connection (protocol, cipher, version, TCP RTT)
        and Cloudflare's bot management signals: bot score (1-100, higher = more
        likely human), whether the UA is a verified bot, and the verified bot
        category when applicable.
      responses:
        '200':
          description: Security and bot-detection data for the current connection.
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
            x-ipapp-sec-http-protocol: { schema: { type: string, example: HTTP/2 } }
            x-ipapp-sec-tls-version: { schema: { type: string, example: TLSv1.3 } }
            x-ipapp-sec-tls-cipher: { schema: { type: string } }
            x-ipapp-sec-tcp-rtt-ms: { schema: { type: string } }
            x-ipapp-sec-bot-score: { schema: { type: string, example: "99" } }
            x-ipapp-sec-is-verified-bot: { schema: { type: string, enum: ["true", "false"] } }
            x-ipapp-sec-bot-category: { schema: { type: string } }
          content:
            text/plain: { schema: { type: string } }
            application/json:
              schema: { $ref: '#/components/schemas/SecurityResponse' }
    post: { $ref: '#/paths/~1sec/get' }
    head:
      tags: [security]
      summary: Security info in response headers only
      responses:
        '200':
          description: No body; security info in `x-ipapp-sec-*` headers.

  /tz:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [network]
      summary: Caller timezone
      description: IANA timezone name, e.g. `Europe/Berlin`.
      responses:
        '200':
          description: Timezone (empty when unknown).
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
            x-ipapp-tz: { schema: { type: string, example: Europe/Berlin } }
          content:
            text/plain: { schema: { type: string, example: Europe/Berlin } }
            application/json:
              schema: { $ref: '#/components/schemas/TimezoneResponse' }
    post: { $ref: '#/paths/~1tz/get' }
    head:
      tags: [network]
      summary: Timezone in response headers only
      responses:
        '200':
          description: No body; timezone in `x-ipapp-tz` header.

  /ua:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [request]
      summary: Caller User-Agent
      description: Returns the `User-Agent` header sent by the client.
      responses:
        '200':
          description: User-Agent string.
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
            x-ipapp-ua: { schema: { type: string } }
          content:
            text/plain: { schema: { type: string } }
            application/json:
              schema: { $ref: '#/components/schemas/UserAgentResponse' }
    post: { $ref: '#/paths/~1ua/get' }
    head:
      tags: [request]
      summary: User-Agent in response headers only
      responses:
        '200':
          description: No body; UA in `x-ipapp-ua` header.

  /whereami:
    parameters:
      - $ref: '#/components/parameters/Json'
      - $ref: '#/components/parameters/Format'
    get:
      tags: [geolocation]
      summary: Coordinates (Mozilla Location Service-compatible)
      description: |
        Returns geolocation coordinates for the caller. JSON response shape is
        compatible with the decommissioned Mozilla Location Service / Ichnaea
        `geolocate` response so that Geoclue (Linux) can use it as a drop-in.

        Accuracy semantics:
        - `50000` (50 km) — precise IP-geolocation coords available
        - `500000` (500 km) — only country-level; falls back to country centroid
      responses:
        '200':
          description: Coordinates found.
          headers:
            x-ipapp-ip: { $ref: '#/components/headers/XIpappIp' }
            x-ipapp-ip-version: { $ref: '#/components/headers/XIpappIpVersion' }
            x-ipapp-latitude: { schema: { type: string } }
            x-ipapp-longitude: { schema: { type: string } }
          content:
            text/plain:
              schema: { type: string, example: "52.52437, 13.41053" }
            application/json:
              schema: { $ref: '#/components/schemas/WhereamiResponse' }
        '404':
          description: No geolocation data available for this IP.
          content:
            text/plain: { schema: { type: string, example: "Not Found" } }
    post: { $ref: '#/paths/~1whereami/get' }
    head:
      tags: [geolocation]
      summary: Coordinates in response headers only
      responses:
        '200':
          description: No body; coords in `x-ipapp-latitude` / `x-ipapp-longitude`.
        '404':
          description: No geolocation data available.

  /docs:
    get:
      tags: [ip]
      summary: Redirect to HTML docs at `/`
      responses:
        '301':
          description: Permanent redirect to `/`.
          headers:
            Location: { schema: { type: string, example: "/" } }

  /robots.txt:
    get:
      tags: [ip]
      summary: robots.txt
      responses:
        '200':
          description: Robots exclusion file.
          content:
            text/plain: { schema: { type: string } }

  /llms.txt:
    get:
      tags: [ip]
      summary: llms.txt summary for LLMs
      description: |
        An [llms.txt](https://llmstxt.org)-format summary of the API — a concise
        Markdown file LLMs can use to quickly locate the endpoints and the
        OpenAPI spec.
      responses:
        '200':
          description: Markdown summary.
          content:
            text/plain: { schema: { type: string } }

  /openapi.yaml:
    get:
      tags: [ip]
      summary: OpenAPI specification (YAML)
      responses:
        '200':
          description: This OpenAPI document in YAML.
          content:
            application/yaml: { schema: { type: string } }

  /openapi.json:
    get:
      tags: [ip]
      summary: OpenAPI specification (JSON)
      responses:
        '200':
          description: This OpenAPI document in JSON.
          content:
            application/json: { schema: { type: object } }

components:

  parameters:
    Json:
      name: json
      in: query
      required: false
      description: Set to `1` to force a JSON response.
      schema: { type: string, enum: ["1"] }
    Format:
      name: format
      in: query
      required: false
      description: Set to `json` to force a JSON response.
      schema: { type: string, enum: ["json"] }

  headers:
    XIpappIp:
      description: Detected public IP of the caller (or `Unknown` / empty).
      schema: { type: string, example: "1.1.1.1" }
    XIpappIpVersion:
      description: IP version — `4`, `6`, or empty string if no valid IP.
      schema: { type: string, enum: ["4", "6", ""] }

  schemas:
    IpResponse:
      type: object
      required: [ip, ip_version]
      properties:
        ip:
          type: string
          description: Public IPv4 or IPv6 address, or `Unknown`.
          example: "1.1.1.1"
        ip_version:
          type: string
          enum: ["4", "6", ""]
          example: "4"

    AsnResponse:
      type: object
      description: Empty object `{}` when no ASN is available.
      properties:
        asn:
          type: string
          pattern: '^AS[0-9]+$'
          example: "AS15169"
        organization:
          type: string
          example: "GOOGLE"

    HeadersResponse:
      type: object
      description: |
        Map of header name → value. Keys depend on what the client sent.
        Cloudflare-injected headers and common proxy/tracing headers are
        filtered out.
      additionalProperties: { type: string }
      example:
        user-agent: "curl/8.5.0"
        accept: "*/*"
        accept-encoding: "gzip"

    LocationResponse:
      type: object
      description: Any field may be empty when not known.
      properties:
        latitude: { type: [string, number], example: 52.52437 }
        longitude: { type: [string, number], example: 13.41053 }
        city: { type: string, example: Berlin }
        region: { type: string, example: Berlin }
        region_code: { type: string, example: BE }
        country: { type: string, example: DE, description: ISO 3166-1 alpha-2 }
        postal_code: { type: string, example: "10115" }
        continent: { type: string, example: EU }
        is_eu_country: { type: [boolean, string], example: true }

    SecurityResponse:
      type: object
      properties:
        http_protocol: { type: string, example: "HTTP/2" }
        tls_version:   { type: string, example: "TLSv1.3" }
        tls_cipher:    { type: string, example: "AEAD-AES256-GCM-SHA384" }
        tcp_rtt_ms:    { type: [string, number], example: 12 }
        bot_score:
          type: [string, number]
          description: 1 (bot) → 100 (human). `0` when bot management unavailable.
          example: 99
        is_verified_bot: { type: boolean, example: false }
        bot_category:
          type: string
          description: Cloudflare verified-bot category, or `Unknown`.
          example: "Search Engine Crawler"

    TimezoneResponse:
      type: object
      description: Empty `{}` when unknown.
      properties:
        tz: { type: string, example: "Europe/Berlin" }

    UserAgentResponse:
      type: object
      required: [ua]
      properties:
        ua: { type: string, example: "curl/8.5.0" }

    WhereamiResponse:
      type: object
      required: [location, accuracy]
      description: Mozilla Location Service / Ichnaea-compatible shape.
      properties:
        location:
          type: object
          required: [lat, lng]
          properties:
            lat: { type: number, example: 52.52437 }
            lng: { type: number, example: 13.41053 }
        accuracy:
          type: integer
          description: Accuracy radius in meters (50000 = IP precision, 500000 = country centroid).
          enum: [50000, 500000]
          example: 50000

    Error:
      type: object
      properties:
        error: { type: string }

  responses:
    RateLimited:
      description: |
        Rate limit exceeded. Cloudflare may additionally return a non-standard
        `1015` block page at the edge.
      content:
        text/plain:
          schema: { type: string }
    MethodNotAllowed:
      description: Method not allowed. Use GET, POST, or HEAD.
      headers:
        Allow:
          schema: { type: string, example: "GET, POST, HEAD" }
      content:
        text/plain:
          schema: { type: string, example: "Method not allowed. Use GET, POST, or HEAD." }
    NotFound:
      description: Path not found.
      content:
        text/plain:
          schema: { type: string, example: "Not Found" }
