openapi: 3.1.0

info:
  title: theindex.fyi
  version: '1.0'
  description: |
    A public, read-only API for theindex.fyi — a maintained meta-index of indie web
    and small web index sites.

    All responses follow the [JSON:API 1.1](https://jsonapi.org/) specification and use
    the `application/vnd.api+json` content type.

    **Rate limiting:** 60 requests per minute per IP. Responses include `X-RateLimit-Limit`,
    `X-RateLimit-Remaining`, and `Retry-After` headers.

    **Versioning:** The API is currently unversioned. If a breaking change is ever necessary,
    versioning will be introduced via an `API-Version` request header rather than URL prefixes.
  contact:
    url: https://theindex.fyi/about

servers:
  - url: https://theindex.fyi/api
    description: Production

tags:
  - name: Indexes
    description: Approved indie web and small web index sites.

paths:

  /indexes:
    get:
      operationId: listIndexes
      summary: List indexes
      description: Returns a paginated, filterable list of all active indexes.
      tags: [Indexes]
      parameters:
        - $ref: '#/components/parameters/FilterCategory'
        - $ref: '#/components/parameters/FilterLanguage'
        - $ref: '#/components/parameters/PageNumber'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: A paginated collection of index resources.
          content:
            application/vnd.api+json:
              schema:
                $ref: '#/components/schemas/IndexCollection'
              example:
                data:
                  - type: indexes
                    id: '1'
                    attributes:
                      name: ooh.directory
                      slug: ooh-directory
                      url: https://ooh.directory
                      description: A curated directory of personal websites and blogs.
                      category: curated_directories
                      language: null
                      accepts_submissions: true
                      last_checked_at: '2026-05-01T12:00:00Z'
                    links:
                      self: https://theindex.fyi/api/indexes/ooh-directory
                meta:
                  total: 84
                  per_page: 25
                  current_page: 1
                  last_page: 4
                links:
                  self: https://theindex.fyi/api/indexes?page[number]=1
                  first: https://theindex.fyi/api/indexes?page[number]=1
                  last: https://theindex.fyi/api/indexes?page[number]=4
                  prev: null
                  next: https://theindex.fyi/api/indexes?page[number]=2
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /indexes/{slug}:
    get:
      operationId: getIndex
      summary: Get an index
      description: Returns a single index by its slug.
      tags: [Indexes]
      parameters:
        - name: slug
          in: path
          required: true
          description: The unique slug of the index.
          schema:
            type: string
          example: ooh-directory
      responses:
        '200':
          description: A single index resource.
          content:
            application/vnd.api+json:
              schema:
                $ref: '#/components/schemas/IndexResource'
              example:
                data:
                  type: indexes
                  id: '1'
                  attributes:
                    name: ooh.directory
                    slug: ooh-directory
                    url: https://ooh.directory
                    description: A curated directory of personal websites and blogs.
                    category: curated_directories
                    language: null
                    accepts_submissions: true
                    last_checked_at: '2026-05-01T12:00:00Z'
                  links:
                    self: https://theindex.fyi/api/indexes/ooh-directory
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

components:

  parameters:

    FilterCategory:
      name: filter[category]
      in: query
      required: false
      description: Filter indexes by category.
      schema:
        type: string
        enum:
          - curated_directories
          - rss_feed_aggregators
          - search_engines
          - random_discovery
          - constraint_based_clubs
          - indieweb_infrastructure
      example: curated_directories

    FilterLanguage:
      name: filter[language]
      in: query
      required: false
      description: Filter by ISO 639-1 language code. Omit to return all languages.
      schema:
        type: string
        minLength: 2
        maxLength: 2
      example: es

    PageNumber:
      name: page[number]
      in: query
      required: false
      description: Page number (1-based).
      schema:
        type: integer
        minimum: 1
        default: 1

    PageSize:
      name: page[size]
      in: query
      required: false
      description: Number of results per page. Maximum 100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25

  schemas:

    IndexAttributes:
      type: object
      required:
        - name
        - slug
        - url
        - description
        - category
        - accepts_submissions
      properties:
        name:
          type: string
          description: Display name of the index.
          example: ooh.directory
        slug:
          type: string
          description: URL-safe unique identifier.
          example: ooh-directory
        url:
          type: string
          format: uri
          description: The index's URL.
          example: https://ooh.directory
        description:
          type: string
          description: One-sentence description of what the index is and who it's for.
          example: A curated directory of personal websites and blogs.
        category:
          type: string
          description: The category this index belongs to.
          enum:
            - curated_directories
            - rss_feed_aggregators
            - search_engines
            - random_discovery
            - constraint_based_clubs
            - indieweb_infrastructure
          example: curated_directories
        language:
          type: ['string', 'null']
          description: ISO 639-1 language code of the index's primary language. Null if English or unknown.
          example: es
        accepts_submissions:
          type: boolean
          description: Whether this index accepts submissions from the public.
          example: true
        last_checked_at:
          type: ['string', 'null']
          format: date-time
          description: When the link was last verified as live.
          example: '2026-05-01T12:00:00Z'

    IndexResourceObject:
      type: object
      required: [type, id, attributes, links]
      properties:
        type:
          type: string
          enum: [indexes]
        id:
          type: string
          description: Unique numeric ID as a string, per JSON:API spec.
          example: '1'
        attributes:
          $ref: '#/components/schemas/IndexAttributes'
        links:
          type: object
          required: [self]
          properties:
            self:
              type: string
              format: uri
              example: https://theindex.fyi/api/indexes/ooh-directory

    IndexResource:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/IndexResourceObject'

    IndexCollection:
      type: object
      required: [data, meta, links]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/IndexResourceObject'
        meta:
          type: object
          required: [total, per_page, current_page, last_page]
          properties:
            total:
              type: integer
              description: Total number of matching indexes across all pages.
              example: 84
            per_page:
              type: integer
              example: 25
            current_page:
              type: integer
              example: 1
            last_page:
              type: integer
              example: 4
        links:
          type: object
          required: [self, first, last]
          properties:
            self:
              type: string
              format: uri
            first:
              type: string
              format: uri
            last:
              type: string
              format: uri
            prev:
              type: ['string', 'null']
              format: uri
            next:
              type: ['string', 'null']
              format: uri

    ErrorObject:
      type: object
      required: [status, title]
      properties:
        status:
          type: string
          description: HTTP status code as a string.
          example: '404'
        title:
          type: string
          description: Short, human-readable summary of the problem.
          example: Not Found
        detail:
          type: string
          description: Human-readable explanation specific to this occurrence.
          example: No index exists with the slug "not-a-real-index".

    ErrorDocument:
      type: object
      required: [errors]
      properties:
        errors:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ErrorObject'

  responses:

    NotFound:
      description: The requested resource does not exist.
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/ErrorDocument'
          example:
            errors:
              - status: '404'
                title: Not Found
                detail: No index exists with the slug "not-a-real-index".

    TooManyRequests:
      description: Rate limit exceeded. Wait for the `Retry-After` period before retrying.
      headers:
        Retry-After:
          description: Seconds until the rate limit resets.
          schema:
            type: integer
          example: 42
        X-RateLimit-Limit:
          description: Maximum requests allowed per minute.
          schema:
            type: integer
          example: 60
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
          example: 0
      content:
        application/vnd.api+json:
          schema:
            $ref: '#/components/schemas/ErrorDocument'
          example:
            errors:
              - status: '429'
                title: Too Many Requests
                detail: Rate limit of 60 requests per minute exceeded. Please slow down.
