openapi: 3.1.0
info:
  title: inetGeek public feeds
  version: 2026-10-25
  summary: Sourced, dated infrastructure facts, scores, plans and live DNS checks. Free, unauthenticated, read-only.
  description: |
    Everything here is published at https://inetgeek.com and needs no account or key.
    Every fact carries the vendor page it was read from, the sentence it was read from
    and the date it was last checked against that page. Where a figure is blank, the
    feeds keep three reasons apart: not documented, not offered, and not stated on a
    vendor page read on a stated date (`/ledger.json`).

    **Rate limits.** The three live DNS endpoints are limited per IP address; a
    limited call answers `429` with `Retry-After`. The static feeds are cached at the
    edge for an hour and are not limited.

    **Data licence.** The MCP server's code is MIT; the data these feeds serve is not
    part of that licence. Each figure is attributed to its vendor page. Corrections:
    https://inetgeek.com/contact/.

    **MCP.** The same data is exposed to AI assistants as an MCP server; see the MCP
    section of these docs. This reference covers the plain HTTP feeds.
  license:
    name: Data not licensed for redistribution without attribution; server code MIT
    url: https://inetgeek.com/methodology/
  contact:
    name: inetGeek corrections and requests
    url: https://inetgeek.com/contact/
servers:
  - url: https://inetgeek.com
    description: Production
tags:
  - name: Providers
    x-displayName: Providers
    description: Sourced provider facts, in three sizes.
  - name: Scores and plans
    x-displayName: Scores and plans
    description: iScore, the stack planner and per-volume prices.
  - name: Evidence
    x-displayName: Evidence
    description: What was read and found silent, what changed, and where a vendor's pages disagree.
  - name: Pages
    x-displayName: Pages
    description: The Markdown mirror of every published page.
  - name: DNS
    x-displayName: Live DNS
    description: Live lookups against public resolvers. Nothing queried is stored.
