# KnowFirm — canonical API contract. SINGLE source of truth for every marketplace listing.
# Every billable capability is one operation with a stable operationId, an x-capability
# (the internal service that backs it), and x-pricing { unit, meter }.
# distribution/generate.mjs reads THIS file to emit every listing artifact. Do not hand-edit
# generated artifacts — edit here and regenerate.
#
# Backed by the persoon.dev warehouse (read-only): cross-source resolved business entities
# (identifiers lei/cik/ein/fdicCert, confidence, sourceIds, recordCount) + U.S. Consolidated
# Screening List. KYB / firmographic data only — NOT an FCRA consumer product.
openapi: 3.1.0
info:
  title: KnowFirm API
  version: 0.1.0
  description: >
    KYB (Know Your Business) & firmographic data API. Verify, enrich, and sanctions-screen any
    U.S. business by EIN, name, LEI, or CIK in one call — resolved across authoritative government
    and global sources with provenance on every field. Not a consumer-reporting (FCRA) product;
    not for credit, employment, or insurance eligibility decisions.
  contact:
    name: KnowFirm Support
    email: support@knowfirm.com
  x-product:
    slug: knowfirm
    catalog: ../catalog/capabilities.json
servers:
  - url: https://api.knowfirm.com
    description: Production
security:
  - ApiKeyAuth: []
components:
  securitySchemes:
    # API key is the lowest common denominator every marketplace accepts.
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
  schemas:
    SegmentRow:
      type: object
      description: One firm-level segment row (Discover). Fields are null when the source lacks them.
      properties:
        subjectKey: { type: string }
        name: { type: string }
        city: { type: string, nullable: true }
        state: { type: string, nullable: true }
        zip: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        email: { type: string, nullable: true }
        size: { type: number, nullable: true }
        latestDate: { type: string, nullable: true }
        identifier:
          type: object
          nullable: true
          properties:
            key: { type: string }
            value: { type: string }
        addressFull: { type: string, nullable: true }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        request_id: { type: string }
    Identifiers:
      type: object
      description: Strong identifiers attached to the resolved entity (any subset present).
      properties:
        lei: { type: string, description: GLEIF Legal Entity Identifier }
        cik: { type: string, description: SEC Central Index Key }
        ein: { type: string, description: IRS Employer Identification Number }
        fdicCert: { type: string, description: FDIC certificate number }
        ticker: { type: string, description: SEC ticker (not a merge key) }
    Provenance:
      type: object
      description: Per-field source attribution. Maps a field name to the source(s) it came from.
      additionalProperties:
        type: array
        items: { type: string }
    ResolvedBusiness:
      type: object
      description: One business resolved across sources. Confidence is match quality, not a claim about the entity.
      required: [resolvedEntityId, canonicalName, subjectType, confidence, sourceIds, recordCount]
      properties:
        resolvedEntityId: { type: string, example: "name:acmecorporation" }
        canonicalName: { type: string, example: "ACME CORPORATION" }
        subjectType: { type: string, enum: [company, nonprofit] }
        identifiers: { $ref: "#/components/schemas/Identifiers" }
        sourceIds:
          type: array
          items: { type: string }
          example: ["gleif", "sec", "irs_exempt"]
        recordCount: { type: integer, example: 7 }
        confidence: { type: integer, minimum: 0, maximum: 100, example: 85 }
        resolutionMethod: { type: string, enum: [name_only, strong_id, strong_id_crosswalk] }
        mergedSubjectKeys:
          type: array
          items: { type: string }
          description: Present when >1 name variant merged on a shared strong identifier.
        firstCapturedAt: { type: string, format: date-time, nullable: true }
        lastCapturedAt: { type: string, format: date-time, nullable: true }
        firmographics:
          type: object
          description: >
            Best-available firmographic attributes assembled from the underlying records
            (e.g. addressFull, state, zip, website, phone) where present. Availability varies by source.
          additionalProperties: true
        provenance: { $ref: "#/components/schemas/Provenance" }
    BusinessSearchResult:
      type: object
      required: [resolvedEntityId, canonicalName, confidence]
      properties:
        resolvedEntityId: { type: string }
        canonicalName: { type: string }
        subjectType: { type: string, enum: [company, nonprofit] }
        identifiers: { $ref: "#/components/schemas/Identifiers" }
        confidence: { type: integer }
        sourceIds: { type: array, items: { type: string } }
    ScreeningRequest:
      type: object
      required: [name]
      properties:
        name: { type: string, description: Business or party name to screen }
        country: { type: string, description: ISO-3166 country code (optional, narrows matches) }
        address: { type: string, description: Optional address text to help disambiguate }
    ScreeningMatch:
      type: object
      properties:
        name: { type: string }
        score: { type: integer, minimum: 0, maximum: 100, description: Fuzzy-match score }
        list: { type: string, description: Source list within the Consolidated Screening List }
        source: { type: string, example: "csl_screening" }
        capturedAt: { type: string, format: date-time, nullable: true }
    ScreeningResult:
      type: object
      required: [query, matches, disclaimer]
      properties:
        query: { type: string }
        matchCount: { type: integer, description: Total rows matching the (deliberately wide) recall query — never miss a listed party. }
        strongMatchCount: { type: integer, description: Subset of matchCount scoring >= 85 (exact / alias / near-exact) — the plausible hits to adjudicate, minus the generic-token fuzzy tail. }
        matches:
          type: array
          items: { $ref: "#/components/schemas/ScreeningMatch" }
        listFreshness: { type: string, format: date-time, nullable: true, description: Most recent CSL capture time }
        disclaimer:
          type: string
          example: >-
            Informational screening against the U.S. Consolidated Screening List. Not legal advice;
            verify any potential match against the official source before acting.
    EnrichRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          maxItems: 100
          items:
            type: object
            description: One lookup key per item (any of ein/lei/cik/name).
            properties:
              ein: { type: string }
              lei: { type: string }
              cik: { type: string }
              name: { type: string }
    EnrichResult:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              query: { type: object, additionalProperties: true }
              business:
                oneOf:
                  - { $ref: "#/components/schemas/ResolvedBusiness" }
                  - { type: "null" }
