openapi: 3.1.0
info:
  title: Network Data Labs API
  version: "0.1.0"
  summary: Honest IP intelligence — geo with transparent accuracy, ASN/network, hosting/Tor/VPN flags
  description: |
    Developer-first IP intelligence API from Network Data Labs.

    Every geo response includes `accuracy_radius_km` and `confidence` — we do not publish
    single-number "99% city accuracy" claims. Country/ASN/hosting flags ship day one;
    downloadable MMDB/CSV databases come later.

    **Base URL:** `https://api.networkdatalabs.com`

    **Auth:** `Authorization: Bearer <api_key>` or `?api_key=`

    **Languages:** Docs and support in Spanish and English.
  contact:
    name: Network Data Labs
    email: hello@networkdatalabs.com
    url: https://networkdatalabs.com
  license:
    name: Proprietary
    url: https://networkdatalabs.com

servers:
  - url: https://api.networkdatalabs.com
    description: Production

security:
  - BearerAuth: []
  - ApiKeyQuery: []

tags:
  - name: IP Lookup
    description: Geolocation, network, and traits for a single IP
  - name: ASN
    description: Autonomous System details
  - name: Bulk
    description: Batch lookups (up to 100 IPs in v0)
  - name: Account
    description: Caller identity and quota

paths:
  /v0/ip/{ip}:
    get:
      tags: [IP Lookup]
      operationId: getIpLookup
      summary: Full IP intelligence lookup
      description: |
        Primary endpoint. Returns location (with accuracy radius + confidence),
        network/ASN, traits (hosting/Tor/VPN/proxy/etc.), and meta.
      parameters:
        - $ref: "#/components/parameters/IpPath"
        - $ref: "#/components/parameters/ApiKeyQueryOptional"
      responses:
        "200":
          description: Successful lookup
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IpLookup"
              examples:
                google_dns:
                  summary: 8.8.8.8 (Google Public DNS)
                  value:
                    ip: "8.8.8.8"
                    version: "v0"
                    location:
                      continent_code: "NA"
                      country_code: "US"
                      country: "United States"
                      is_eu: false
                      region: "California"
                      region_code: "CA"
                      city: "Mountain View"
                      postal: "94043"
                      latitude: 37.4056
                      longitude: -122.0775
                      timezone: "America/Los_Angeles"
                      accuracy_radius_km: 1000
                      confidence: "low"
                    network:
                      asn: 15169
                      as_org: "Google LLC"
                      isp: "Google LLC"
                      organization: "Google LLC"
                      network: "8.8.8.0/24"
                    traits:
                      is_anycast: true
                      is_hosting: true
                      is_mobile: false
                      is_satellite: false
                      is_bogon: false
                      is_proxy: false
                      is_vpn: false
                      is_tor: false
                      is_relay: false
                    meta:
                      data_version: "2026-09-20"
                      sources: ["rir", "licensed_geo", "public_blocklists"]
                      cached: false
                latam_ip:
                  summary: 200.32.0.1 (LATAM example)
                  value:
                    ip: "200.32.0.1"
                    version: "v0"
                    location:
                      continent_code: "SA"
                      country_code: "AR"
                      country: "Argentina"
                      is_eu: false
                      region: "Buenos Aires"
                      region_code: "B"
                      city: "Buenos Aires"
                      postal: null
                      latitude: -34.6037
                      longitude: -58.3816
                      timezone: "America/Argentina/Buenos_Aires"
                      accuracy_radius_km: 50
                      confidence: "medium"
                    network:
                      asn: 10481
                      as_org: "Telecom Argentina S.A."
                      isp: "Telecom Argentina"
                      organization: "Telecom Argentina S.A."
                      network: "200.32.0.0/19"
                    traits:
                      is_anycast: false
                      is_hosting: false
                      is_mobile: false
                      is_satellite: false
                      is_bogon: false
                      is_proxy: false
                      is_vpn: false
                      is_tor: false
                      is_relay: false
                    meta:
                      data_version: "2026-09-20"
                      sources: ["rir", "licensed_geo", "public_blocklists"]
                      cached: false
        "400":
          $ref: "#/components/responses/InvalidIp"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/Internal"

  /v0/ip/{ip}/geo:
    get:
      tags: [IP Lookup]
      operationId: getIpGeo
      summary: Geo-only lookup
      description: Smaller payload for CDN/edge — `location` + `ip` + `meta` only.
      parameters:
        - $ref: "#/components/parameters/IpPath"
        - $ref: "#/components/parameters/ApiKeyQueryOptional"
      responses:
        "200":
          description: Geo-only result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IpGeo"
              examples:
                google_dns:
                  summary: 8.8.8.8 geo
                  value:
                    ip: "8.8.8.8"
                    location:
                      continent_code: "NA"
                      country_code: "US"
                      country: "United States"
                      is_eu: false
                      region: "California"
                      region_code: "CA"
                      city: "Mountain View"
                      postal: "94043"
                      latitude: 37.4056
                      longitude: -122.0775
                      timezone: "America/Los_Angeles"
                      accuracy_radius_km: 1000
                      confidence: "low"
                    meta:
                      data_version: "2026-09-20"
                      sources: ["rir", "licensed_geo"]
                      cached: false
                latam_ip:
                  summary: 200.32.0.1 geo (LATAM)
                  value:
                    ip: "200.32.0.1"
                    location:
                      continent_code: "SA"
                      country_code: "AR"
                      country: "Argentina"
                      is_eu: false
                      region: "Buenos Aires"
                      region_code: "B"
                      city: "Buenos Aires"
                      postal: null
                      latitude: -34.6037
                      longitude: -58.3816
                      timezone: "America/Argentina/Buenos_Aires"
                      accuracy_radius_km: 50
                      confidence: "medium"
                    meta:
                      data_version: "2026-09-20"
                      sources: ["rir", "licensed_geo"]
                      cached: false
        "400":
          $ref: "#/components/responses/InvalidIp"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/Internal"

  /v0/ip/{ip}/network:
    get:
      tags: [IP Lookup]
      operationId: getIpNetwork
      summary: Network / ASN lookup
      description: ASN, org, ISP, and prefix for an IP — no geo payload.
      parameters:
        - $ref: "#/components/parameters/IpPath"
        - $ref: "#/components/parameters/ApiKeyQueryOptional"
      responses:
        "200":
          description: Network-only result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IpNetwork"
              examples:
                google_dns:
                  summary: 8.8.8.8 network
                  value:
                    ip: "8.8.8.8"
                    network:
                      asn: 15169
                      as_org: "Google LLC"
                      isp: "Google LLC"
                      organization: "Google LLC"
                      network: "8.8.8.0/24"
                      rir: "ARIN"
                    meta:
                      data_version: "2026-09-20"
                latam_ip:
                  summary: 200.32.0.1 network (LATAM)
                  value:
                    ip: "200.32.0.1"
                    network:
                      asn: 10481
                      as_org: "Telecom Argentina S.A."
                      isp: "Telecom Argentina"
                      organization: "Telecom Argentina S.A."
                      network: "200.32.0.0/19"
                      rir: "LACNIC"
                    meta:
                      data_version: "2026-09-20"
        "400":
          $ref: "#/components/responses/InvalidIp"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/Internal"

  /v0/asn/{asn}:
    get:
      tags: [ASN]
      operationId: getAsn
      summary: ASN detail
      description: Autonomous System name, org, type, RIR, and sample prefixes.
      parameters:
        - name: asn
          in: path
          required: true
          description: ASN number (with or without `AS` prefix)
          schema:
            oneOf:
              - type: integer
                minimum: 0
                example: 15169
              - type: string
                pattern: "^(AS|as)?[0-9]+$"
                example: "AS15169"
          examples:
            numeric:
              value: 15169
            prefixed:
              value: "AS15169"
        - $ref: "#/components/parameters/ApiKeyQueryOptional"
      responses:
        "200":
          description: ASN details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AsnDetail"
              examples:
                google:
                  summary: AS15169 Google
                  value:
                    asn: 15169
                    name: "GOOGLE"
                    org: "Google LLC"
                    domain: "google.com"
                    type: "hosting"
                    country_code: "US"
                    rir: "ARIN"
                    prefixes_sample: ["8.8.8.0/24", "8.8.4.0/24"]
                    meta:
                      data_version: "2026-09-20"
                telecom_ar:
                  summary: AS10481 Telecom Argentina
                  value:
                    asn: 10481
                    name: "TELECOM-ARGENTINA"
                    org: "Telecom Argentina S.A."
                    domain: "telecom.com.ar"
                    type: "isp"
                    country_code: "AR"
                    rir: "LACNIC"
                    prefixes_sample: ["200.32.0.0/19", "200.42.0.0/16"]
                    meta:
                      data_version: "2026-09-20"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/Internal"

  /v0/bulk:
    post:
      tags: [Bulk]
      operationId: postBulk
      summary: Bulk IP lookup
      description: |
        Batch lookup up to **100 IPs** per request in v0.
        Returns a map of results keyed by IP; per-IP failures appear under `errors`
        (partial success — one bad IP does not fail the whole batch).

        **Quota:** counts as **1** request toward the per-minute rate limit, but **N**
        (number of IPs in the body) toward the daily UTC quota.
      parameters:
        - $ref: "#/components/parameters/ApiKeyQueryOptional"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkRequest"
            examples:
              mixed:
                summary: Google DNS + Cloudflare + LATAM
                value:
                  ips: ["8.8.8.8", "1.1.1.1", "200.32.0.1"]
      responses:
        "200":
          description: Bulk results (partial success possible)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkResponse"
              examples:
                success:
                  summary: Three successful lookups
                  value:
                    results:
                      "8.8.8.8":
                        ip: "8.8.8.8"
                        version: "v0"
                        location:
                          continent_code: "NA"
                          country_code: "US"
                          country: "United States"
                          is_eu: false
                          region: "California"
                          region_code: "CA"
                          city: "Mountain View"
                          postal: "94043"
                          latitude: 37.4056
                          longitude: -122.0775
                          timezone: "America/Los_Angeles"
                          accuracy_radius_km: 1000
                          confidence: "low"
                        network:
                          asn: 15169
                          as_org: "Google LLC"
                          isp: "Google LLC"
                          organization: "Google LLC"
                          network: "8.8.8.0/24"
                        traits:
                          is_anycast: true
                          is_hosting: true
                          is_mobile: false
                          is_satellite: false
                          is_bogon: false
                          is_proxy: false
                          is_vpn: false
                          is_tor: false
                          is_relay: false
                        meta:
                          data_version: "2026-09-20"
                          sources: ["rir", "licensed_geo", "public_blocklists"]
                          cached: false
                      "1.1.1.1":
                        ip: "1.1.1.1"
                        version: "v0"
                        location:
                          continent_code: "OC"
                          country_code: "AU"
                          country: "Australia"
                          is_eu: false
                          region: null
                          region_code: null
                          city: null
                          postal: null
                          latitude: -33.494
                          longitude: 143.2104
                          timezone: "Australia/Sydney"
                          accuracy_radius_km: 1000
                          confidence: "low"
                        network:
                          asn: 13335
                          as_org: "Cloudflare, Inc."
                          isp: "Cloudflare"
                          organization: "APNIC and Cloudflare DNS Resolver Project"
                          network: "1.1.1.0/24"
                        traits:
                          is_anycast: true
                          is_hosting: true
                          is_mobile: false
                          is_satellite: false
                          is_bogon: false
                          is_proxy: false
                          is_vpn: false
                          is_tor: false
                          is_relay: false
                        meta:
                          data_version: "2026-09-20"
                          sources: ["rir", "licensed_geo", "public_blocklists"]
                          cached: false
                      "200.32.0.1":
                        ip: "200.32.0.1"
                        version: "v0"
                        location:
                          continent_code: "SA"
                          country_code: "AR"
                          country: "Argentina"
                          is_eu: false
                          region: "Buenos Aires"
                          region_code: "B"
                          city: "Buenos Aires"
                          postal: null
                          latitude: -34.6037
                          longitude: -58.3816
                          timezone: "America/Argentina/Buenos_Aires"
                          accuracy_radius_km: 50
                          confidence: "medium"
                        network:
                          asn: 10481
                          as_org: "Telecom Argentina S.A."
                          isp: "Telecom Argentina"
                          organization: "Telecom Argentina S.A."
                          network: "200.32.0.0/19"
                        traits:
                          is_anycast: false
                          is_hosting: false
                          is_mobile: false
                          is_satellite: false
                          is_bogon: false
                          is_proxy: false
                          is_vpn: false
                          is_tor: false
                          is_relay: false
                        meta:
                          data_version: "2026-09-20"
                          sources: ["rir", "licensed_geo", "public_blocklists"]
                          cached: false
                    errors: {}
        "400":
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "INVALID_IP"
                  message: "Request must include an 'ips' array with 1–100 IP addresses"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/Internal"

  /v0/me:
    get:
      tags: [Account]
      operationId: getMe
      summary: Caller IP and account info
      description: |
        Returns the caller's apparent IP (from the request) plus plan/quota hints
        when authenticated. Useful for "what's my IP" and key validation.
      parameters:
        - $ref: "#/components/parameters/ApiKeyQueryOptional"
      responses:
        "200":
          description: Caller context
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
              examples:
                authenticated:
                  summary: Authenticated caller
                  value:
                    ip: "200.32.0.1"
                    plan: "free"
                    requests_remaining_today: 847
                    meta:
                      data_version: "2026-09-20"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/Internal"

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |
        Prefer `Authorization: Bearer ndl_live_xxx`.
        Keys look like `ndl_live_*` (production) or `ndl_test_*` (sandbox).
    ApiKeyQuery:
      type: apiKey
      in: query
      name: api_key
      description: Optional alternative to Bearer — useful for quick curl tests. Prefer header in production.

  parameters:
    IpPath:
      name: ip
      in: path
      required: true
      description: IPv4 or IPv6 address
      schema:
        type: string
        examples:
          - "8.8.8.8"
          - "200.32.0.1"
          - "2001:4860:4860::8888"
    ApiKeyQueryOptional:
      name: api_key
      in: query
      required: false
      description: Optional API key (if not using Bearer header)
      schema:
        type: string
        example: "ndl_live_xxx"

  responses:
    InvalidIp:
      description: Malformed or unsupported IP address
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "INVALID_IP"
              message: "Malformed IP address"
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "UNAUTHORIZED"
              message: "Missing or invalid API key"
    RateLimited:
      description: |
        Burst (per-minute) or daily quota exceeded.
        Per-minute → `RATE_LIMITED`; daily UTC quota → `QUOTA_EXCEEDED`.
      headers:
        Retry-After:
          description: Seconds until quota resets (when known)
          schema:
            type: integer
        X-RateLimit-Limit:
          description: Per-minute request limit
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Remaining requests in the current minute window
          schema:
            type: integer
        X-RateLimit-Limit-Day:
          description: Daily quota (calendar UTC day)
          schema:
            type: integer
        X-RateLimit-Remaining-Day:
          description: Remaining daily quota units
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            rate_limited:
              summary: Per-minute burst exceeded
              value:
                error:
                  code: "RATE_LIMITED"
                  message: "Rate limit exceeded (60 requests per minute). Retry after 12s."
            quota_exceeded:
              summary: Daily UTC quota exceeded
              value:
                error:
                  code: "QUOTA_EXCEEDED"
                  message: "Daily quota exceeded (1000 requests per UTC calendar day). Resets in 3600s (UTC midnight)."
    NotFound:
      description: Resource not found (e.g. unknown ASN)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "NOT_FOUND"
              message: "ASN not found"
    Internal:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "INTERNAL"
              message: "Internal server error"

  schemas:
    Location:
      type: object
      required:
        - continent_code
        - country_code
        - country
        - accuracy_radius_km
        - confidence
      properties:
        continent_code:
          type: string
          description: Continent code (e.g. NA, SA, EU)
          examples: ["NA", "SA"]
        country_code:
          type: ["string", "null"]
          description: ISO-3166-1 alpha-2
          examples: ["US", "AR"]
        country:
          type: ["string", "null"]
          examples: ["United States", "Argentina"]
        is_eu:
          type: boolean
          description: Whether the country is in the European Union
        region:
          type: ["string", "null"]
          examples: ["California", "Buenos Aires"]
        region_code:
          type: ["string", "null"]
          examples: ["CA", "B"]
        city:
          type: ["string", "null"]
          examples: ["Mountain View", "Buenos Aires"]
        postal:
          type: ["string", "null"]
        latitude:
          type: ["number", "null"]
          format: double
        longitude:
          type: ["number", "null"]
          format: double
        timezone:
          type: ["string", "null"]
          description: IANA timezone
          examples: ["America/Los_Angeles", "America/Argentina/Buenos_Aires"]
        accuracy_radius_km:
          type: ["number", "null"]
          description: |
            Estimated accuracy radius in kilometers. Always present when geo is returned —
            this is our honesty signal. Wide radii (e.g. 1000) mean city is a best-effort guess.
          examples: [50, 1000]
        confidence:
          type: string
          enum: [high, medium, low, unknown]
          description: Qualitative confidence in the geo estimate

    Network:
      type: object
      properties:
        asn:
          type: ["integer", "null"]
          examples: [15169, 10481]
        as_org:
          type: ["string", "null"]
          examples: ["Google LLC", "Telecom Argentina S.A."]
        isp:
          type: ["string", "null"]
        organization:
          type: ["string", "null"]
        network:
          type: ["string", "null"]
          description: Containing prefix (CIDR)
          examples: ["8.8.8.0/24", "200.32.0.0/19"]
        rir:
          type: ["string", "null"]
          description: Regional Internet Registry (present on /network endpoint)
          examples: ["ARIN", "LACNIC", "RIPE", "APNIC", "AFRINIC"]

    Traits:
      type: object
      required:
        - is_hosting
        - is_proxy
        - is_vpn
        - is_tor
        - is_bogon
      properties:
        is_anycast:
          type: boolean
        is_hosting:
          type: boolean
          description: Likely datacenter / hosting ASN or range
        is_mobile:
          type: boolean
        is_satellite:
          type: boolean
        is_bogon:
          type: boolean
          description: Unallocated / reserved / bogon space
        is_proxy:
          type: boolean
        is_vpn:
          type: boolean
          description: Known public VPN exit (best-effort; may have false negatives)
        is_tor:
          type: boolean
        is_relay:
          type: boolean
          description: Known privacy relay (e.g. iCloud Private Relay style)

    Meta:
      type: object
      required:
        - data_version
      properties:
        data_version:
          type: string
          description: Dataset build date (YYYY-MM-DD)
          examples: ["2026-09-20"]
        sources:
          type: array
          items:
            type: string
          examples:
            - ["rir", "licensed_geo", "public_blocklists"]
        cached:
          type: boolean
          description: Whether this response was served from cache

    IpLookup:
      type: object
      required: [ip, version, location, network, traits, meta]
      properties:
        ip:
          type: string
        version:
          type: string
          const: "v0"
        location:
          $ref: "#/components/schemas/Location"
        network:
          $ref: "#/components/schemas/Network"
        traits:
          $ref: "#/components/schemas/Traits"
        meta:
          $ref: "#/components/schemas/Meta"

    IpGeo:
      type: object
      required: [ip, location, meta]
      properties:
        ip:
          type: string
        location:
          $ref: "#/components/schemas/Location"
        meta:
          $ref: "#/components/schemas/Meta"

    IpNetwork:
      type: object
      required: [ip, network, meta]
      properties:
        ip:
          type: string
        network:
          $ref: "#/components/schemas/Network"
        meta:
          $ref: "#/components/schemas/Meta"

    AsnDetail:
      type: object
      required: [asn, name, org, meta]
      properties:
        asn:
          type: integer
        name:
          type: string
          examples: ["GOOGLE"]
        org:
          type: string
        domain:
          type: ["string", "null"]
        type:
          type: ["string", "null"]
          description: Coarse ASN category
          enum: [hosting, isp, education, government, business, unknown, null]
        country_code:
          type: ["string", "null"]
        rir:
          type: ["string", "null"]
        prefixes_sample:
          type: array
          items:
            type: string
          description: Sample of announced prefixes (not exhaustive)
        meta:
          $ref: "#/components/schemas/Meta"

    BulkRequest:
      type: object
      required: [ips]
      properties:
        ips:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
          description: IPv4/IPv6 addresses to look up (max 100 in v0)
          examples:
            - ["8.8.8.8", "1.1.1.1", "200.32.0.1"]

    BulkResponse:
      type: object
      required: [results, errors]
      properties:
        results:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/IpLookup"
          description: Map of IP → full lookup result
        errors:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/ErrorBody"
          description: Map of IP → error for failed lookups (empty object if all succeeded)

    Me:
      type: object
      required: [ip, meta]
      properties:
        ip:
          type: string
          description: Apparent client IP
        plan:
          type: string
          examples: ["free", "starter", "growth", "scale"]
        requests_remaining_today:
          type: ["integer", "null"]
        meta:
          $ref: "#/components/schemas/Meta"

    ErrorBody:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum:
            - INVALID_IP
            - UNAUTHORIZED
            - RATE_LIMITED
            - QUOTA_EXCEEDED
            - NOT_FOUND
            - INTERNAL
        message:
          type: string

    Error:
      type: object
      required: [error]
      properties:
        error:
          $ref: "#/components/schemas/ErrorBody"
