openapi: 3.1.0
info:
  title: Forum API
  version: "1.0"
  description: Public, read-only access to Attention Index data.
  contact:
    name: Forum Support
    email: contact@forum.market
    url: https://forum.market

servers:
  - url: https://api.forum.market/v1
    description: Production

tags:
  - name: Indices
    description: Attention Index data.
  - name: Source Data
    description: Raw and per-source index values, for approved institutions. Requests are signed with an API key.

paths:
  /indices:
    get:
      operationId: listIndices
      summary: List indices
      description: Returns every Attention Index that is being updated and has published a value, with its display details, latest value and change over the past week, ordered by name.
      tags: [Indices]
      security: []
      responses:
        "200":
          description: The indices.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/IndexSummary"
              example:
                - name: NIKE-IDX
                  slug: NIKE
                  displayName: Nike
                  category: fashion
                  shortDescription: null
                  picture: null
                  currentValue: 54.898168577997346
                  changePercentPastWeek: 0.011409625617612995
        "429":
          $ref: "#/components/responses/RateLimited"

  /indices/{name}:
    get:
      operationId: getIndex
      summary: Get index
      description: Returns an Attention Index's latest value and the sources that make it up, with each source's weight, the metrics it tracks and how its score has changed.
      tags: [Indices]
      security: []
      parameters:
        - $ref: "#/components/parameters/IndexName"
      responses:
        "200":
          description: The index.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Index"
        "404":
          description: No index has this name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: NOT_FOUND
                  message: Index not found
        "429":
          $ref: "#/components/responses/RateLimited"

  /indices/{name}/history:
    get:
      operationId: getIndexHistory
      summary: Get index history
      description: |
        Returns published values of an Attention Index within a time range, oldest first.

        With `interval=raw`, every published value is returned. With a sampled interval, values are grouped into buckets aligned to the Unix epoch in UTC, and the last value in each bucket is returned.

        When more values match than `limit`, the most recent ones are returned. An unknown index name returns an empty array.
      tags: [Indices]
      security: []
      parameters:
        - $ref: "#/components/parameters/IndexName"
        - name: start
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Start of the time range (ISO 8601, inclusive).
          example: "2026-10-12T00:00:00.000Z"
        - name: end
          in: query
          schema:
            type: string
            format: date-time
          description: End of the time range (ISO 8601, inclusive). Defaults to the current time.
        - name: interval
          in: query
          schema:
            type: string
            pattern: '^(raw|8h|[1-9][0-9]*d)$'
            default: raw
          description: Resolution of the returned values. Use `raw` for every published value, `8h`, or a whole number of days such as `1d` or `7d`.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 2500
            default: 2500
          description: Maximum number of values to return.
      responses:
        "200":
          description: Index values, oldest first.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/IndexHistoryRecord"
              example:
                - id: 505595
                  name: FORTNITE-IDX
                  value: 56.576344892425176
                  timestamp: "2026-10-22T20:03:00.047Z"
                  backfillVersion: null
                - id: 505637
                  name: FORTNITE-IDX
                  value: 56.565172976752784
                  timestamp: "2026-10-22T20:33:00.050Z"
                  backfillVersion: null
        "400":
          description: A query parameter is missing or invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: INVALID_PARAMETER
                  message: Request validation failed
                  details:
                    fields:
                      - field: start
                        message: "Invalid input: expected string, received undefined"
        "429":
          $ref: "#/components/responses/RateLimited"

  /indices/{name}/history/sources:
    get:
      operationId: getIndexSourceHistory
      summary: Get source history
      description: |
        Returns published values of an Attention Index within a time range, oldest first, with the raw index value and each source's score.

        `start`, `end`, `interval` and `limit` work as in `GET /indices/{name}/history`. An unknown index name returns an empty array.

        Requires an API key whose owner belongs to an approved institution. See [Authentication](/api-reference/authentication).
      tags: [Source Data]
      security:
        - AccessKey: []
          AccessTimestamp: []
          AccessSign: []
      parameters:
        - $ref: "#/components/parameters/IndexName"
        - name: start
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Start of the time range (ISO 8601, inclusive).
          example: "2026-10-12T00:00:00.000Z"
        - name: end
          in: query
          schema:
            type: string
            format: date-time
          description: End of the time range (ISO 8601, inclusive). Defaults to the current time.
        - name: interval
          in: query
          schema:
            type: string
            pattern: '^(raw|8h|[1-9][0-9]*d)$'
            default: raw
          description: Resolution of the returned values. Use `raw` for every published value, `8h`, or a whole number of days such as `1d` or `7d`.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 2500
            default: 2500
          description: Maximum number of values to return. When more values match, the most recent ones are returned.
      responses:
        "200":
          description: Index values with their sources, oldest first.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit-Account"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/IndexSourceHistoryRecord"
              example:
                - id: 505595
                  name: FORTNITE-IDX
                  value: 56.576344892425176
                  raw: 56.21093815276502
                  timestamp: "2026-10-22T20:03:00.047Z"
                  scheduledAt: "2026-10-22T20:03:00.000Z"
                  sourceValues:
                    youtube: 6.012477350238114
                    reddit: 4.871903257091735
                    x: 5.103388224510987
                    google_trends: 5.794126107344519
                  backfillVersion: null
        "400":
          $ref: "#/components/responses/InvalidParameter"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/AccountRateLimited"
        "503":
          $ref: "#/components/responses/AccessCheckUnavailable"

  /indices/{name}/history/export:
    get:
      operationId: exportIndexHistory
      summary: Export full history
      description: |
        Returns every value an Attention Index has published, with its raw value and source scores, in pages of up to 2,500 ordered by `id`.

        Start with `afterId=0`. The first page fixes `throughId`, the last `id` the export covers. Request the next page with the returned `nextAfterId` and `throughId` until `nextAfterId` is null. Values published after the export started are left out, so pages stay consistent.

        IDs increase over time but are shared across indices, so they are not consecutive. Backfilled values can have higher IDs than live values with earlier timestamps; sort by `timestamp` if you need time order. An unknown index name returns an empty page.

        Requires an API key whose owner belongs to an approved institution. See [Authentication](/api-reference/authentication).
      tags: [Source Data]
      security:
        - AccessKey: []
          AccessTimestamp: []
          AccessSign: []
      parameters:
        - $ref: "#/components/parameters/IndexName"
        - name: afterId
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Return values with an `id` greater than this. Use `0` for the first page, then the previous page's `nextAfterId`.
        - name: throughId
          in: query
          schema:
            type: integer
            minimum: 0
          description: Return values with an `id` up to and including this. Omit on the first page, then pass the `throughId` it returned.
      responses:
        "200":
          description: A page of index values, ordered by `id`.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/X-RateLimit-Limit-Account"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/X-RateLimit-Remaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/X-RateLimit-Reset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IndexHistoryExportPage"
              example:
                history:
                  - id: 505595
                    name: FORTNITE-IDX
                    value: 56.576344892425176
                    raw: 56.21093815276502
                    timestamp: "2026-10-22T20:03:00.047Z"
                    scheduledAt: "2026-10-22T20:03:00.000Z"
                    sourceValues:
                      youtube: 6.012477350238114
                      reddit: 4.871903257091735
                      x: 5.103388224510987
                      google_trends: 5.794126107344519
                    backfillVersion: null
                throughId: 512840
                nextAfterId: 505595
        "400":
          $ref: "#/components/responses/InvalidParameter"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/AccountRateLimited"
        "503":
          $ref: "#/components/responses/AccessCheckUnavailable"