paths:
  /v1/health:
    get:
      operationId: health
      summary: Health check (unmetered)
      security: []
      x-pricing: { unit: free, meter: null }
      responses:
        "200": { description: OK }
  /v1/business:
    get:
      operationId: businessLookup
      summary: Resolve one business by EIN, LEI, CIK, or name
      description: >
        Returns the cross-source resolved entity with identifiers, contributing sources,
        confidence, capture dates, best-available firmographics, and per-field provenance.
        Provide exactly one of ein, lei, cik, cert, ticker, or name.
      x-capability: business-resolve
      x-pricing: { unit: per_call, meter: business_lookup }
      parameters:
        - { name: ein, in: query, schema: { type: string }, description: IRS EIN }
        - { name: lei, in: query, schema: { type: string }, description: GLEIF LEI }
        - { name: cik, in: query, schema: { type: string }, description: SEC CIK }
        - { name: cert, in: query, schema: { type: string }, description: FDIC certificate number (round-trips a Discover bank export) }
        - { name: ticker, in: query, schema: { type: string }, description: Exchange ticker symbol }
        - { name: name, in: query, schema: { type: string }, description: Legal/business name }
      responses:
        "200":
          description: Resolved business
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResolvedBusiness" }
        "404": { description: No matching entity, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "402": { description: Payment required / over quota, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "429": { description: Rate limited }
  /v1/business/search:
    get:
      operationId: businessSearch
      summary: Search businesses by name
      description: Normalized name search returning ranked candidates — the exact normalized name plus its prefix extensions ("verizon" also finds Verizon Communications), largest entity first. Case, punctuation, and legal suffixes (Inc/LLC) are ignored, and a misspelled query is typo-tolerant via trigram fuzzy matching ("jpmorgn" still recalls JPMorgan).
      x-capability: business-search
      x-pricing: { unit: per_call, meter: business_search }
      parameters:
        - { name: q, in: query, required: true, schema: { type: string }, description: Search query (name) }
        - { name: limit, in: query, schema: { type: integer, default: 10, minimum: 1, maximum: 50 } }
      responses:
        "200":
          description: Ranked candidates
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/BusinessSearchResult" }
        "402": { description: Payment required / over quota }
        "429": { description: Rate limited }
  /v1/segments/preview:
    get:
      operationId: segmentPreview
      summary: Preview a firm segment (Discover)
      description: >
        Exact matching-entity count plus the first 25 rows for a filtered segment of one public
        source (Form 5500 plan sponsors, small-plan sponsors, FDIC banks, NCUA credit unions,
        FMCSA motor carriers). Firm-level records only — no personal contact data.
      x-capability: segment-preview
      x-pricing: { unit: per_call, meter: segment_search }
      parameters:
        - { name: source, in: query, required: true, schema: { type: string, enum: [dol_form5500, dol_form5500_sf, fdic_institutions, ncua_credit_unions, fmcsa_carriers, irs_eo_bmf] } }
        - { name: state, in: query, schema: { type: string, minLength: 2, maxLength: 2 }, description: Two-letter state code }
        - { name: minSize, in: query, schema: { type: number, minimum: 0 }, description: Source-specific size floor (participants / assets / power units) }
        - { name: industryPrefix, in: query, schema: { type: string, pattern: "^\\d{2,6}$" }, description: NAICS prefix (DOL sources only) }
        - { name: filedSinceDays, in: query, schema: { type: integer, minimum: 1, maximum: 3650 }, description: Latest filing within N days (DOL sources only) }
      responses:
        "200":
          description: Count + first 25 rows
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  sample: { type: array, items: { $ref: "#/components/schemas/SegmentRow" } }
        "400": { description: Invalid filter }
        "402": { description: Payment required / over quota }
        "429": { description: Rate limited }
  /v1/segments/export:
    post:
      operationId: segmentExport
      summary: Export a firm segment (Discover)
      description: >
        Firm-level rows for a filtered segment, billed per returned row (1 credit/row). limit
        defaults to 1000, max 10000; the credit pre-check uses the requested limit. Rows carry
        name, location, phone/email where the source publishes them, size, and the source
        identifier. Not for FCRA-covered eligibility decisions.
      x-capability: segment-export
      x-pricing: { unit: per_record, meter: segment_export }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [source]
              properties:
                source: { type: string, enum: [dol_form5500, dol_form5500_sf, fdic_institutions, ncua_credit_unions, fmcsa_carriers, irs_eo_bmf] }
                state: { type: string, minLength: 2, maxLength: 2 }
                minSize: { type: number, minimum: 0 }
                industryPrefix: { type: string, pattern: "^\\d{2,6}$" }
                filedSinceDays: { type: integer, minimum: 1, maximum: 3650 }
                limit: { type: integer, minimum: 1, maximum: 10000, default: 1000 }
      responses:
        "200":
          description: Segment rows (billed per row)
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  rows: { type: array, items: { $ref: "#/components/schemas/SegmentRow" } }
        "400": { description: Invalid filter }
        "402": { description: Payment required / over quota }
        "429": { description: Rate limited }
  /v1/screening:
    post:
      operationId: screening
      summary: Sanctions / denied-party screening (U.S. Consolidated Screening List)
      description: >
        Screens a name against the U.S. Consolidated Screening List and returns scored matches
        with the source list and a freshness timestamp. Informational only — not legal advice.
      x-capability: screening
      x-pricing: { unit: per_call, meter: screening }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScreeningRequest" }
      responses:
        "200":
          description: Screening result
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScreeningResult" }
        "402": { description: Payment required / over quota }
        "429": { description: Rate limited }
  /v1/enrich:
    post:
      operationId: enrich
      summary: Batch business resolution / enrichment
      description: Resolve up to 100 businesses in one request. Billed per record returned.
      x-capability: enrich
      x-pricing: { unit: per_record, meter: enrich_records }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EnrichRequest" }
      responses:
        "200":
          description: Batch results
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EnrichResult" }
        "402": { description: Payment required / over quota }
        "429": { description: Rate limited }