paths:
  /providers/core.json:
    get:
      operationId: getProvidersCore
      tags: [Providers]
      summary: Every provider with its facts, without the evidence text
      description: |
        The size most callers want: each fact's value, source URL and verification date,
        but not the verbatim excerpt behind it. About 1.6 MB. `display` is present only
        where it differs from `value`.
      responses:
        '200':
          description: The provider feed, core view.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProvidersFeed' }
  /providers.json:
    get:
      operationId: getProvidersFull
      tags: [Providers]
      summary: Every provider with the full fact envelope
      description: |
        As the core view, plus the verbatim `excerpt` and `source_title` behind every
        fact. About 3 MB; use `/providers/core.json` unless you need the evidence.
      responses:
        '200':
          description: The provider feed, full view.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProvidersFeed' }
  /providers/keys.json:
    get:
      operationId: getProvidersKeys
      tags: [Providers]
      summary: Which facts each provider documents, and nothing about what they say
      description: "The smallest view: for each provider, the list of fact keys it documents."
      responses:
        '200':
          description: Fact keys per provider.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProvidersKeysFeed' }
  /scores.json:
    get:
      operationId: getScores
      tags: [Scores and plans]
      summary: iScore per provider per category, with intervals and pillars
      description: |
        Always read `low` and `high` with `overall`. A rank is an ordering, not a
        finding, unless `distinct` is true. `readingNotes` says in words which pillar
        is thin for that provider.
      responses:
        '200':
          description: Scores grouped by category id.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScoresFeed' }
  /plan.json:
    get:
      operationId: getPlan
      tags: [Scores and plans]
      summary: Planner data — one pick per infrastructure layer, plus every ranked candidate
      description: |
        Price is the only ranking here, so read `pickBasis`: `price` means the pick is
        genuinely cheaper than the rest of its layer, `tie` means every name in
        `coEqual` documents the same starting price and the pick was separated by
        nothing but the alphabet. `candidates` lists every provider per layer in rank
        order, with a `vendor` key for matching a platform.
      responses:
        '200':
          description: The planner feed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlanFeed' }
  /usage-pricing.json:
    get:
      operationId: getUsagePricing
      tags: [Scores and plans]
      summary: Plans priced by volume, one row per plan or band
      description: |
        Pageviews, events, emails or monthly active users a month, with the plan fee,
        allowance, overage and source. A row with `quote_above` carries no price: the
        vendor sells above that volume by quote. Any total computed from these rows is
        arithmetic on published rates, not a quote.
      responses:
        '200':
          description: Volume-priced plans.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UsagePricingFeed' }
  /startup-credits.json:
    get:
      operationId: getStartupCredits
      tags: [Scores and plans]
      summary: Providers that publish a startup credit programme
      responses:
        '200':
          description: Startup credit programmes as stated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StartupCreditsFeed' }
  /ledger.json:
    get:
      operationId: getLedger
      tags: [Evidence]
      summary: Pages read and found silent, criteria a product cannot have, vendor-page disagreements, changes
      description: |
        `not_stated` means: on `checked_at`, the page at `page_read` did not state this
        criterion. It does not mean the provider lacks it. `source_disagreements` are
        places where two pages of one vendor contradict each other; no value is
        recorded for those criteria.
      responses:
        '200':
          description: The ledger.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LedgerFeed' }
  /changelog.json:
    get:
      operationId: getChangelog
      tags: [Evidence]
      summary: Every recorded change to a published fact
      responses:
        '200':
          description: Changes, newest first, with the old and new value and the date.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChangelogFeed' }
  /search-index.json:
    get:
      operationId: getSearchIndex
      tags: [Pages]
      summary: Every published page with its title and description
      responses:
        '200':
          description: The site search index.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchIndex' }
  /{section}/{slug}.md:
    get:
      operationId: getProviderPage
      tags: [Pages]
      summary: One provider's fact sheet as Markdown
      description: |
        The sourced fact sheet for one provider, with its iScore and the reading notes
        for it. `section` is the URL section (`hosting`, `databases`, `analytics`,
        `docs-platforms`, …); `GET /providers/core.json` lists every provider's `path`.
      parameters:
        - name: section
          in: path
          required: true
          schema: { type: string, examples: [analytics] }
        - name: slug
          in: path
          required: true
          schema: { type: string, examples: [plausible] }
      responses:
        '200':
          description: Markdown.
          content:
            text/markdown:
              schema: { type: string }
        '404':
          description: No such page.
  /compare/{pair}.md:
    get:
      operationId: getComparison
      tags: [Pages]
      summary: A sourced side-by-side for a curated pair, as Markdown
      description: Only curated pairs exist. The pair order is the one in the page URL, for example `plausible-vs-umami`.
      parameters:
        - name: pair
          in: path
          required: true
          schema: { type: string, examples: [plausible-vs-umami] }
      responses:
        '200':
          description: Markdown.
          content:
            text/markdown:
              schema: { type: string }
        '404':
          description: No such comparison. This is the answer for a pair that has not been researched, not an error in the request.
  /stacks/{framework}.md:
    get:
      operationId: getStack
      tags: [Pages]
      summary: Hosts that document real support for a framework, as Markdown
      parameters:
        - name: framework
          in: path
          required: true
          schema: { type: string, examples: [astro] }
      responses:
        '200':
          description: Markdown.
          content:
            text/markdown:
              schema: { type: string }
        '404':
          description: No recommendation page for that framework.
  /api/dns:
    get:
      operationId: checkDns
      tags: [DNS]
      summary: A live DNS lookup for a domain
      description: A, AAAA, CNAME, MX, NS records plus SPF, DMARC and DNSSEC state, from a public resolver. Nothing is stored.
      parameters:
        - $ref: '#/components/parameters/Domain'
      responses:
        '200':
          description: The lookup.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsReport' }
        '400': { $ref: '#/components/responses/BadDomain' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/ResolverUnavailable' }
  /api/spf:
    get:
      operationId: checkSpf
      tags: [DNS]
      summary: A live SPF evaluation, including the ten-lookup limit
      parameters:
        - $ref: '#/components/parameters/Domain'
      responses:
        '200':
          description: The evaluation, with the include tree.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SpfReport' }
        '400': { $ref: '#/components/responses/BadDomain' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/ResolverUnavailable' }
  /api/propagation:
    get:
      operationId: checkPropagation
      tags: [DNS]
      summary: One record asked of four independent public resolvers
      parameters:
        - $ref: '#/components/parameters/Domain'
        - name: type
          in: query
          required: true
          description: The record type to compare.
          schema:
            type: string
            enum: [A, AAAA, CNAME, MX, TXT, NS]
            examples: [A]
      responses:
        '200':
          description: What each resolver answered, and whether they agree.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PropagationReport' }
        '400': { $ref: '#/components/responses/BadDomain' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/ResolverUnavailable' }
components:
  parameters:
    Domain:
      name: domain
      in: query
      required: true
      description: A hostname or URL; it is normalised to a bare domain.
      schema: { type: string, examples: [example.com] }
  responses:
    BadDomain:
      description: The domain could not be normalised.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Too many lookups from this address.
      headers:
        Retry-After:
          description: Seconds to wait.
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ResolverUnavailable:
      description: No resolver could be reached.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      required: [error, code]
      properties:
        error: { type: string }
        code: { type: string }
    Fact:
      type: object
      description: One sourced value.
      required: [value, source_url, verified_at]
      properties:
        value: { description: 'A string, boolean or number as the page states it. Support-typed fields are exactly "yes", "limited" or "no".' }
        display: { type: string, description: "The rendered value, where it differs from `value`." }
        source_url: { type: string, format: uri }
        source_title: { type: string }
        verified_at: { type: string, format: date }
        confidence: { type: string, enum: [official-pricing, official-docs, official-site, third-party] }
        excerpt: { type: string, description: The verbatim sentence the value was read from. Full view only. }
        qualifier: { type: string }
        magnitude: { type: number, description: "The value as a number in `unit_family`, where a comparable one exists." }
        unit_family: { type: string }
        support: { type: string, enum: [yes, limited, no] }
    Provider:
      type: object
      required: [id, name, category, path, homepage, facts]
      properties:
        id: { type: string }
        name: { type: string }
        category: { type: string, description: The canonical category id. }
        also_in: { type: array, items: { type: string } }
        path: { type: string, description: 'The provider page, with trailing slash; append `.md` for the Markdown mirror (drop the slash).' }
        homepage: { type: string, format: uri }
        description: { type: string }
        best_for: { type: [string, 'null'] }
        consider_alternatives_when: { type: [string, 'null'] }
        reviewed_by: { type: [string, 'null'] }
        unstated:
          type: object
          description: Criteria whose vendor page was read on `checked_at` and did not state them.
          additionalProperties:
            type: object
            properties:
              source_url: { type: string, format: uri }
              checked_at: { type: string, format: date }
        facts:
          type: object
          additionalProperties: { $ref: '#/components/schemas/Fact' }
      additionalProperties: true
    ProvidersFeed:
      type: object
      required: [site, view, count, fact_labels, providers]
      properties:
        site: { type: string }
        view: { type: string, enum: [full, core] }
        views:
          type: object
          properties:
            full: { type: string, format: uri }
            core: { type: string, format: uri }
            keys: { type: string, format: uri }
        description: { type: string }
        count: { type: integer }
        fact_labels:
          type: object
          description: Human label for each fact key.
          additionalProperties: { type: string }
        providers:
          type: array
          items: { $ref: '#/components/schemas/Provider' }
      additionalProperties: true
    ProvidersKeysFeed:
      type: object
      required: [site, view, providers]
      properties:
        site: { type: string }
        view: { type: string, const: keys }
        description: { type: string }
        providers: { type: array, items: { type: object, additionalProperties: true } }
      additionalProperties: true
    ScoresFeed:
      type: object
      required: [site, method, generated_at, pillars, categories]
      properties:
        site: { type: string }
        description: { type: string }
        method: { type: string, format: uri }
        generated_at: { type: string }
        pillars: { type: object, additionalProperties: { type: string } }
        default_weights: { type: object, additionalProperties: { type: number } }
        category_weights: { type: object, additionalProperties: true }
        categories:
          type: object
          description: Scores per category id, best first.
          additionalProperties:
            type: array
            items: { $ref: '#/components/schemas/ProviderScore' }
      additionalProperties: true
    ProviderScore:
      type: object
      required: [id, name, category, overall, low, high, rank, of, distinct]
      properties:
        id: { type: string }
        name: { type: string }
        category: { type: string }
        overall: { type: number, description: 0 to 100. }
        low: { type: number, description: Lower end of the 80% interval. }
        high: { type: number, description: Upper end of the 80% interval. }
        rank: { type: integer, description: 1-based within the category. }
        of: { type: integer }
        coverage: { type: number }
        distinct: { type: boolean, description: True only when the rank is supported against the next provider at 90% or better. }
        caveat: { type: string }
        readingNotes: { type: array, items: { type: string }, description: Why this record's number deserves extra care. }
        weights: { type: object, additionalProperties: { type: number } }
        pillars: { type: object, additionalProperties: true }
      additionalProperties: true
    PlanFeed:
      type: object
      required: [site, layers, results, stages]
      properties:
        site: { type: string }
        description: { type: string }
        layers: { type: array, items: { type: object, required: [key, label], properties: { key: { type: string }, label: { type: string } } } }
        securityRequirementKeys: { type: array, items: { type: string } }
        results: { type: array, items: { $ref: '#/components/schemas/PlanResult' } }
        stages: { type: array, items: { type: object, additionalProperties: true } }
        keywordLayers: { type: object, additionalProperties: { type: array, items: { type: string } } }
        securityCandidates: { type: object, additionalProperties: { type: array, items: { type: object, additionalProperties: true } } }
        candidates:
          type: object
          description: Every ranked provider per layer key.
          additionalProperties:
            type: array
            items: { $ref: '#/components/schemas/PlanCandidate' }
      additionalProperties: true
    PlanResult:
      type: object
      required: [key, label, pickBasis]
      properties:
        key: { type: string }
        label: { type: string }
        requires: { type: array, items: { type: string } }
        providerId: { type: [string, 'null'] }
        providerName: { type: [string, 'null'] }
        providerHref: { type: [string, 'null'] }
        priceLabel: { type: string }
        priceUsd: { type: [number, 'null'] }
        coEqual: { type: array, items: { type: string }, description: Providers the price rule cannot separate from the pick. }
        pickBasis: { type: string, enum: [price, tie, none] }
      additionalProperties: true
    PlanCandidate:
      type: object
      required: [id, name, vendor, priceLabel, href, primary]
      properties:
        id: { type: string }
        name: { type: string }
        vendor: { type: string, description: "Registrable-domain label of the homepage (\"cloudflare\", \"amazon\")." }
        priceUsd: { type: [number, 'null'] }
        priceLabel: { type: string }
        href: { type: string }
        primary: { type: boolean, description: False for a provider that reaches this layer only through also_in. }
    UsagePricingFeed:
      type: object
      required: [site, description, count, providers, plans]
      properties:
        site: { type: string }
        description: { type: string }
        count: { type: integer }
        providers: { type: integer }
        plans: { type: array, items: { $ref: '#/components/schemas/UsagePlan' } }
    UsagePlan:
      type: object
      required: [provider, name, category, path, plan, unit, source_url, verified_at]
      properties:
        provider: { type: string }
        name: { type: string }
        category: { type: string }
        path: { type: string }
        plan: { type: string }
        unit: { type: string, enum: [pageviews, events, emails, mau] }
        currency: { type: string, description: ISO 4217. Rows not in USD are not comparable here. }
        monthly_usd: { type: number, description: 'The recurring fee, in `currency`. Absent on a quote-only row.' }
        one_time_usd: { type: [number, 'null'] }
        included: { type: [number, 'null'], description: "Units a month covered; null when `uncapped` says why." }
        uncapped: { type: [string, 'null'], enum: [unlimited, fair-use, null] }
        quote_above: { type: number, description: 'Present only on a quote-only row: above this volume the vendor sells by quote.' }
        overage_usd_per_unit: { type: [number, 'null'] }
        overage_tiers:
          type: [array, 'null']
          items: { type: object, required: [from, per_unit], properties: { from: { type: number }, per_unit: { type: number } } }
        overage_block:
          type: [object, 'null']
          properties:
            size: { type: number }
            usd: { type: number }
            granularity: { type: string, enum: [unstated, whole, prorated] }
        max_units: { type: [number, 'null'] }
        sites: { type: [number, 'null'] }
        sites_unlimited: { type: boolean }
        term: { type: [string, 'null'] }
        source_url: { type: string, format: uri }
        also_read_from: { type: array, items: { type: string } }
        verified_at: { type: string, format: date }
        note: { type: [string, 'null'] }
    StartupCreditsFeed:
      type: object
      required: [site, description, count, providers]
      properties:
        site: { type: string }
        description: { type: string }
        count: { type: integer }
        providers:
          type: array
          items:
            type: object
            required: [id, name, category, path, program, source_url, verified_at]
            properties:
              id: { type: string }
              name: { type: string }
              category: { type: string }
              path: { type: string }
              program: { type: string }
              source_url: { type: string, format: uri }
              verified_at: { type: string, format: date }
    LedgerFeed:
      type: object
      required: [site, counts, not_stated, not_offered, source_disagreements]
      properties:
        site: { type: string }
        page: { type: string, format: uri }
        description: { type: string }
        corrections: { type: string, format: uri }
        generated_at: { type: string }
        counts: { type: object, additionalProperties: { type: integer } }
        not_stated:
          type: array
          items:
            type: object
            required: [id, name, path, not_stated]
            properties:
              id: { type: string }
              name: { type: string }
              path: { type: string }
              not_stated:
                type: array
                items:
                  type: object
                  required: [criterion, page_read, checked_at]
                  properties:
                    criterion: { type: string }
                    label: { type: string }
                    page_read: { type: string, format: uri }
                    checked_at: { type: string, format: date }
        not_offered: { type: array, items: { type: object, additionalProperties: true } }
        source_disagreements:
          type: array
          items:
            type: object
            required: [id, name, path, disagreements]
            properties:
              id: { type: string }
              name: { type: string }
              path: { type: string }
              disagreements:
                type: array
                items:
                  type: object
                  required: [criterion, summary, pages]
                  properties:
                    criterion: { type: string }
                    label: { type: string }
                    summary: { type: string }
                    pages:
                      type: array
                      items:
                        type: object
                        required: [source_url, excerpt, read_at]
                        properties:
                          source_url: { type: string, format: uri }
                          excerpt: { type: string }
                          read_at: { type: string, format: date }
        recent_changes: { type: array, items: { $ref: '#/components/schemas/ChangelogEntry' } }
      additionalProperties: true
    ChangelogEntry:
      type: object
      required: [kind, id, name, date]
      properties:
        kind: { type: string, description: 'For example value-changed, fact-added, provider-added.' }
        cause: { type: string, description: 'Whether the vendor moved a value or inetGeek corrected its own reading.' }
        id: { type: string }
        name: { type: string }
        field: { type: string }
        from: { description: "The value before the change, as recorded: usually a string, sometimes a boolean or number." }
        to: { description: "The value after the change, as recorded: usually a string, sometimes a boolean or number." }
        source: { type: string }
        verified_at: { type: string }
        date: { type: string, format: date }
        commit: { type: string }
    ChangelogFeed:
      type: object
      required: [site, generated_at, entries]
      properties:
        site: { type: string }
        generated_at: { type: string }
        counts: { type: object, additionalProperties: { type: integer } }
        entries: { type: array, items: { $ref: '#/components/schemas/ChangelogEntry' } }
    SearchIndex:
      type: object
      required: [count, docs]
      properties:
        count: { type: integer }
        docs:
          type: array
          items:
            type: object
            required: [path, title, description, kind]
            properties:
              path: { type: string }
              title: { type: string }
              description: { type: string }
              kind: { type: string }
              keywords: { type: string }
    DnsRecord:
      type: object
      properties:
        value: { type: string }
        ttl: { type: integer }
        priority: { type: integer }
        host: { type: string }
      additionalProperties: true
    DnsReport:
      type: object
      required: [domain, found, checkedAt]
      properties:
        domain: { type: string }
        found: { type: boolean }
        checkedAt: { type: string, format: date-time }
        resolver: { type: string }
        dnssec: { type: string }
        a: { type: array, items: { $ref: '#/components/schemas/DnsRecord' } }
        aaaa: { type: array, items: { $ref: '#/components/schemas/DnsRecord' } }
        cname: { type: array, items: { $ref: '#/components/schemas/DnsRecord' } }
        mx: { type: array, items: { $ref: '#/components/schemas/DnsRecord' } }
        ns: { type: array, items: { $ref: '#/components/schemas/DnsRecord' } }
        spf: { type: [string, 'null'] }
        dmarc: { type: [string, 'null'] }
        ttl: { type: integer, description: Seconds the answer may be cached. }
      additionalProperties: true
    SpfReport:
      type: object
      required: [domain, found, checkedAt]
      properties:
        domain: { type: string }
        found: { type: boolean }
        record: { type: [string, 'null'] }
        records: { type: array, items: { type: string } }
        lookups: { type: integer, description: DNS lookups the record costs; the limit is ten. }
        limit: { type: integer }
        voidLookups: { type: integer }
        voidLimit: { type: integer }
        exceededAt: { type: [string, 'null'] }
        status: { type: string }
        problems: { type: array, items: { type: object, additionalProperties: true } }
        tree: { type: object, additionalProperties: true }
        heaviest: { type: [object, 'null'], additionalProperties: true }
        checkedAt: { type: string, format: date-time }
        ttl: { type: integer }
      additionalProperties: true
    PropagationReport:
      type: object
      required: [domain, type, checkedAt, answers, consistent]
      properties:
        domain: { type: string }
        type: { type: string }
        checkedAt: { type: string, format: date-time }
        answers:
          type: array
          items:
            type: object
            required: [id, label]
            properties:
              id: { type: string }
              label: { type: string }
              values: { type: array, items: { type: string } }
              error: { type: string }
        consistent: { type: boolean }
        reachable: { type: integer }
      additionalProperties: true
