openapi: 3.1.0
info:
  title: iplookupapi
  version: "1"
  summary: IP geolocation, network, timezone and security lookups.
  description: >-
    iplookupapi resolves IP addresses to geolocation, network (ASN), timezone
    and security data. All endpoints are served over HTTPS and return JSON.
    Authenticate every request with your API key, either as the `apikey` query
    parameter or the `apikey` request header.


    `HEAD` answers 405 (`method_not_allowed`; `Allow` names the methods the
    endpoint takes) on every billed endpoint, without running or billing it.
    `/v1/status` and the other free endpoints answer it like `GET`.


    This document is generated from the iplookupapi service implementation and
    is the machine-readable source of truth for the v1 REST API. A
    human-readable version is available at https://iplookupapi.com/docs.
  contact:
    name: iplookupapi support
    email: support@iplookupapi.com
    url: https://support.iplookupapi.com/
servers:
  - url: https://api.iplookupapi.com
    description: Production
security:
  - ApiKeyQuery: []
  - ApiKeyHeader: []
tags:
  - name: Lookup
    description: IP address lookups.
  - name: Metadata
    description: Account status.
paths:
  /v1/info:
    get:
      tags:
        - Lookup
      summary: Look up an IP address
      operationId: getInfo
      description: >-
        Returns geolocation, network, timezone, security and hosted-domain data
        for an IP address. When `ip` is omitted, the caller's own IP is used.
        Paid plans also get the `network` object: anycast, hosting, CDN,
        mobile carrier, carrier-grade NAT, satellite, privacy relay, the class
        of the announcing AS number and the RPKI state of its route.
        Paid plans also get `company` and `abuse`: the holder of the address
        block and its abuse contact, from the regional internet registries.
        `location` also says how exact it is: `precision`,
        `accuracy_radius_km` and `confidence`.
      parameters:
        - name: ip
          in: query
          required: false
          description: >-
            The IPv4 or IPv6 address to look up. Defaults to the requesting
            client's IP when omitted.
          schema:
            type: string
          example: "80.110.0.1"
        - name: language
          in: query
          required: false
          description: Language for translated location names.
          schema:
            type: string
            enum: [de, en, es, fr, ja, pt, ru, zh]
            default: en
        - name: hostname
          in: query
          required: false
          description: >-
            When `true`, performs a reverse-DNS lookup and includes the
            `hostname` field. May add latency.
          schema:
            type: boolean
            default: false
        - name: fields
          in: query
          required: false
          description: >-
            Comma-separated dot paths to return instead of the full object,
            e.g. `ip,location.country.alpha2,security` or
            `network.is_hosting,network.rpki_status`. Naming an object keeps
            it whole; unknown paths are ignored.
          schema:
            type: string
          example: "ip,location.country.alpha2,connection.asn"
        - name: user_agent
          in: query
          required: false
          description: >-
            The `User-Agent` header your own visitor sent (one for every
            address of a batch). With it, `security.bot.spoofed` says whether
            the address belongs to the operator of the bot that user agent
            claims to be (plans with security data). At most 512 characters.
            A query parameter ends up in URLs and access logs: for many
            addresses, send it in the body of `POST /v1/batch`.
          schema:
            type: string
            maxLength: 512
          example: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
        - name: coarse_location
          in: query
          required: false
          description: >-
            `omit` leaves the city out of the answer for an anycast address
            and for a coarse one (`location.precision` `coarse`: a mobile,
            carrier-grade NAT, satellite or relay network, whose users move
            between places the data cannot tell apart): the `city` names and
            IDs and `zip` are empty, the coordinates move to the centre of the
            region (of the country without one), `accuracy_radius_km` grows by
            the distance they moved and `confidence` is `null`. For the plans
            with the `network` object (paid plans); other callers may send it
            and get the city as without it. Left out, the city stays.
          schema:
            type: string
            enum: [omit]
          example: "omit"
      responses:
        '200':
          description: IP lookup result.
          headers:
            X-RateLimit-Limit-Quota-Month:
              $ref: '#/components/headers/RateLimitLimitQuotaMonth'
            X-RateLimit-Remaining-Quota-Month:
              $ref: '#/components/headers/RateLimitRemainingQuotaMonth'
            X-RateLimit-Limit-Quota-Minute:
              $ref: '#/components/headers/RateLimitLimitQuotaMinute'
            X-RateLimit-Remaining-Quota-Minute:
              $ref: '#/components/headers/RateLimitRemainingQuotaMinute'
            x-ratelimit-limit-grace-month:
              $ref: '#/components/headers/RateLimitLimitGraceMonth'
            x-ratelimit-remaining-grace-month:
              $ref: '#/components/headers/RateLimitRemainingGraceMonth'
            x-ratelimit-limit-overage-month:
              $ref: '#/components/headers/RateLimitLimitOverageMonth'
            x-ratelimit-remaining-overage-month:
              $ref: '#/components/headers/RateLimitRemainingOverageMonth'
            X-Cost:
              $ref: '#/components/headers/Cost'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InfoResponse'
              example:
                data:
                  ip: "80.110.0.1"
                  hostname: null
                  type: "v4"
                  range_type:
                    type: "PUBLIC"
                    description: "Public address"
                  connection:
                    asn: 8412
                    organization: "Magenta Telekom GmbH"
                    isp: "Magenta Telekom"
                    range: "80.110.0.0/24"
                  network:
                    is_anycast: false
                    is_hosting: false
                    hosting: { provider: null, service: null, region: null }
                    is_cdn: false
                    cdn: { provider: null }
                    is_mobile: false
                    carrier: { name: null, mcc: null, mnc: null }
                    is_cgnat: false
                    is_satellite: false
                    satellite: { provider: null }
                    is_relay: false
                    relay: { provider: null }
                    as_type: "isp"
                    rpki_status: "valid"
                    rpki_invalid_reason: null
                  company:
                    name: "Magenta Telekom GmbH"
                    domain: null
                    type: "isp"
                  abuse:
                    email: "abuse@magenta.at"
                    name: "Magenta Abuse"
                    phone: null
                    network: "80.108.0.0 - 80.110.255.255"
                    country: "AT"
                  location:
                    geonames_id: 2761369
                    latitude: 48.198674
                    longitude: 16.348388
                    zip: "1010"
                    continent:
                      code: "EU"
                      name: "Europe"
                      name_translated: "Europe"
                    country:
                      alpha2: "AT"
                      alpha3: "AUT"
                      calling_codes: ["+43"]
                      emoji: "🇦🇹"
                      name: "Austria"
                      name_translated: "Austria"
                      is_in_european_union: true
                    city:
                      name: "Vienna"
                      name_translated: "Vienna"
                    region:
                      alpha2: "AT-9"
                      name: "Vienna"
                      name_translated: "Vienna"
                    wof_id: 101748073
                    precision: "city"
                    accuracy_radius_km: 3.1
                    confidence: 0.92
                  tlds: [".at"]
                  timezone:
                    id: "Europe/Vienna"
                    current_time: "2026-10-14T16:46:37+02:00"
                    code: "CEST"
                    is_daylight_saving: true
                    gmt_offset: 7200
                  security:
                    is_anonymous: false
                    is_datacenter: false
                    is_vpn: false
                    is_bot: false
                    is_abuser: false
                    is_known_attacker: false
                    is_proxy: false
                    is_spam: false
                    is_tor: false
                    proxy_type: ""
                    is_icloud_relay: false
                    threat_score: 0
                  domains:
                    count: 0
                    domains: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/batch:
    get:
      tags:
        - Lookup
      summary: Look up many IP addresses at once (query string)
      operationId: batchInfoGet
      x-mcp-exclude: true
      description: >-
        The batch lookup of `POST /v1/batch` with the addresses in the query
        string, for quick tests: up to 100 IPv4 or IPv6 addresses, separated by
        commas (a `POST` body takes up to 1,000 with an API key); more are
        refused with 422. It returns the same object and costs the same. MCP
        clients use the `batchInfo` tool (`POST /v1/batch`) instead.
      parameters:
        - name: ips
          in: query
          required: true
          description: >-
            1 to 100 IPv4 or IPv6 addresses, separated by commas. An entry
            that is no IP address goes into `errors`.
          schema:
            type: string
          example: "80.110.0.1,8.8.8.8"
        - name: language
          in: query
          required: false
          description: Language for translated location names.
          schema:
            type: string
            enum: [de, en, es, fr, ja, pt, ru, zh]
            default: en
        - name: hostname
          in: query
          required: false
          description: >-
            When `true`, looks up the reverse-DNS name of every address, in
            parallel within 8 seconds: each answer gets `hostname` (`null`
            without a name) and `hostname_status`.
          schema:
            type: boolean
            default: false
        - name: fields
          in: query
          required: false
          description: >-
            Comma-separated dot paths to return instead of the full object,
            e.g. `ip,location.country.alpha2,security` or
            `network.is_hosting,network.rpki_status`. Naming an object keeps
            it whole; unknown paths are ignored.
          schema:
            type: string
          example: "ip,location.country.alpha2,connection.asn"
        - name: user_agent
          in: query
          required: false
          description: >-
            The `User-Agent` header your own visitor sent (one for every
            address of a batch). With it, `security.bot.spoofed` says whether
            the address belongs to the operator of the bot that user agent
            claims to be (plans with security data). At most 512 characters.
            A query parameter ends up in URLs and access logs: for many
            addresses, send it in the body of `POST /v1/batch`.
          schema:
            type: string
            maxLength: 512
          example: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
        - name: coarse_location
          in: query
          required: false
          description: >-
            `omit` leaves the city out of the answer for an anycast address
            and for a coarse one (`location.precision` `coarse`: a mobile,
            carrier-grade NAT, satellite or relay network, whose users move
            between places the data cannot tell apart): the `city` names and
            IDs and `zip` are empty, the coordinates move to the centre of the
            region (of the country without one), `accuracy_radius_km` grows by
            the distance they moved and `confidence` is `null`. For the plans
            with the `network` object (paid plans); other callers may send it
            and get the city as without it. Left out, the city stays.
          schema:
            type: string
            enum: [omit]
          example: "omit"
      responses:
        '200':
          description: Lookup results keyed by IP address.
          headers:
            X-Cost:
              $ref: '#/components/headers/Cost'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Your plan does not include the batch endpoint. `error.code` is
            `plan_upgrade_required`; `actions.upgrade` opens the plans. No lookup
            runs, and it is not counted against your quota.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "You are not allowed to use the batch endpoint, please upgrade your plan"
                error: { code: "plan_upgrade_required", message: "You are not allowed to use the batch endpoint, please upgrade your plan" }
                actions:
                  upgrade: "https://app.iplookupapi.com/upgrade?utm_source=api_error&utm_campaign=plan_upgrade_required&team=7123456789012345678"
                  docs: "https://iplookupapi.com/docs/openapi.yaml"
                info: "For more information, see documentation: https://iplookupapi.com/docs/status-codes#_403"
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags:
        - Lookup
      summary: Look up many IP addresses at once
      operationId: batchInfo
      description: >-
        Looks up to 1,000 IPv4 or IPv6 addresses in one request and returns the
        same object as the single lookup for each, keyed by address. Duplicates
        are looked up once. Each valid address costs one request (see `X-Cost`).
        An item that is no IP address does not fail the batch: `errors` lists it
        with the reason, and it is neither looked up nor billed; a batch is
        refused with 422 only when it has no single IP address, or more
        addresses than allowed: up to 1,000 in a `POST` body with an API key
        (100 with a trial key). A batch your quota cannot cover is refused with
        429 before any lookup runs; `quota.required` is what it needed. Private
        and reserved addresses come back with their `range_type` and without
        location data. The `batchInfo` MCP tool takes up to 100 addresses a
        call. The same lookup also works as `GET
        /v1/batch?ips=80.110.0.1,8.8.8.8`, with up to 100 addresses (more are
        refused with 422).
      parameters:
        - name: language
          in: query
          required: false
          description: Language for translated location names.
          schema:
            type: string
            enum: [de, en, es, fr, ja, pt, ru, zh]
            default: en
        - name: hostname
          in: query
          required: false
          description: >-
            When `true`, looks up the reverse-DNS name of every address, in
            parallel within 8 seconds: each answer gets `hostname` (`null`
            without a name) and `hostname_status`.
          schema:
            type: boolean
            default: false
        - name: fields
          in: query
          required: false
          description: >-
            Comma-separated dot paths to return instead of the full object,
            e.g. `ip,location.country.alpha2,security` or
            `network.is_hosting,network.rpki_status`. Naming an object keeps
            it whole; unknown paths are ignored.
          schema:
            type: string
          example: "ip,location.country.alpha2,connection.asn"
        - name: coarse_location
          in: query
          required: false
          description: >-
            `omit` leaves the city out of the answer for an anycast address
            and for a coarse one (`location.precision` `coarse`: a mobile,
            carrier-grade NAT, satellite or relay network, whose users move
            between places the data cannot tell apart): the `city` names and
            IDs and `zip` are empty, the coordinates move to the centre of the
            region (of the country without one), `accuracy_radius_km` grows by
            the distance they moved and `confidence` is `null`. For the plans
            with the `network` object (paid plans); other callers may send it
            and get the city as without it. Left out, the city stays.
          schema:
            type: string
            enum: [omit]
          example: "omit"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ips:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  x-mcp-max-items: 100
                  items:
                    type: string
                  description: >-
                    IPv4 or IPv6 addresses. An item that is no IP address goes
                    into `errors`.
                user_agent:
                  type: string
                  maxLength: 512
                  description: >-
                    The `User-Agent` header your own visitor sent, one for
                    every address of the batch. With it, `security.bot.spoofed`
                    says whether each address belongs to the operator of the
                    bot that user agent claims to be (plans with security
                    data). At most 512 characters.
                  example: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
              required: [ips]
            example:
              ips: ["80.110.0.1", "8.8.8.8"]
      responses:
        '200':
          description: >-
            Lookup results keyed by IP address, and `errors` for the items
            that are no IP address.
          headers:
            X-Cost:
              $ref: '#/components/headers/Cost'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
              example:
                data:
                  80.110.0.1:
                    ip: 80.110.0.1
                    hostname: null
                    type: v4
                    range_type:
                      type: PUBLIC
                      description: 'Public address'
                    connection:
                      asn: 8412
                      organization: 'Magenta Telekom GmbH'
                      isp: 'Magenta Telekom'
                      range: 80.110.0.0/24
                    location:
                      geonames_id: 2761369
                      latitude: 48.198674
                      longitude: 16.348388
                      zip: '1010'
                      continent:
                        code: EU
                        name: Europe
                        name_translated: Europe
                      country:
                        alpha2: AT
                        alpha3: AUT
                        calling_codes:
                          - '+43'
                        emoji: 🇦🇹
                        name: Austria
                        name_translated: Austria
                        is_in_european_union: true
                      city:
                        name: Vienna
                        name_translated: Vienna
                      region:
                        alpha2: AT-9
                        name: Vienna
                        name_translated: Vienna
                      wof_id: 101748073
                      precision: city
                      accuracy_radius_km: 3.1
                      confidence: 0.92
                    tlds:
                      - .at
                    timezone:
                      id: Europe/Vienna
                      current_time: '2026-10-14T16:46:37+02:00'
                      code: CEST
                      is_daylight_saving: true
                      gmt_offset: 7200
                    security:
                      is_anonymous: false
                      is_datacenter: false
                      is_vpn: false
                      is_bot: false
                      is_abuser: false
                      is_known_attacker: false
                      is_proxy: false
                      is_spam: false
                      is_tor: false
                      proxy_type: ""
                      is_icloud_relay: false
                      threat_score: 0
                    domains:
                      count: 0
                      domains: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Your plan does not include the batch endpoint. `error.code` is
            `plan_upgrade_required`; `actions.upgrade` opens the plans. No lookup
            runs, and it is not counted against your quota.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "You are not allowed to use the batch endpoint, please upgrade your plan"
                error: { code: "plan_upgrade_required", message: "You are not allowed to use the batch endpoint, please upgrade your plan" }
                actions:
                  upgrade: "https://app.iplookupapi.com/upgrade?utm_source=api_error&utm_campaign=plan_upgrade_required&team=7123456789012345678"
                  docs: "https://iplookupapi.com/docs/openapi.yaml"
                info: "For more information, see documentation: https://iplookupapi.com/docs/status-codes#_403"
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/status:
    get:
      tags:
        - Metadata
      summary: Account quota status
      operationId: getStatus
      description: >-
        Returns your current monthly and grace quota usage and the plan of your
        API key's account. Requests to this endpoint do not count against your
        quota or rate limit.
      responses:
        '200':
          description: Current quota usage for the authenticated account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
              example:
                account_id: 313373133731337
                account_id_str: "313373133731337"
                quotas:
                  month: { total: 150, used: 72, remaining: 78 }
                  grace: { total: 0, used: 0, remaining: 0 }
                plan:
                  name: "free"
                  interval: "month"
                  price_id: "price_1LNIQ2KszEJd98nk6DswpzXk"
                  free: true
                  custom: false
                  status: "active"
                  renews_at: "2026-11-01T00:00:00+00:00"
                  scheduled: null
                  upgrade_url: "https://app.iplookupapi.com/upgrade?utm_source=agent&utm_campaign=status&team=313373133731337"
                  plans_url: "https://api.iplookupapi.com/v1/agent/plans"
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  securitySchemes:
    ApiKeyQuery:
      type: apiKey
      in: query
      name: apikey
      description: API key passed as the `apikey` query parameter.
    ApiKeyHeader:
      type: apiKey
      in: header
      name: apikey
      description: >-
        API key passed as the `apikey` request header. Recommended, as it keeps
        the key out of URLs and access logs.
  headers:
    RateLimitLimitQuotaMonth:
      description: >-
        Your monthly request quota. Replaced by the grace-quota headers while
        your account is consuming grace quota.
      schema: { type: integer }
      example: 150
    RateLimitRemainingQuotaMonth:
      description: >-
        Remaining requests in your monthly quota. Replaced by the grace-quota
        headers while your account is consuming grace quota.
      schema: { type: integer }
      example: 78
    RateLimitLimitQuotaMinute:
      description: >-
        The minute rate limit of the gateway node that answered the request
        (5 on the free plan). The limit counts on each of our two gateway
        nodes, which makes the plan's 10 requests per minute. Only present on
        plans with a minute rate limit.
      schema: { type: integer }
      example: 5
    RateLimitRemainingQuotaMinute:
      description: >-
        Remaining requests in the current minute on the gateway node that
        answered the request. Only present on plans with a minute rate limit.
      schema: { type: integer }
      example: 3
    RateLimitLimitGraceMonth:
      description: >-
        Your grace quota. Sent instead of the monthly quota headers while your
        account is consuming grace quota (e.g. while a payment is pending).
      schema: { type: integer }
      example: 300
    RateLimitRemainingGraceMonth:
      description: >-
        Remaining requests in your grace quota. Sent instead of the monthly
        quota headers while your account is consuming grace quota.
      schema: { type: integer }
      example: 120
    RateLimitLimitOverageMonth:
      description: >-
        Your overage quota. Only present when overages are enabled for your
        account and the monthly quota is exhausted.
      schema: { type: integer }
      example: 1000
    RateLimitRemainingOverageMonth:
      description: >-
        Remaining requests in your overage quota. Only present when overages
        are enabled for your account and the monthly quota is exhausted.
      schema: { type: integer }
      example: 900
    Cost:
      description: Requests this call counted against your quota (0 for sandbox requests).
      schema: { type: integer }
      example: 1
  responses:
    Unauthorized:
      description: >-
        The API key is missing or invalid: every request needs one, in the
        `apikey` header or query parameter. Without a key the message is "No
        API key found in request" (`error.code` `missing_api_key`), with an
        unknown key "Unauthorized" (`invalid_api_key`). `actions.sign_up`
        creates an account with a free API key. `actions` can also carry
        `agent_setup` (setup steps for AI coding agents), `pricing` (the plans
        as JSON) and `openapi`; `docs` then points to the docs' llms.txt
        (example `missing_api_key_agent_links`).
      headers:
        WWW-Authenticate:
          description: Always `Key`.
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            missing_api_key:
              summary: No API key sent
              value:
                message: "No API key found in request"
                error: { code: "missing_api_key", message: "No API key found in request" }
                actions:
                  sign_up: "https://app.iplookupapi.com/register?utm_source=api_error&utm_campaign=missing_api_key"
                  docs: "https://iplookupapi.com/docs/openapi.yaml"
            invalid_api_key:
              summary: Unknown API key
              value:
                message: "Unauthorized"
                error: { code: "invalid_api_key", message: "Unauthorized" }
                actions:
                  sign_up: "https://app.iplookupapi.com/register?utm_source=api_error&utm_campaign=invalid_api_key"
                  docs: "https://iplookupapi.com/docs/openapi.yaml"
            missing_api_key_agent_links:
              summary: No API key sent, with the links for AI coding agents
              value:
                message: "No API key found in request"
                error: { code: "missing_api_key", message: "No API key found in request" }
                actions:
                  agent_setup: "https://iplookupapi.com/agents.md"
                  sign_up: "https://app.iplookupapi.com/register?utm_source=api_error&utm_campaign=missing_api_key"
                  pricing: "https://api.iplookupapi.com/v1/agent/plans"
                  docs: "https://iplookupapi.com/docs/llms.txt"
                  openapi: "https://iplookupapi.com/docs/openapi.yaml"
    ValidationError:
      description: One or more request parameters failed validation.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ValidationError' }
          example:
            message: "Validation error"
            errors:
              ip:
                - "The ip field must be a valid IP address."
            info: "For more information, see documentation: https://iplookupapi.com/docs/status-codes#_422"
    RateLimited:
      description: >-
        A limit is used up. `error.code` says which: `quota_exceeded` (the
        month's requests; `quota.resets_at` says when they come back),
        `grace_quota_exceeded` (the grace quota of an unpaid invoice:
        `actions.complete_payment` pays it), `overage_quota_exceeded` (the
        overage budget) or `rate_limit_exceeded` (the free plan's 10 requests
        a minute). Quota errors carry `quota`; a batch the quota cannot cover
        is refused before it runs, with `quota.required`. `actions.upgrade`
        opens the plans of the API key's account. `actions` can also carry
        `pricing` (the plans as JSON) and `openapi`; `docs` then points to the
        docs' llms.txt (the examples ending in `_agent_links`).
      headers:
        Retry-After:
          description: >-
            Seconds until the next full minute, with the requests-per-minute
            `rate_limit_exceeded` of an API key. A used-up quota has none: it
            lasts until `quota.resets_at`.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            quota_exceeded:
              summary: The month's requests are used up
              value:
                message: "You used all your monthly requests. Please upgrade your plan at https://app.iplookupapi.com/upgrade"
                error: { code: "quota_exceeded", message: "You used all your monthly requests. Please upgrade your plan at https://app.iplookupapi.com/upgrade" }
                quota: { limit: 150, used: 150, remaining: 0, resets_at: "2026-11-01T00:00:00+00:00" }
                actions:
                  upgrade: "https://app.iplookupapi.com/upgrade?utm_source=api_error&utm_campaign=quota_exceeded&team=7123456789012345678"
                  docs: "https://iplookupapi.com/docs/openapi.yaml"
            rate_limit_exceeded:
              summary: The requests per minute are used up
              value:
                message: "API rate limit exceeded"
                error: { code: "rate_limit_exceeded", message: "API rate limit exceeded" }
                actions:
                  upgrade: "https://app.iplookupapi.com/upgrade?utm_source=api_error&utm_campaign=rate_limit_exceeded&team=7123456789012345678"
                  docs: "https://iplookupapi.com/docs/openapi.yaml"
            quota_exceeded_agent_links:
              summary: The month's requests are used up, with the links for AI coding agents
              value:
                message: "You used all your monthly requests. Please upgrade your plan at https://app.iplookupapi.com/upgrade"
                error: { code: "quota_exceeded", message: "You used all your monthly requests. Please upgrade your plan at https://app.iplookupapi.com/upgrade" }
                quota: { limit: 150, used: 150, remaining: 0, resets_at: "2026-11-01T00:00:00+00:00" }
                actions:
                  upgrade: "https://app.iplookupapi.com/upgrade?utm_source=api_error&utm_campaign=quota_exceeded&team=7123456789012345678"
                  pricing: "https://api.iplookupapi.com/v1/agent/plans"
                  docs: "https://iplookupapi.com/docs/llms.txt"
                  openapi: "https://iplookupapi.com/docs/openapi.yaml"
            rate_limit_exceeded_agent_links:
              summary: The requests per minute are used up, with the links for AI coding agents
              value:
                message: "API rate limit exceeded"
                error: { code: "rate_limit_exceeded", message: "API rate limit exceeded" }
                actions:
                  upgrade: "https://app.iplookupapi.com/upgrade?utm_source=api_error&utm_campaign=rate_limit_exceeded&team=7123456789012345678"
                  pricing: "https://api.iplookupapi.com/v1/agent/plans"
                  docs: "https://iplookupapi.com/docs/llms.txt"
                  openapi: "https://iplookupapi.com/docs/openapi.yaml"
  schemas:
    InfoResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/IpInfo'
      required: [data]
    IpInfo:
      type: object
      properties:
        ip:
          type: string
          example: "80.110.0.1"
        hostname:
          type: [string, "null"]
          description: Reverse-DNS hostname; only populated when `hostname=true`.
          example: "80-110-0-1.static.upcbusiness.at"
        hostname_status:
          type: string
          enum: [found, not_found, timeout]
          description: >-
            Batch lookups with `hostname=true` only, after the other keys:
            whether the reverse-DNS lookup found a name, found none, or got
            no answer within 8 seconds (`hostname` is then `null`; ask again
            later).
        type:
          type: string
          enum: [v4, v6]
        range_type:
          type: object
          properties:
            type: { type: string, example: "PUBLIC" }
            description: { type: string, example: "Public address" }
        connection:
          type: object
          properties:
            asn:
              type: integer
              example: 8412
              description: >-
                The AS number that announces the address in BGP (the origin of
                the most specific route that covers it). Where no route covers
                the address, or a submitted geofeed or our manual data names the
                range's AS number or organization, the AS number our location
                data has for the range.
            organization:
              type: string
              example: "Magenta Telekom GmbH"
              description: The name of the organization of the AS number in `asn`.
            isp: { type: string, example: "Magenta Telekom" }
            range: { type: string, example: "80.110.0.0/24" }
        network:
          $ref: '#/components/schemas/Network'
        company:
          $ref: '#/components/schemas/Company'
        abuse:
          $ref: '#/components/schemas/Abuse'
        location:
          $ref: '#/components/schemas/Location'
        tlds:
          type: array
          items: { type: string }
          example: [".at"]
        timezone:
          type: object
          properties:
            id: { type: string, example: "Europe/Vienna" }
            current_time: { type: string, example: "2026-10-14T16:46:37+02:00" }
            code: { type: string, example: "CEST" }
            is_daylight_saving: { type: boolean }
            gmt_offset: { type: integer, example: 7200 }
        security:
          type: object
          description: >-
            Security flags of the address. All fields are `null` when your plan does not include security data.
            With security data, the detail after `threat_score` follows: who runs the VPN, bot or gateway,
            when the lists last saw the address, how strongly they back each flag, and `reasons`. Without
            security data (and without an API key) the block has the first 12 keys only.
          properties:
            is_anonymous:
              type: [boolean, "null"]
              description: Tor exit node, VPN, open proxy or privacy relay (e.g. iCloud Private Relay).
            is_datacenter:
              type: [boolean, "null"]
              description: Address of a datacenter, cloud, hosting or CDN network.
            is_vpn:
              type: [boolean, "null"]
              description: Server of a commercial or consumer VPN service.
            is_bot:
              type: [boolean, "null"]
              description: Verified crawler or monitor published by its operator (e.g. Googlebot) or an internet-wide research scanner.
            is_abuser:
              type: [boolean, "null"]
              description: Recently reported for abusive behaviour by one or more blocklists.
            is_known_attacker:
              type: [boolean, "null"]
              description: Recently seen attacking (brute force, exploitation, botnets) by threat feeds, or part of a hijacked netblock.
            is_proxy:
              type: [boolean, "null"]
              description: Public or open proxy (HTTP, SOCKS).
            is_spam:
              type: [boolean, "null"]
              description: Known spam sender.
            is_tor:
              type: [boolean, "null"]
              description: Tor exit node.
            proxy_type:
              type: [string, "null"]
              enum: [tor, vpn, proxy, relay, "", null]
              description: >-
                Anonymity type: tor, vpn, proxy, relay or an empty string when the IP is not anonymous (if several apply, the first in this order).
              example: ""
            is_icloud_relay:
              type: [boolean, "null"]
              description: iCloud Private Relay egress address.
            threat_score:
              type: [integer, "null"]
              minimum: 0
              maximum: 100
              multipleOf: 5
              description: >-
                Evidence-based threat score from 0 (no known threat) to 100, in steps of 10.
                The threat_score combines the anonymity type (Tor exit, open proxy, VPN, relay), internet scanners, datacenter hosting and every threat feed that lists the address, weighted by the feed's reliability and by how recently it saw the address. Verified crawlers and allowlisted infrastructure such as public DNS resolvers score 0. For example, a datacenter address alone scores 10, a VPN server 30, a VPN server in a datacenter 40, a Tor exit node 60 and a Tor exit node that is also reported for attacks 90.
              example: 0
            vpn:
              type: object
              description: >-
                About `is_vpn`: each value is `null` while `is_vpn` is not
                `true`.
              properties:
                  provider:
                    type: [string, "null"]
                    description: The VPN service the lists name for the address.
                    example: "NordVPN"
                  last_seen:
                    type: [string, "null"]
                    format: date
                    description: The last day (UTC) a list that sets `is_vpn` saw the address.
                    example: "2026-10-16"
                  confidence:
                    type: [number, "null"]
                    minimum: 0.05
                    maximum: 1
                    multipleOf: 0.05
                    description: >-
                      How strongly the lists that set `is_vpn` back it, from
                      0.05 to 1 in steps of 0.05: one minus the product of
                      (1 - trust) over those lists, where the trust of each
                      list is a weight we set by hand. It ranks the evidence;
                      it is not a calibrated probability (0.9 does not mean
                      that 9 in 10 such addresses are what the flag says).
                    example: 0.95
            proxy:
              type: object
              description: >-
                About `is_proxy`: each value is `null` while `is_proxy` is not
                `true`.
              properties:
                  provider:
                    type: [string, "null"]
                    description: The proxy service; no list names one yet, so it is `null`.
                  last_seen:
                    type: [string, "null"]
                    format: date
                    description: The last day (UTC) a list that sets `is_proxy` saw the address.
                    example: "2026-10-16"
                  confidence:
                    type: [number, "null"]
                    minimum: 0.05
                    maximum: 1
                    multipleOf: 0.05
                    description: >-
                      How strongly the lists that set `is_proxy` back it, from
                      0.05 to 1 in steps of 0.05: one minus the product of
                      (1 - trust) over those lists, where the trust of each
                      list is a weight we set by hand. It ranks the evidence;
                      it is not a calibrated probability (0.9 does not mean
                      that 9 in 10 such addresses are what the flag says).
                    example: 0.95
            tor:
              type: object
              description: >-
                About `is_tor`: each value is `null` while `is_tor` is not
                `true`.
              properties:
                  last_seen:
                    type: [string, "null"]
                    format: date
                    description: The last day (UTC) a list that sets `is_tor` saw the address.
                    example: "2026-10-16"
                  confidence:
                    type: [number, "null"]
                    minimum: 0.05
                    maximum: 1
                    multipleOf: 0.05
                    description: >-
                      How strongly the lists that set `is_tor` back it, from
                      0.05 to 1 in steps of 0.05: one minus the product of
                      (1 - trust) over those lists, where the trust of each
                      list is a weight we set by hand. It ranks the evidence;
                      it is not a calibrated probability (0.9 does not mean
                      that 9 in 10 such addresses are what the flag says).
                    example: 0.95
            known_attacker:
              type: object
              description: >-
                About `is_known_attacker`: each value is `null` while `is_known_attacker` is not
                `true`.
              properties:
                  last_seen:
                    type: [string, "null"]
                    format: date
                    description: The last day (UTC) a list that sets `is_known_attacker` saw the address.
                    example: "2026-10-16"
                  confidence:
                    type: [number, "null"]
                    minimum: 0.05
                    maximum: 1
                    multipleOf: 0.05
                    description: >-
                      How strongly the lists that set `is_known_attacker` back it, from
                      0.05 to 1 in steps of 0.05: one minus the product of
                      (1 - trust) over those lists, where the trust of each
                      list is a weight we set by hand. It ranks the evidence;
                      it is not a calibrated probability (0.9 does not mean
                      that 9 in 10 such addresses are what the flag says).
                    example: 0.95
            is_known_good_bot:
              type: [boolean, "null"]
              description: >-
                The address is in a list a bot's operator publishes for its
                crawlers, monitors or webhook senders (e.g. Googlebot). `false`
                where every such list on the address belongs to a service that
                also sends requests its customers define (shared egress): `bot`
                then still names it.
            bot:
              type: object
              description: >-
                The bot whose operator lists the address; `operator`,
                `provider` and `purpose` are `null` while `is_bot` comes from no
                such list.
              properties:
                operator:
                  type: [string, "null"]
                  description: The company that publishes the list.
                  example: "google"
                provider:
                  type: [string, "null"]
                  description: The bot of the list.
                  example: "googlebot"
                purpose:
                  type: [string, "null"]
                  enum: [search, ai_training, ai_search, user_fetch, ads, seo, social, monitor, webhook, null]
                  description: What the bot fetches for; `null` where its list mixes purposes.
                  example: "search"
                spoofed:
                  type: [boolean, "null"]
                  description: >-
                    Only with `user_agent`: `true` when the user agent claims a
                    bot whose operator publishes its IP ranges and the address
                    is in none of them, `false` when it is in the ranges of an
                    operator whose bot it claims, and `null` when that cannot
                    be told (no `user_agent`, or a bot we do not know). For a
                    bot whose operator documents the reverse DNS names of its
                    crawlers (Googlebot, bingbot, Applebot, YandexBot and
                    others), an address outside the published ranges is
                    checked by reverse and forward DNS, within 1.5 seconds:
                    `false` when the name and its address match, `true` when
                    they do not; without an answer in time, the published
                    ranges decide. A `true` adds a sentence to `reasons`.
            is_corporate_gateway:
              type: [boolean, "null"]
              description: >-
                The egress of a corporate security gateway (e.g. Zscaler), whose
                addresses VPN lists often list too.
            corporate_gateway:
              type: object
              properties:
                provider:
                  type: [string, "null"]
                  description: The gateway's company; `null` while `is_corporate_gateway` is not `true`.
                  example: "zscaler"
            last_seen:
              type: [string, "null"]
              format: date
              description: The last day (UTC) any list saw the address.
              example: "2026-10-16"
            reasons:
              type: array
              items: { type: string }
              description: >-
                One short sentence for each flag that is set, e.g. `Tor exit
                node, seen 2026-10-16` or `VPN exit of NordVPN`; empty when
                none is.
              example: ["Tor exit node, seen 2026-10-16", "Reported for attacks, seen 2026-10-15"]
        domains:
          type: object
          description: >-
            Domains hosted on the address: their number and up to 5 of them.
            Without security data in your plan, `count` is `null` and `domains`
            is empty.
          properties:
            count: { type: [integer, "null"], example: 12337 }
            domains:
              type: array
              items: { type: string }
      required: [ip, type]
    Network:
      type: object
      description: >-
        The kind of network the address is in: anycast, hosting, CDN, mobile,
        carrier-grade NAT, satellite and privacy relay, the class of the AS
        number that announces it, and the RPKI state of its route. Only in the
        answers of paid plans; the free plan gets no `network` object. A flag is `false` when the data behind it does not
        list the address, and `null` only while that data is unavailable
        (the names that come with it are `null` then too). A name is `null`
        unless its flag is `true` and the operator is known.
        `is_mobile`, `carrier`, `is_cgnat`, `as_type` and the RPKI values
        describe the AS number that announces the address in BGP (where no
        route covers it, the AS number of its network in our ASN data).
        `connection.asn` names the same AS number, except where a submitted
        geofeed or our manual data sets `connection.asn` or `organization` for
        the range, where no route covers the address, and where our ASN data
        has no name for the announcing AS number (together at most 15 of 9,921
        answers in a random sample of IPv4 addresses).
      properties:
        is_anycast:
          type: [boolean, "null"]
          description: >-
            The address is in an anycast prefix: announced from many places at
            once, e.g. a public DNS resolver or a CDN edge.
        is_hosting:
          type: [boolean, "null"]
          description: >-
            Address of a datacenter, cloud, hosting or CDN network (the same
            data as `security.is_datacenter`, also for plans without security
            data).
        hosting:
          type: object
          properties:
            provider:
              type: [string, "null"]
              description: The hosting or cloud operator, in lower case (e.g. `aws`, `google`, `azure`).
              example: "aws"
            service:
              type: [string, "null"]
              description: >-
                The cloud service of the range, where the operator publishes one
                (e.g. AWS's `ec2` or `s3`).
              example: "ec2"
            region:
              type: [string, "null"]
              description: The cloud region of the range, where the operator publishes one (e.g. `eu-west-1`).
              example: "eu-west-1"
        is_cdn:
          type: [boolean, "null"]
          description: Edge address of a content delivery network.
        cdn:
          type: object
          properties:
            provider:
              type: [string, "null"]
              description: The CDN, in lower case (e.g. `cloudflare`).
              example: "cloudflare"
        is_mobile:
          type: [boolean, "null"]
          description: >-
            The address is announced by an AS number that carries a mobile
            network and nothing else. An operator with fixed lines and mobile on
            one AS number is not flagged.
        carrier:
          type: object
          properties:
            name:
              type: [string, "null"]
              description: The mobile operator.
              example: "T-Mobile US"
            mcc:
              type: [string, "null"]
              description: The operator's E.212 mobile country code, where known.
              example: "310"
            mnc:
              type: [string, "null"]
              description: The operator's E.212 mobile network code, where known.
              example: "260"
        is_cgnat:
          type: [boolean, "null"]
          description: >-
            Carrier-grade NAT space: many subscribers of the operator share the
            address (operators' known NAT pools).
        is_satellite:
          type: [boolean, "null"]
          description: Address of a satellite internet access network.
        satellite:
          type: object
          properties:
            provider:
              type: [string, "null"]
              description: The satellite operator, in lower case (e.g. `starlink`).
              example: "starlink"
        is_relay:
          type: [boolean, "null"]
          description: Egress address of a privacy relay (e.g. iCloud Private Relay).
        relay:
          type: object
          properties:
            provider:
              type: [string, "null"]
              description: The relay, in lower case (e.g. `icloud`).
              example: "icloud"
        as_type:
          type: [string, "null"]
          enum: [isp, hosting, business, education, government, null]
          description: >-
            The class of the AS number that announces the address. `null` when
            it has no class or the address has no AS number.
          example: "isp"
        rpki_status:
          type: [string, "null"]
          enum: [valid, invalid, not_found, null]
          description: >-
            RFC 6811 route origin validation of the most specific announced
            route that covers the address, against the day's RPKI data: `valid`
            (a ROA authorises its origin AS and length), `invalid` (ROAs cover
            the route but none authorises it) or `not_found` (no ROA covers
            it). `null` when no announced route covers the address, and for a
            route that is new or changed its origin since the last RPKI check.
          example: "valid"
        rpki_invalid_reason:
          type: [string, "null"]
          enum: [asn, length, null]
          description: >-
            Why an `invalid` route is invalid: `asn` (a ROA allows the length
            but names another AS number) or `length` (the route is longer than
            every covering ROA allows). `null` unless `rpki_status` is
            `invalid`.
          example: null
    Company:
      type: object
      description: >-
        The holder of the address block, from the databases of the regional
        internet registries (RIPE NCC, ARIN, APNIC, LACNIC and AFRINIC) and
        their RDAP services: the organisation of the most specific registered
        block that names one. Only in the answers of paid plans, after
        `network`; the free plan gets no `company` object. A value is
        `null` where the registry data has none, and all of them for an
        address no registered block covers.
      properties:
        name:
          type: [string, "null"]
          description: >-
            The holder's name as registered: the organisation's, else the
            block's description.
          example: "Magenta Telekom GmbH"
        domain:
          type: [string, "null"]
          description: >-
            The holder's registrable domain, where the domain of its e-mail
            addresses or of its AS number matches the name.
          example: "google.com"
        type:
          type: [string, "null"]
          enum: [isp, hosting, business, education, government, null]
          description: >-
            The kind of organisation: from its name and domain, else the class
            of the AS number that announces the block.
          example: "isp"
    Abuse:
      type: object
      description: >-
        Where to report abuse from the address: the abuse contact of the most
        specific registered block that has one (its abuse contact, incident
        response team or the organisation's abuse contact; RDAP's abuse
        entity). Only in the answers of paid plans, after `company`. A value
        is `null` where the registry data has none.
      properties:
        email:
          type: [string, "null"]
          description: The abuse mailbox.
          example: "abuse@magenta.at"
        name:
          type: [string, "null"]
          description: The name of the contact or team.
          example: "Magenta Abuse"
        phone:
          type: [string, "null"]
          description: The contact's phone number as registered.
          example: "+1-650-253-0000"
        network:
          type: [string, "null"]
          description: >-
            The registered block the contact is for: a prefix, or `first -
            last` for a range that is none.
          example: "80.108.0.0 - 80.110.255.255"
        country:
          type: [string, "null"]
          description: >-
            The ISO 3166-1 alpha-2 country the registry states for that block
            (`null` for ARIN's blocks, whose RDAP answers state none).
          example: "AT"
    Location:
      type: object
      description: >-
        Where the address is. Every answer ends with `precision`,
        `accuracy_radius_km` and `confidence`, which say how exact it is.
      properties:
        geonames_id:
          type: [integer, "null"]
          description: >-
            The GeoNames ID of the most specific place found (the city, where
            there is one), as is the `geonames_id` of `continent`, `country`,
            `region` and `city`. `null` where the place has no GeoNames ID.
            Until the change described at https://iplookupapi.com/docs/info#what-changed
            all but the continent's held Who's On First IDs; `wof_id` holds
            those now.
          example: 2761369
        latitude: { type: number, example: 48.2089 }
        longitude: { type: number, example: 16.3721 }
        zip: { type: [string, "null"], example: "1010" }
        continent:
          type: object
          properties:
            code: { type: string, example: "EU" }
            name: { type: string, example: "Europe" }
            name_translated: { type: string, example: "Europe" }
            geonames_id: { type: [integer, "null"], example: 6255148 }
            wof_id: { type: [integer, "null"], example: 102191581 }
        country:
          type: object
          properties:
            alpha2: { type: string, example: "AT" }
            alpha3: { type: string, example: "AUT" }
            calling_codes:
              type: array
              items: { type: string }
              example: ["+43"]
            emoji: { type: string, example: "🇦🇹" }
            name: { type: string, example: "Austria" }
            name_translated: { type: string, example: "Austria" }
            is_in_european_union: { type: boolean }
            geonames_id: { type: [integer, "null"], example: 2782113 }
            wof_id: { type: [integer, "null"], example: 85632785 }
        city:
          type: object
          properties:
            name: { type: [string, "null"], example: "Vienna" }
            name_translated: { type: [string, "null"], example: "Vienna" }
            geonames_id: { type: [integer, "null"], example: 2761369 }
            wof_id: { type: [integer, "null"], example: 101748073 }
        region:
          type: object
          properties:
            alpha2: { type: [string, "null"], example: "AT-9" }
            name: { type: [string, "null"], example: "Vienna" }
            name_translated: { type: [string, "null"], example: "Vienna" }
            geonames_id: { type: [integer, "null"], example: 2761367 }
            wof_id: { type: [integer, "null"], example: 85681667 }
        wof_id:
          type: [integer, "null"]
          description: >-
            The Who's On First ID of the most specific place found, as is the
            `wof_id` of `continent`, `country`, `region` and `city`. For the
            location, `country`, `region` and `city` it is the value
            `geonames_id` had before; `continent.geonames_id` was already a
            GeoNames ID and stays.
          example: 101748073
        precision:
          type: [string, "null"]
          enum: [city, region, country, coarse, null]
          description: >-
            The finest level the location names: `city`, `region` or
            `country`. For the plans with the `network` object (paid plans) it
            is `coarse` for an address of a mobile, carrier-grade NAT,
            satellite or relay network (as `network` flags them), whose users
            move between places the data cannot tell apart; the city stays
            (`coarse_location=omit` leaves it out). `null` without a location.
          example: "city"
        accuracy_radius_km:
          type: [number, "null"]
          description: >-
            The radius in km around `latitude` and `longitude` that holds 80%
            of the addresses of answers like this one (the same location data,
            level, network class, region and range size), measured on
            addresses whose location is known (RIPE Atlas probes) and checked
            on addresses left out of the measurement. `null` where there are
            too few of them to say.
          example: 3.1
        confidence:
          type: [number, "null"]
          description: >-
            The share, from 0 to 1, of the addresses of answers like this one
            that are within 50 km of `latitude` and `longitude`, measured the
            same way. `null` where `accuracy_radius_km` is, and after
            `coarse_location=omit` moved the coordinates.
          example: 0.92
    BatchResponse:
      type: object
      properties:
        data:
          type: object
          description: The lookup of each requested address, keyed by the address.
          additionalProperties:
            $ref: '#/components/schemas/IpInfo'
        errors:
          type: object
          description: >-
            Only when the batch has items that are no IP address: each such
            item (cut to 64 characters) with the reason. They are neither
            looked up nor billed.
          additionalProperties: { type: string }
          example: { "not-an-ip": "Not a valid IP address." }
      required: [data]
    StatusResponse:
      type: object
      properties:
        account_id:
          type: integer
          example: 313373133731337
        account_id_str:
          type: string
          description: >-
            The same id as `account_id`, as a string for JavaScript clients,
            whose numbers lose precision above 2^53.
          example: "313373133731337"
        quotas:
          type: object
          description: >-
            Quota buckets: `month` and `grace` (all zero unless a grace quota
            is active, e.g. while a payment is being completed).
          properties:
            month: { $ref: '#/components/schemas/QuotaBucket' }
            grace: { $ref: '#/components/schemas/QuotaBucket' }
          required: [month]
        plan:
          type: object
          description: >-
            The plan of the API key's account, so you can check that an upgrade
            took effect.
          properties:
            name: { type: [string, "null"], example: "free" }
            interval: { type: [string, "null"], example: "month" }
            price_id: { type: [string, "null"], example: "price_1LNIQ2KszEJd98nk6DswpzXk" }
            free: { type: [boolean, "null"], example: true }
            custom:
              type: boolean
              description: A plan made for this account only; it has no `upgrade_url`.
              example: false
            status:
              type: [string, "null"]
              description: The status of the subscription, e.g. `active` or `past_due`.
              example: "active"
            renews_at: { type: [string, "null"], format: date-time, example: "2026-11-01T00:00:00+00:00" }
            scheduled:
              type: [object, "null"]
              description: A plan change that starts later, e.g. a downgrade at the end of the period.
              properties:
                name: { type: [string, "null"], example: "basic" }
                interval: { type: [string, "null"], example: "month" }
                starts_at: { type: string, format: date-time }
            upgrade_url:
              type: [string, "null"]
              format: uri
              description: Opens the plans of this account. `null` for a custom plan.
            plans_url:
              type: string
              format: uri
              description: Every plan as JSON.
              example: "https://api.iplookupapi.com/v1/agent/plans"
      required: [quotas]
    QuotaBucket:
      type: object
      properties:
        total: { type: integer, example: 150 }
        used: { type: integer, example: 72 }
        remaining: { type: integer, example: 78 }
    Error:
      type: object
      description: >-
        Error responses always carry a human-readable `message` and sometimes
        an `info` link to the documentation. Responses with status 401, 403,
        404 and 429 additionally carry a machine-readable envelope: a stable
        `error.code`, the affected `quota` (quota errors only) and `actions`
        with next-step URLs. Treat `error`, `quota` and `actions` as optional
        when parsing.
      properties:
        message: { type: string }
        info: { type: string }
        error:
          type: object
          description: Machine-readable error envelope.
          properties:
            code:
              type: string
              description: >-
                Stable machine-readable error code, e.g. `invalid_api_key`,
                `quota_exceeded`, `rate_limit_exceeded`, `not_found`,
                `forbidden`, `plan_upgrade_required`.
              example: "quota_exceeded"
            message: { type: string }
          required: [code, message]
        quota:
          type: object
          description: >-
            Details of the exhausted quota. Only present on quota errors;
            `required` is the cost of a batch that was refused before it ran.
          properties:
            limit: { type: integer, example: 150 }
            used: { type: integer, example: 150 }
            remaining: { type: integer, example: 0 }
            resets_at:
              type: [string, "null"]
              description: ISO 8601 timestamp of the next quota reset.
            required: { type: integer, example: 100 }
        actions:
          type: object
          description: >-
            Next steps as absolute URLs, keyed by action. A key is only
            present when it applies to this error and is available for this
            product right now, so read the keys you know and ignore the
            others. `sign_up`: create an account with a free API key.
            `upgrade`: the plans of the API key's account (quota and plan
            errors). `complete_payment`: pay the open invoice (grace quota).
            `verify_email`: confirm the account's email address. `docs`: the
            documentation (always present). `openapi`: this specification.
            `pricing`: the plans as JSON. `agent_setup`: setup steps for AI
            coding agents. `get_free_api_key`, `get_trial_key`, `get_api_key`:
            endpoints that issue API keys to agents; `agent_setup` explains
            them.
            `enable_overage`: turn on overage for the account, so requests past
            the monthly quota are served and billed per request up to a budget
            (quota_exceeded of a paid plan that has overage, while it is off).
          additionalProperties: { type: string, format: uri }
      required: [message]
    ValidationError:
      type: object
      properties:
        message: { type: string, example: "Validation error" }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
        info: { type: string }
      required: [message, errors]
