openapi: 3.1.0
info:
  title: KrabyData API
  version: 1.1.0
  summary: ETF holdings, fund facts and security identity from primary sources.
  description: |
    Every answer names its source (issuer or filing, file URL, fetch time, hash, the source's notice) and how old
    the data is. SEC Form N-PORT holdings are two to five months old: only the third month of each fiscal quarter
    is made public, 60 days after the quarter ends. The `source.freshness` line says so on every such answer.

    Keys. Every route but `/v1/health` needs `Authorization: Bearer <key>`. Keys are never read from a query string.
    External keys see the datasets PentaLab may redistribute: SEC N-PORT holdings, US fund fees, the identity
    resolver and the security master. A fund from any other source answers 404 `not_licensed`, and is left out of
    every list. A fund whose issuer has not agreed but which files N-PORT itself (Global X US funds) is shown
    through those filings only: its ISIN, ticker, name, EDGAR ids, SEC prospectus fee and N-PORT dates, never the
    issuer's NAV, net assets, launch date or daily files. The resolver answers an external key from public
    evidence only (OpenFIGI listings, the security master rows N-PORT names, lines of N-PORT filings); a symbol
    that only an issuer's file names answers as an unknown one. External answers never carry a `cusip` or `sedol`
    field (CUSIP Global Services and LSEG own those codes). FIGIs are given with the attribution "FIGI data from
    OpenFIGI".

    Limits. Plans set a daily quota (UTC day) and a per-minute rate. Answers carry `X-RateLimit-Limit` and
    `X-RateLimit-Remaining` for the day; a 429 carries `Retry-After` in seconds.

    Versions. The base path is `/v1`. Fields may be added within v1; nothing is removed or changes meaning
    without a `/v2`. Changes are listed in docs/CHANGELOG.md.
  contact:
    email: contact@krabydata.com
servers:
  - url: https://api.krabydata.com
security:
  - bearer: []

paths:
  /v1/health:
    get:
      operationId: getHealth
      summary: Service status, for status pages and uptime checks
      description: Needs no key. Whether the database answers, each source's latest holdings date, and each collector's last run.
      security: []
      responses:
        '200':
          description: Up
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Health' }
        '503':
          description: The database does not answer
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: false }
                  database: { type: object, properties: { ok: { type: boolean } } }
                  message: { type: string }

  /v1/stats:
    get:
      operationId: getStats
      summary: What the service holds, for the site's front page
      description: |
        Needs no key. Funds, the holding lines of each fund's latest file, the securities in the master, and per source
        its funds and latest holdings date. Kept for a minute; the answer says when it was computed.
      security: []
      responses:
        '200':
          description: The figures
          headers:
            Cache-Control: { description: public, max-age=60, schema: { type: string } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Stats' }

  /v1/issuers:
    get:
      operationId: listIssuers
      summary: Sources collected, with their counts, latest date and last run
      description: An external key sees only the issuers whose data may be redistributed.
      responses:
        '200':
          description: The issuers
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Issuer' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/etfs:
    get:
      operationId: listEtfs
      summary: A page of funds
      parameters:
        - { name: issuer, in: query, schema: { type: string }, description: 'An issuer id from /v1/issuers (sec-nport, globalx...)' }
        - { name: q, in: query, schema: { type: string }, description: 'Text found in the name, ticker or ISIN' }
        - name: sort
          in: query
          schema: { type: string, enum: [ticker, name, issuer, isin, fees, launched, nav, holdingsAsOf, lines] }
        - { name: dir, in: query, schema: { type: string, enum: [asc, desc], default: asc } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - { name: offset, in: query, schema: { type: integer, minimum: 0, default: 0 } }
      responses:
        '200':
          description: The page
          content:
            application/json:
              schema:
                type: object
                properties:
                  pagination:
                    type: object
                    properties:
                      limit: { type: integer }
                      offset: { type: integer }
                      count: { type: integer }
                      total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/Etf' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/etfs/{id}:
    get:
      operationId: getEtf
      summary: One fund's facts
      parameters:
        - $ref: '#/components/parameters/FundId'
      responses:
        '200':
          description: The fund
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Etf'
                      - type: object
                        properties:
                          issuerName: { type: [string, 'null'] }
                          history:
                            type: object
                            properties:
                              days: { type: integer, description: Holdings dates stored }
                              from: { type: [string, 'null'], format: date }
                          source: { $ref: '#/components/schemas/Source' }
        '300': { $ref: '#/components/responses/Ambiguous' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/etfs/{id}/holdings:
    get:
      operationId: getHoldings
      summary: A fund's holdings on a date
      description: |
        The latest file on or before `date` (today when omitted) whose weights passed the check on write. Lines keep
        the file's own identifiers; a line whose file gave no ISIN may carry one found by a rule, named in `isinFrom`.
      parameters:
        - $ref: '#/components/parameters/FundId'
        - { name: date, in: query, schema: { type: string, format: date } }
      responses:
        '200':
          description: The holdings
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      etf: { $ref: '#/components/schemas/FundRef' }
                      asOf: { type: string, format: date }
                      requestedDate: { type: [string, 'null'], format: date }
                      totals:
                        type: object
                        properties:
                          lines: { type: integer }
                          securities: { type: integer }
                          weightPct: { type: number }
                          securitiesWeightPct: { type: number }
                          cashWeightPct: { type: number }
                          cryptoWeightPct: { type: number }
                          otherWeightPct: { type: number }
                          identifiedWeightPct: { type: [number, 'null'], description: Share of the security weight whose lines carry an ISIN }
                      source: { $ref: '#/components/schemas/Source' }
                      attribution: { type: string, description: '"FIGI data from OpenFIGI" when a line carries a FIGI (external keys)' }
                      holdings: { type: array, items: { $ref: '#/components/schemas/Holding' } }
        '300': { $ref: '#/components/responses/Ambiguous' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: 'No such fund (not_found), not licensed for an external key (not_licensed), no file on or before the date (no_holdings), or only files held back by the weight check (suspect_holdings, with the reason)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/etfs/{id}/holdings/dates:
    get:
      operationId: listHoldingsDates
      summary: Every holdings date stored for a fund
      parameters:
        - $ref: '#/components/parameters/FundId'
      responses:
        '200':
          description: The dates, newest first; a held-back date is listed with its reason and never served
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      etf: { $ref: '#/components/schemas/FundRef' }
                      dates:
                        type: array
                        items:
                          type: object
                          properties:
                            asOf: { type: string, format: date }
                            count: { type: integer }
                            fetchedAt: { type: string, format: date-time }
                            suspect: { type: boolean }
                            suspectReason: { type: [string, 'null'] }
        '300': { $ref: '#/components/responses/Ambiguous' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/etfs/{id}/holdings/open:
    get:
      operationId: listOpenLines
      summary: Security lines of the latest file still without an ISIN, heaviest first, and why
      parameters:
        - $ref: '#/components/parameters/FundId'
      responses:
        '200':
          description: The open lines
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      etf: { $ref: '#/components/schemas/FundRef' }
                      asOf: { type: string, format: date }
                      identifiedWeightPct: { type: [number, 'null'] }
                      openLines: { type: integer }
                      openWeightPct: { type: number }
                      open:
                        type: array
                        items:
                          type: object
                          properties:
                            line: { type: integer }
                            name: { type: string }
                            ticker: { type: [string, 'null'] }
                            cusip: { type: [string, 'null'], description: Internal keys only }
                            assetCat: { type: [string, 'null'] }
                            weightPct: { type: [number, 'null'] }
                            why:
                              type: string
                              enum: ['tie: preferred', 'tie: share classes', successor unconfirmed, venue unknown, no candidate, debt, no identifier, resolvable]
                            candidates: { type: array, items: { type: string, description: ISIN } }
        '300': { $ref: '#/components/responses/Ambiguous' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/etfs/{id}/listings:
    get:
      operationId: listEtfListings
      summary: Every venue a fund trades on, from OpenFIGI
      parameters:
        - $ref: '#/components/parameters/FundId'
      responses:
        '200':
          description: The listings
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      etf: { $ref: '#/components/schemas/FundRef' }
                      listings: { type: array, items: { $ref: '#/components/schemas/Listing' } }
                      attribution: { type: string, const: FIGI data from OpenFIGI }
        '300': { $ref: '#/components/responses/Ambiguous' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/listings/resolve:
    get:
      operationId: resolveListing
      summary: The ISIN behind a symbol, a ticker on a venue, or the listings of an ISIN
      description: |
        Give one of `symbol` (a Yahoo-style symbol: DFNS.MI, NESN.SW, AMZN, BRK-B), `ticker` with `mic`, or `isin`.
        A fund is found in its OpenFIGI listings, a share in the security master, then in holdings lines. When several
        ISINs answer, `isin` is null and `candidates` lists them: the API never guesses. An external key's answer rests
        on public evidence only: OpenFIGI listings, the master rows SEC N-PORT filings name with the ticker they give,
        and N-PORT lines that carry their own ISIN.
      parameters:
        - { name: symbol, in: query, schema: { type: string } }
        - { name: ticker, in: query, schema: { type: string } }
        - { name: mic, in: query, schema: { type: string }, description: ISO 10383 market identifier code }
        - { name: isin, in: query, schema: { type: string } }
      responses:
        '200':
          description: The answer, possibly empty
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      isin: { type: [string, 'null'] }
                      candidates: { type: array, items: { type: string } }
                      funds:
                        type: array
                        items:
                          type: object
                          properties:
                            isin: { type: string }
                            ticker: { type: [string, 'null'] }
                            name: { type: string }
                            issuer: { type: string }
                      listings: { type: array, items: { $ref: '#/components/schemas/Listing' } }
                      securities:
                        type: array
                        items:
                          type: object
                          properties:
                            isin: { type: string }
                            name: { type: [string, 'null'] }
                            currency: { type: [string, 'null'] }
                            tickers: { type: array, items: { type: string } }
                            heldByFunds: { type: integer }
                            lastSeen: { type: [string, 'null'], format: date }
                            cusip: { type: [string, 'null'], description: Internal keys only }
                            country: { type: [string, 'null'] }
                      attribution: { type: string, const: FIGI data from OpenFIGI }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/securities/{isin}:
    get:
      operationId: getSecurity
      summary: One security from the master, with its succession
      parameters:
        - $ref: '#/components/parameters/Isin'
      responses:
        '200':
          description: The security
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      isin: { type: string }
                      ticker: { type: [string, 'null'] }
                      name: { type: [string, 'null'] }
                      cusip: { type: [string, 'null'], description: Internal keys only }
                      lei: { type: [string, 'null'] }
                      country: { type: [string, 'null'] }
                      firstSeen: { type: [string, 'null'], format: date }
                      lastSeen: { type: [string, 'null'], format: date }
                      heldByFunds: { type: [integer, 'null'] }
                      succeededBy: { oneOf: [{ $ref: '#/components/schemas/Succession' }, { type: 'null' }] }
                      succeeds: { type: array, items: { $ref: '#/components/schemas/Succession' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooMany' }

  /v1/securities/{isin}/funds:
    get:
      operationId: listSecurityHolders
      summary: The funds holding a security, from each fund's latest file on or before the date
      parameters:
        - $ref: '#/components/parameters/Isin'
        - { name: date, in: query, schema: { type: string, format: date } }
      responses:
        '200':
          description: The funds, heaviest weight first
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      isin: { type: string }
                      name: { type: [string, 'null'] }
                      requestedDate: { type: [string, 'null'], format: date }
                      funds:
                        type: array
                        items:
                          type: object
                          properties:
                            isin: { type: string }
                            ticker: { type: [string, 'null'] }
                            name: { type: string }
                            issuer: { type: string }
                            asOf: { type: string, format: date }
                            weightPct: { type: [number, 'null'] }
                            shares: { type: [number, 'null'] }
                            marketValue: { type: [number, 'null'] }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooMany' }

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: 'kd_ followed by 43 characters, shown once when the key is created'

  parameters:
    FundId:
      name: id
      in: path
      required: true
      schema: { type: string }
      description: The fund's ISIN, or its ticker when no other fund shares it (300 otherwise)
    Isin:
      name: isin
      in: path
      required: true
      schema: { type: string, pattern: '^[A-Z]{2}[A-Z0-9]{9}[0-9]$' }

  headers:
    Limit: { description: Requests allowed per UTC day on the key's plan, schema: { type: integer } }
    Remaining: { description: Requests left today, schema: { type: integer } }
    RetryAfter: { description: Seconds to wait, schema: { type: integer } }

  responses:
    BadRequest:
      description: 'A malformed parameter (bad_date, bad_isin, bad_sort, bad_request)'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: 'No key (missing_key) or an unknown one (invalid_key)'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: 'A revoked key (revoked_key)'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: 'No such fund or security (not_found), or a dataset an external key may not see (not_licensed)'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Ambiguous:
      description: Several funds share that ticker; use the ISIN
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Error'
              - type: object
                properties:
                  candidates: { type: array, items: { $ref: '#/components/schemas/Etf' } }
    TooMany:
      description: 'Over the daily quota (quota_exceeded, until midnight UTC) or the per-minute rate (rate_limited)'
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-RateLimit-Limit: { $ref: '#/components/headers/Limit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/Remaining' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error: { type: string }
        message: { type: string }

    Source:
      type: object
      description: Where the numbers came from, and how old they are
      properties:
        issuer: { type: [string, 'null'] }
        issuerName: { type: [string, 'null'] }
        url: { type: [string, 'null'] }
        fetchedAt: { type: [string, 'null'], format: date-time }
        sha256: { type: [string, 'null'] }
        notice: { type: [string, 'null'], description: The source's own terms notice }
        externalDistribution: { type: boolean }
        freshness: { type: [string, 'null'], description: How old this source's data is (N-PORT two to five months) }

    FundRef:
      type: object
      properties:
        isin: { type: string }
        ticker: { type: [string, 'null'] }
        name: { type: string }
        issuer: { type: string }

    Etf:
      type: object
      properties:
        isin: { type: string }
        ticker: { type: [string, 'null'] }
        name: { type: string }
        issuer: { type: string }
        shareClass: { type: [string, 'null'] }
        theme: { type: [string, 'null'] }
        sfdr: { type: [string, 'null'] }
        ongoingChargesPct: { type: [number, 'null'] }
        inceptionDate: { type: [string, 'null'], format: date }
        countries: { type: array, items: { type: string } }
        netAssets: { type: [number, 'null'] }
        nav: { type: [number, 'null'] }
        navAsOf: { type: [string, 'null'], format: date }
        domicile: { type: [string, 'null'] }
        replication: { type: [string, 'null'] }
        distributionFrequency: { type: [string, 'null'] }
        baseCurrency: { type: [string, 'null'] }
        indexName: { type: [string, 'null'] }
        detailsAt: { type: [string, 'null'], format: date-time }
        holdings:
          oneOf:
            - type: 'null'
            - type: object
              properties:
                asOf: { type: string, format: date }
                count: { type: integer }
        identifiedWeightPct: { type: [number, 'null'] }
        edgar:
          description: A US fund's ids in SEC EDGAR
          oneOf:
            - type: 'null'
            - type: object
              properties:
                cik: { type: string }
                seriesId: { type: [string, 'null'] }
                classId: { type: [string, 'null'] }

    Holding:
      type: object
      properties:
        line: { type: integer }
        kind: { type: string, enum: [security, cash, crypto, other] }
        name: { type: string }
        isin: { type: [string, 'null'] }
        isinFrom:
          type: [string, 'null']
          enum: [file, cusip, master-cusip, master-sedol, treasury, master-ticker, successor, null]
          description: The file, or the rule that found the ISIN
        sedol: { type: [string, 'null'], description: Internal keys only }
        ticker: { type: [string, 'null'] }
        tickerFrom: { type: [string, 'null'], enum: [file, master, null] }
        country: { type: [string, 'null'] }
        figi: { type: [string, 'null'] }
        cusip: { type: [string, 'null'], description: Internal keys only }
        currency: { type: [string, 'null'] }
        sector: { type: [string, 'null'] }
        assetCat: { type: [string, 'null'], description: 'N-PORT asset category (EC, EP, DBT...)' }
        weightPct: { type: [number, 'null'] }
        shares: { type: [number, 'null'] }
        price: { type: [number, 'null'] }
        marketValue: { type: [number, 'null'] }

    Listing:
      type: object
      properties:
        figi: { type: string }
        isin: { type: string }
        ticker: { type: string }
        exchCode: { type: [string, 'null'], description: Bloomberg venue code }
        mic: { type: [string, 'null'] }
        symbol: { type: [string, 'null'], description: Yahoo-style symbol where one exists }
        name: { type: [string, 'null'] }
        securityType: { type: [string, 'null'] }
        fetchedAt: { type: string, format: date-time }

    Succession:
      type: object
      properties:
        isin: { type: string }
        effectiveFrom: { type: [string, 'null'], format: date }
        evidence: { type: object }

    Issuer:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        website: { type: [string, 'null'] }
        notice: { type: [string, 'null'] }
        externalDistribution: { type: boolean }
        funds: { type: integer }
        fundsWithHoldings: { type: integer }
        latestAsOf: { type: [string, 'null'], format: date }
        lastFetchedAt: { type: [string, 'null'], format: date-time }
        linesLatest: { type: integer }
        lastRun: { type: [object, 'null'] }

    Stats:
      type: object
      properties:
        data:
          type: object
          properties:
            funds: { type: integer }
            fundsWithHoldings: { type: integer }
            holdingLines: { type: integer, description: Lines of each fund's latest holdings file, summed }
            securities: { type: integer, description: ISINs in the security master }
            sources:
              type: array
              items:
                type: object
                properties:
                  source: { type: string }
                  name: { type: string }
                  externalDistribution: { type: boolean, description: Whether external keys see this source }
                  funds: { type: integer }
                  fundsWithHoldings: { type: integer }
                  fundsWithFees: { type: integer }
                  holdingLines: { type: integer }
                  latestAsOf: { type: [string, 'null'], format: date }
        asOf: { type: string, format: date-time, description: When the figures were computed }

    Health:
      type: object
      properties:
        ok: { type: boolean }
        time: { type: string, format: date-time }
        database:
          type: object
          properties:
            ok: { type: boolean }
            ms: { type: integer }
        holdings:
          type: array
          items:
            type: object
            properties:
              source: { type: string }
              latestAsOf: { type: string, format: date }
              lastFetchedAt: { type: string, format: date-time }
        collectors:
          type: array
          items:
            type: object
            properties:
              collector: { type: string }
              startedAt: { type: string, format: date-time }
              finishedAt: { type: [string, 'null'], format: date-time }
              funds: { type: integer }
              files: { type: integer }
              rows: { type: integer }
              errors: { type: integer }
        lastRun: { type: [object, 'null'], description: The most recent of the collectors' runs }