components:
  securitySchemes:
    AccessKey:
      type: apiKey
      in: header
      name: FORUM-ACCESS-KEY
      description: Your API key ID, starting with `fk_`.
    AccessTimestamp:
      type: apiKey
      in: header
      name: FORUM-ACCESS-TIMESTAMP
      description: The current Unix time in seconds. Must be within 30 seconds of the server's time.
    AccessSign:
      type: apiKey
      in: header
      name: FORUM-ACCESS-SIGN
      description: Base64-encoded HMAC-SHA256 of the timestamp, method, path and body, keyed with your API secret. See [Authentication](/api-reference/authentication).

  parameters:
    IndexName:
      name: name
      in: path
      required: true
      schema:
        type: string
      description: The index name, which is the index’s ticker followed by `-IDX`. `GET /indices` returns every name.
      example: FORTNITE-IDX

  responses:
    RateLimited:
      description: Too many requests from this IP address. Public endpoints allow 400 requests per minute per IP.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: RATE_LIMIT_EXCEEDED
              message: Rate limit exceeded. Retry after 12 seconds
              details:
                retryAfter: 12

    InvalidParameter:
      description: A query parameter is missing or invalid.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: INVALID_PARAMETER
              message: Request validation failed
              details:
                fields:
                  - field: start
                    message: "Invalid input: expected string, received undefined"

    Unauthorized:
      description: The authentication headers are missing, the key is unknown or revoked, the timestamp is outside the 30-second window, or the signature does not match.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            missingHeaders:
              summary: Missing headers
              value:
                error:
                  code: UNAUTHORIZED
                  message: Missing HMAC authentication headers
            timestampExpired:
              summary: Timestamp outside the window
              value:
                error:
                  code: TIMESTAMP_EXPIRED
                  message: Request timestamp is outside the allowed window
            invalidSignature:
              summary: Signature does not match
              value:
                error:
                  code: INVALID_SIGNATURE
                  message: Signature verification failed

    Forbidden:
      description: The key's owner does not belong to an approved institution.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: INSUFFICIENT_PERMISSIONS
              message: Source data requires membership of an approved institution

    AccountRateLimited:
      description: Too many requests from this key's account. Source data endpoints allow 300 requests per minute per account unless we have agreed a different limit with you.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: RATE_LIMIT_EXCEEDED
              message: Rate limit exceeded. Retry after 12 seconds
              details:
                retryAfter: 12

    AccessCheckUnavailable:
      description: Institution approval could not be checked. Retry shortly.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: IDENTITY_VERIFICATION_UNAVAILABLE
              message: Institution access check is unavailable

  headers:
    X-RateLimit-Limit-Account:
      description: Requests allowed per minute for this key's account.
      schema:
        type: integer
    X-RateLimit-Limit:
      description: Requests allowed per minute from this IP address.
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests remaining in the current window.
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix time (seconds) when the oldest request in the window expires.
      schema:
        type: integer

  schemas:
    Index:
      type: object
      required: [name, createdAt, lastUpdate, currentValue, sourceConfigs, interval, ready, live]
      properties:
        name:
          type: string
          description: The index name.
          example: FORTNITE-IDX
        createdAt:
          type: string
          format: date-time
          description: When the index was created (ISO 8601, UTC).
          example: "2026-02-19T07:31:25.840Z"
        lastUpdate:
          type: ["string", "null"]
          format: date-time
          description: When the latest value was published (ISO 8601, UTC), or null if none has been yet.
          example: "2026-10-23T00:03:00.091Z"
        currentValue:
          type: ["number", "null"]
          description: The latest published Attention Index value (after EMA smoothing), or null if none has been yet.
          example: 56.49316952023987
        sourceConfigs:
          type: array
          description: The sources that make up the index.
          items:
            $ref: "#/components/schemas/IndexSource"
        interval:
          type: integer
          description: Seconds between index updates.
          example: 900
        ready:
          type: boolean
          description: Whether the index has published at least one value.
          example: true
        live:
          type: boolean
          description: Whether the index is currently being updated.
          example: true

    IndexSummary:
      type: object
      required: [name, slug, displayName, category, shortDescription, picture, currentValue, changePercentPastWeek]
      properties:
        name:
          type: string
          description: The index name, used by the other index endpoints.
          example: FORTNITE-IDX
        slug:
          type: string
          description: The index's ticker, its name without `-IDX`; the Index Explorer page is `https://indices.forum.market/{slug}`.
          example: FORTNITE
        displayName:
          type: string
          example: Fortnite
        category:
          type: ["string", "null"]
          description: The Index Explorer section the index is listed under, such as `tech`, `gaming`, `food` or `fashion`.
          example: gaming
        shortDescription:
          type: ["string", "null"]
          description: What the index tracks, or null if it has no description yet.
        picture:
          type: ["string", "null"]
          format: uri
          description: Image URL, or null.
        currentValue:
          type: ["number", "null"]
          description: The latest published Attention Index value (after EMA smoothing).
          example: 58.6314646966042
        changePercentPastWeek:
          type: ["number", "null"]
          description: Change since the first value published in the past 7 days, as a signed fraction (`0.05` is a 5% rise), or null without a value that old.
          example: -0.014313592470827788

    IndexSource:
      type: object
      required: [name, displayName, weight, metrics, dayPercentChangeSigned, weekPercentChangeSigned, monthPercentChangeSigned, yearPercentChangeSigned, allTimePercentChangeSigned]
      description: |
        A source of engagement data. The `*PercentChangeSigned` fields compare the source's latest score with its score that long ago, as a signed fraction: `0.05` is a 5% rise and `-0.05` a 5% fall. They are null when there is no earlier score to compare with.
      properties:
        name:
          type: string
          description: Identifier of the source.
          example: youtube
        displayName:
          type: string
          example: YouTube
        weight:
          type: number
          minimum: 0
          maximum: 1
          description: The source's share of the index. Weights across an index's sources add up to 1.
          example: 0.28
        metrics:
          type: array
          description: The engagement metrics tracked on this source.
          items:
            type: object
            required: [displayName]
            properties:
              displayName:
                type: string
                example: views
        dayPercentChangeSigned:
          type: ["number", "null"]
          example: -0.06600241738463546
        weekPercentChangeSigned:
          type: ["number", "null"]
          example: -0.006974672947391339
        monthPercentChangeSigned:
          type: ["number", "null"]
          example: 0.007944788333089727
        yearPercentChangeSigned:
          type: ["number", "null"]
          example: 0.02636748709447335
        allTimePercentChangeSigned:
          type: ["number", "null"]
          example: 0.016051085659686215

    IndexHistoryRecord:
      type: object
      required: [id, name, value, timestamp, backfillVersion]
      properties:
        id:
          type: integer
          description: Unique ID of the published value.
          example: 505595
        name:
          type: string
          description: The index name.
          example: FORTNITE-IDX
        value:
          type: number
          description: The published Attention Index value (after EMA smoothing).
          example: 56.576344892425176
        timestamp:
          type: string
          format: date-time
          description: When the value was published (ISO 8601, UTC).
          example: "2026-10-22T20:03:00.047Z"
        backfillVersion:
          type: ["string", "null"]
          description: Null for a value recorded live. Otherwise the value was reconstructed after the fact, and this names the method that produced it.
          example: null

    IndexSourceHistoryRecord:
      type: object
      required: [id, name, value, raw, timestamp, scheduledAt, sourceValues, backfillVersion]
      properties:
        id:
          type: integer
          description: Unique ID of the published value.
          example: 505595
        name:
          type: string
          description: The index name.
          example: FORTNITE-IDX
        value:
          type: number
          description: The published Attention Index value (after EMA smoothing).
          example: 56.576344892425176
        raw:
          type: number
          description: The Raw Index value before EMA smoothing.
          example: 56.21093815276502
        timestamp:
          type: string
          format: date-time
          description: When the value was published (ISO 8601, UTC).
          example: "2026-10-22T20:03:00.047Z"
        scheduledAt:
          type: ["string", "null"]
          format: date-time
          description: The publication slot the value was produced for (ISO 8601, UTC), or null if none was recorded.
          example: "2026-10-22T20:03:00.000Z"
        sourceValues:
          type: object
          description: Each source's score at this publication, before weighting, keyed by source identifier such as `youtube`, `reddit`, `x` or `google_trends`.
          additionalProperties:
            type: number
        backfillVersion:
          type: ["string", "null"]
          description: Null for a value recorded live. Otherwise the value was reconstructed after the fact, and this names the method that produced it.
          example: null

    IndexHistoryExportPage:
      type: object
      required: [history, throughId, nextAfterId]
      properties:
        history:
          type: array
          description: Up to 2,500 values, ordered by `id`.
          items:
            allOf:
              - $ref: "#/components/schemas/IndexSourceHistoryRecord"
              - type: object
                properties:
                  value:
                    type: ["number", "null"]
                  raw:
                    type: ["number", "null"]
        throughId:
          type: integer
          description: The last `id` this export covers. Pass it back with every following page.
          example: 512840
        nextAfterId:
          type: ["integer", "null"]
          description: The `afterId` for the next page, or null when this is the last page.
          example: 505595

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: INVALID_PARAMETER
            message:
              type: string
            details:
              type: object
              additionalProperties: true
