openapi: 3.1.0
info:
  title: Indexed.vc API
  version: 1.0.0
  description: |
    Company intelligence API providing comprehensive data on private companies,
    funding history, and investor relationships.

    ## Authentication
    All endpoints require an `X-API-Key` header (Gold tier or higher).
    The free `/industries` endpoint is publicly accessible without a key.

    ## Credits
    Most endpoints consume credits. Costs are documented per-endpoint.
    The 12-month entity cache means repeated access to the same entity is free.

    ## Rate Limiting
    All authenticated responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`,
    and `X-RateLimit-Reset` headers.

    ## Request Correlation
    Every response includes an `X-Request-Id` header (UUID v4). Include this
    in support requests for fast debugging.
  contact:
    name: Indexed.vc Support
    url: https://indexed.vc
  license:
    name: Proprietary

servers:
  - url: https://indexed.vc/api/v1
    description: Production

security:
  - ApiKeyAuth: []

paths:
  /companies:
    get:
      operationId: searchCompanies
      summary: Search companies
      description: |
        Search and filter companies. Returns masked results by default.
        Standard search costs 1 credit. Premium search (techStack/appStack filters)
        costs 3 credits on the first page; subsequent pages with a valid
        `X-Search-Session` token are free for 15 minutes.
      parameters:
        - name: q
          in: query
          schema: { type: string, maxLength: 200 }
          description: Free text search query
        - name: industries
          in: query
          schema: { type: string }
          description: "Comma-separated industry filters. Accepts display labels (Fintech), case variants (fintech), or slugs (ai-ml). Use GET /industries for valid values."
        - name: countries
          in: query
          schema: { type: string }
          description: "Comma-separated country names (e.g., United States,United Kingdom)"
        - name: employees
          in: query
          schema: { type: string }
          description: "Comma-separated employee ranges: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+"
        - name: operatingStatus
          in: query
          schema: { type: string }
          description: "Comma-separated: active, acquired, ipo, closed, merged"
        - name: minFunding
          in: query
          schema: { type: integer, minimum: 0 }
          description: Minimum total funding in whole USD
        - name: maxFunding
          in: query
          schema: { type: integer, minimum: 0 }
          description: Maximum total funding in whole USD
        - name: domain
          in: query
          schema: { type: string }
          description: "Match a company by its website domain (enrichment lookup, e.g. stripe.com). EXACT registrable-domain matching: matches mercury.com and any subdomain (www./app.mercury.com) but never superstrings like bluemercury.com. Accepts bare domains or full URLs."
        - name: minCompleteness
          in: query
          schema: { type: integer, minimum: 0, maximum: 64 }
          description: "Minimum data_completeness_score — skip thin records"
        - name: techStack
          in: query
          schema: { type: string }
          description: "Comma-separated technology names (triggers premium search: 3 credits)"
        - name: appStack
          in: query
          schema: { type: string }
          description: "Comma-separated app/tool names (triggers premium search: 3 credits)"
        - name: stackMatch
          in: query
          schema: { type: string, enum: [any, all], default: any }
          description: Match mode for tech/app stack filters
        - name: sort
          in: query
          schema: { type: string, enum: [total_funding_raised, name, last_funding_date, last_funding_amount, founded_year, data_completeness_score] }
        - name: order
          in: query
          schema: { type: string, enum: [asc, desc] }
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: cursor
          in: query
          schema: { type: string }
          description: "Keyset cursor for stable bulk pulls (drift-free). Pass meta.next_cursor from the prior page; ignores page/sort."
        - name: reveal_status
          in: query
          schema: { type: string, enum: [all, revealed, unrevealed], default: all }
          description: Filter results by reveal state
        - name: X-Search-Session
          in: header
          schema: { type: string }
          description: Session token for free premium search pagination (15-min TTL)
      responses:
        "200":
          description: Masked search results
          headers:
            X-Request-Id: { schema: { type: string, format: uuid } }
            X-Credits-Cost: { schema: { type: integer } }
            X-Search-Session: { schema: { type: string }, description: Premium search session token }
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            X-RateLimit-Reset: { schema: { type: integer }, description: Unix timestamp }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      oneOf:
                        - $ref: "#/components/schemas/MaskedEntity"
                        - $ref: "#/components/schemas/CompanyCard"
                  meta:
                    $ref: "#/components/schemas/MaskedPageMeta"
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/AuthError" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /companies/{slug}:
    get:
      operationId: getCompany
      summary: Get company by slug
      description: |
        Fetch a single company profile. Cost varies by depth:

        **valuation_events field schema** (returned at `depth=full`): each
        round object carries `event_type`, `round_name`, `amount_raised_usd`
        (whole USD; also mirrored as `amount_raised`), `announced_date`
        (YYYY-MM-DD), `pre_money_valuation` / `post_money_valuation`,
        `press_url`, `confidence_score`, and `participants[]` (investors with
        `is_lead`). Note: valuation-only events (409A marks, some tender
        offers) legitimately have `amount_raised_usd: null` — a null amount
        is "undisclosed/not applicable", not missing data. `data_as_of` on the
        response is when this record was last verified by enrichment.

        - `slim` (1 credit): identity fields only
        - `standard` (2 credits): all company scalar fields (no relations/valuations/stack)
        - `full` (3 credits): complete data — funding rounds, investors, tech/app stack, valuations
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string, pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$" }
        - name: depth
          in: query
          schema: { type: string, enum: [slim, standard, full], default: standard }
      responses:
        "200":
          description: Company profile
          headers:
            X-Request-Id: { schema: { type: string, format: uuid } }
            X-Credits-Cost: { schema: { type: integer } }
            X-Credits-Cached: { schema: { type: string, enum: ["true"] } }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CompanyProfile" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFoundError" }

  /investors:
    get:
      operationId: searchInvestors
      summary: Search investors
      description: "Search and filter investors. 1 credit per request."
      parameters:
        - name: q
          in: query
          schema: { type: string, maxLength: 200 }
        - name: types
          in: query
          schema: { type: string }
          description: "Comma-separated: vc, pe, angel, cvc, accelerator, family_office, etc."
        - name: stages
          in: query
          schema: { type: string }
          description: "Comma-separated: seed, series_a, series_b, etc."
        - name: industries
          in: query
          schema: { type: string }
          description: Comma-separated industry preferences
        - name: countries
          in: query
          schema: { type: string }
        - name: minAum
          in: query
          schema: { type: integer, minimum: 0 }
        - name: maxAum
          in: query
          schema: { type: integer, minimum: 0 }
        - name: minCompleteness
          in: query
          schema: { type: integer, minimum: 0, maximum: 64 }
          description: "Minimum data_completeness_score — skip thin records"
        - name: sort
          in: query
          schema: { type: string, enum: [name, aum, portfolio_count, total_investments, last_investment_date, data_completeness_score] }
        - name: order
          in: query
          schema: { type: string, enum: [asc, desc] }
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: cursor
          in: query
          schema: { type: string }
          description: "Keyset cursor for stable bulk pulls (drift-free). Pass meta.next_cursor from the prior page; ignores page/sort."
        - name: reveal_status
          in: query
          schema: { type: string, enum: [all, revealed, unrevealed], default: all }
      responses:
        "200":
          description: Masked search results
          headers:
            X-Request-Id: { schema: { type: string, format: uuid } }
            X-Credits-Cost: { schema: { type: integer } }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      oneOf:
                        - $ref: "#/components/schemas/MaskedEntity"
                        - $ref: "#/components/schemas/InvestorCard"
                  meta:
                    $ref: "#/components/schemas/MaskedPageMeta"
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/AuthError" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /investors/{slug}:
    get:
      operationId: getInvestor
      summary: Get investor by slug
      description: |
        Fetch a single investor profile. Same depth pricing as companies:
        slim (1), standard (2), full (3 credits).
        Full includes portfolioCompanies, investments, and subFunds.
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string, pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$" }
        - name: depth
          in: query
          schema: { type: string, enum: [slim, standard, full], default: standard }
      responses:
        "200":
          description: Investor profile
          headers:
            X-Request-Id: { schema: { type: string, format: uuid } }
            X-Credits-Cost: { schema: { type: integer } }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InvestorProfile" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFoundError" }

  /enrich:
    post:
      operationId: enrichCompanies
      summary: Bulk enrich companies by domain or slug
      description: |
        The GTM/RevOps query: submit a list of domains (or slugs) and get the
        matched company records back. Domain matching is EXACT
        registrable-domain (mercury.com matches Mercury at mercury.com or any
        of its subdomains — never superstrings like bluemercury.com); you are
        never billed for a fuzzy match. Billed like /reveal — 3 credits per
        newly revealed match; previously-revealed matches are free within the
        12-month cache. Unmatched inputs return status "not_found" (no charge).
        Batch size and hourly limits follow your tier's reveal limits.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                domains:
                  type: array
                  items: { type: string }
                  description: "Company website domains, e.g. [\"stripe.com\", \"openai.com\"]"
                slugs:
                  type: array
                  items: { type: string }
                  description: Indexed company slugs
      responses:
        "200":
          description: Enrich results (one per input, in order) + summary
          headers:
            X-Request-Id: { schema: { type: string, format: uuid } }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        query: { type: string }
                        matched: { type: boolean }
                        status: { type: string, enum: [revealed, cached, insufficient_credits, not_found] }
                        credits_charged: { type: integer }
                        data: { type: object }
                  summary: { type: object }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/AuthError" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /webhooks:
    get:
      operationId: listWebhooks
      summary: List webhook endpoints
      description: FREE. Signing secrets are masked (whsec_…last4) — the full secret is only shown at creation.
      responses:
        "200":
          description: Endpoints + per-tier limit
        "401": { $ref: "#/components/responses/AuthError" }
        "403": { $ref: "#/components/responses/ValidationError" }
    post:
      operationId: createWebhook
      summary: Create a webhook endpoint (Gold/Platinum/Enterprise)
      description: |
        Subscribe a URL to funding-round alerts. When a new funding round lands
        for a company on your watchlist (`list_id` — one of your saved company
        lists), or any company if unscoped, Indexed POSTs a signed JSON event
        to your endpoint within ~5 minutes.

        **Billing:** 1 credit per successfully delivered event. Test sends and
        retries are free; a delivery is never charged twice (idempotent per
        delivery id). Deliveries pause if your credits are exhausted and resume
        on the retry schedule once topped up.

        **Delivery + retries:** 10s timeout, HTTPS only, redirects refused.
        Non-2xx responses retry on a backoff of 1m → 10m → 1h → 6h → 24h, then
        dead-letter. 20 consecutive failures auto-disable the endpoint
        (re-enable via PATCH `is_active: true`).

        **Delivery semantics — at-least-once.** Like Stripe/GitHub webhooks, an
        event may occasionally be delivered more than once (e.g. your endpoint
        200s but the ack is lost, or a worker is reclaimed after a crash).
        **Dedupe on `X-Indexed-Delivery-Id`** — process each id once. Billing is
        exactly-once regardless: a given delivery is charged at most one credit
        even if re-sent (idempotent per delivery id).

        **Verifying signatures:** every request carries
        `X-Indexed-Signature: t=<unix_seconds>,v1=<hex>` where
        `v1 = HMAC_SHA256(secret, "{t}.{raw_body}")`. Recompute over the raw
        body, compare constant-time, and reject if `|now - t| > 300s`:
        ```js
        const [t, v1] = header.match(/t=(\d+),v1=([0-9a-f]+)/).slice(1)
        const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
        const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
          && Math.abs(Date.now()/1000 - t) < 300
        ```
        Requests also carry `X-Indexed-Event` and `X-Indexed-Delivery-Id`
        (use the delivery id for idempotent processing on your side).

        **Event payload** (`funding_round.created`):
        ```json
        {
          "id": "<delivery uuid>",
          "event": "funding_round.created",
          "created_at": "2026-07-03T12:00:00Z",
          "data": {
            "company": { "id", "name", "slug", "website", "industries", "hq_city", "hq_country" },
            "funding_round": {
              "event_type": "series_b", "round_name": "Series B",
              "amount_raised_usd": 42000000, "announced_date": "2026-07-01",
              "post_money_valuation": 400000000, "press_url": "https://…",
              "investors": [{ "name": "Example Ventures", "is_lead": true }]
            }
          }
        }
        ```
        Endpoint limits: Gold 3, Platinum 10, Enterprise 25.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, description: "HTTPS endpoint (public host — internal/private addresses rejected)" }
                description: { type: string, maxLength: 200 }
                event_types:
                  type: array
                  items: { type: string, enum: [funding_round.created] }
                list_id: { type: string, format: uuid, description: "Scope to one of your saved company lists (watchlist). Omit for all companies." }
      responses:
        "201":
          description: Created. `secret` (whsec_…) is returned ONLY here — store it to verify signatures.
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/AuthError" }
        "403": { $ref: "#/components/responses/ValidationError" }

  /webhooks/{id}:
    patch:
      operationId: updateWebhook
      summary: Update a webhook endpoint
      description: "Update url/event_types/list_id/description or pause/resume via is_active. Re-enabling resets the failure counter."
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Updated endpoint (secret masked) }
        "404": { description: Not found or not owned by you }
    delete:
      operationId: deleteWebhook
      summary: Delete a webhook endpoint
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Deleted }
        "404": { description: Not found or not owned by you }

  /webhooks/{id}/test:
    post:
      operationId: testWebhook
      summary: Send a sample signed event (FREE)
      description: Sends a `funding_round.created` sample (flagged `"test": true`) so you can verify connectivity and signature handling. Never consumes credits.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: "{ delivered, response_code, error, credits_charged: 0 }" }
        "404": { description: Not found }

  /webhooks/{id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: Recent delivery log (FREE)
      description: Last 50 deliveries with status (pending/delivered/failed/dead/credit_blocked), attempts, response codes, and credits_charged — the per-event billing audit trail. Test sends appear with event_type "test" and credits_charged 0.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Delivery log entries }
        "404": { description: Not found }

  /reveal:
    post:
      operationId: revealEntities
      summary: Reveal masked entities
      description: |
        Unmask one or more entities from search results. 3 credits per newly
        revealed entity (cached reveals are free). Accepts both slugs and UUIDs.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entity_type]
              properties:
                entity_type:
                  type: string
                  enum: [company, investor]
                id:
                  type: string
                  description: Single entity slug or UUID
                ids:
                  type: array
                  items: { type: string }
                  description: Batch of entity slugs/UUIDs (max 10 for Gold)
      responses:
        "200":
          description: Reveal results
          headers:
            X-Request-Id: { schema: { type: string, format: uuid } }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/RevealResult" }
                  summary:
                    $ref: "#/components/schemas/RevealSummary"
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/AuthError" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /usage:
    get:
      operationId: getUsage
      summary: Check credit balance and limits
      description: "FREE — does not consume credits. Returns tier, credit balance, and rate limits."
      responses:
        "200":
          description: Usage data
          headers:
            X-Request-Id: { schema: { type: string, format: uuid } }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/UsageData" }
        "401": { $ref: "#/components/responses/AuthError" }

  /industries:
    get:
      operationId: listIndustries
      summary: List valid industry filters
      description: |
        Returns canonical industry labels and their accepted aliases.
        FREE and publicly accessible — no API key required.
        Response is cached for 24 hours.
      security: []
      responses:
        "200":
          description: Industry list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/IndustryFilter" }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key in format `idx_<32chars>`. Gold tier or higher required."

  schemas:
    MaskedEntity:
      type: object
      required: [id, _masked]
      properties:
        id: { type: string, format: uuid, description: Opaque entity UUID }
        _masked: { type: boolean, const: true }
        teaser:
          type: object
          description: >
            Non-identifying signals so you can decide what to reveal. Identity
            (name/slug/website) stays paywalled. Companies: industry,
            funding_band, hq_country, completeness_band. Investors:
            investor_type, aum_band, hq_country, completeness_band.
            completeness_band grades data quality A (50+ well-enriched),
            B (30-49), C (15-29), D (thin) — pairs with the minCompleteness filter.
          additionalProperties: { type: [string, "null"] }

    CompanyCard:
      type: object
      required: [id, name, slug, _revealed]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        logo_url: { type: [string, "null"] }
        website: { type: [string, "null"] }
        short_description: { type: [string, "null"] }
        industries: { type: [array, "null"], items: { type: string } }
        hq_city: { type: [string, "null"] }
        hq_country: { type: [string, "null"] }
        employee_count_range: { type: [string, "null"], enum: ["1-10", "11-50", "51-200", "201-500", "501-1000", "1001-5000", "5001-10000", "10001+", null] }
        total_funding_raised: { type: [integer, "null"], description: Whole USD }
        operating_status: { type: [string, "null"], enum: [active, acquired, ipo, closed, null] }
        _revealed: { type: boolean, const: true }

    InvestorCard:
      type: object
      required: [id, name, slug, _revealed]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        logo_url: { type: [string, "null"] }
        website: { type: [string, "null"] }
        investor_type: { type: [string, "null"], enum: [vc, pe, growth_equity, hedge_fund, cvc, family_office, sovereign_wealth, accelerator, incubator, angel_group, angel, individual, bank, other, null] }
        hq_city: { type: [string, "null"] }
        hq_country: { type: [string, "null"] }
        total_investments_count: { type: [integer, "null"] }
        _revealed: { type: boolean, const: true }

    CompanyProfile:
      type: object
      required: [id, name, slug]
      description: Shape varies by depth parameter (slim/standard/full)
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        logo_url: { type: [string, "null"] }
        website: { type: [string, "null"] }
        short_description: { type: [string, "null"] }
        long_description: { type: [string, "null"] }
        industries: { type: [array, "null"], items: { type: string } }
        hq_city: { type: [string, "null"] }
        hq_state: { type: [string, "null"] }
        hq_country: { type: [string, "null"] }
        employee_count_range: { type: [string, "null"] }
        total_funding_raised: { type: [integer, "null"], description: Whole USD }
        operating_status: { type: [string, "null"] }
        founded_year: { type: [integer, "null"] }
        valuationEvents:
          type: array
          description: Only present at depth=full
          items:
            type: object
            properties:
              event_type: { type: string }
              amount_raised_usd: { type: [integer, "null"] }
              announced_date: { type: [string, "null"], format: date }
              participants:
                type: array
                items:
                  type: object
                  properties:
                    is_lead: { type: boolean }
                    investor:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        name: { type: string }
                        slug: { type: string }
                        investor_type: { type: [string, "null"] }
        technologies:
          type: array
          description: Only present at depth=full
          items:
            type: object
            properties:
              technology_name: { type: string }
              category: { type: [string, "null"] }
              stack_type: { type: string, const: tech }
        appStack:
          type: array
          description: Only present at depth=full
          items:
            type: object
            properties:
              technology_name: { type: string }
              category: { type: [string, "null"] }
              stack_type: { type: string, const: app }

    InvestorProfile:
      type: object
      required: [id, name, slug]
      description: Shape varies by depth parameter (slim/standard/full)
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        logo_url: { type: [string, "null"] }
        website: { type: [string, "null"] }
        investor_type: { type: [string, "null"] }
        description: { type: [string, "null"] }
        hq_city: { type: [string, "null"] }
        hq_country: { type: [string, "null"] }
        total_investments_count: { type: [integer, "null"] }
        portfolio_company_count: { type: [integer, "null"] }
        portfolioCompanies:
          type: array
          description: Only present at depth=full
          items:
            type: object
            properties:
              companyName: { type: string }
              companySlug: { type: string }
              companyLogoUrl: { type: [string, "null"] }
              industries: { type: array, items: { type: string } }
              hqCountry: { type: [string, "null"] }
        investments:
          type: array
          description: Only present at depth=full
          items:
            type: object
            properties:
              companyName: { type: string }
              companySlug: { type: string }
              eventType: { type: string }
              date: { type: [string, "null"], format: date }
              amount: { type: [integer, "null"] }
              isLead: { type: boolean }
        subFunds:
          type: array
          description: Only present at depth=full
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              name: { type: string }
              slug: { type: string }

    MaskedPageMeta:
      type: object
      required: [total, page, limit, hasMore, revealed_count, masked_count]
      properties:
        total: { type: integer, description: "Total matches (always full count, regardless of reveal_status filter)" }
        page: { type: integer }
        limit: { type: integer }
        hasMore: { type: boolean }
        revealed_count: { type: integer }
        masked_count: { type: integer }
        all_revealed: { type: boolean, description: "Present when reveal_status=unrevealed but all matches are already revealed" }
        resolved_filters:
          type: object
          description: "Echo of resolved filter values (e.g., industries: ai-ml → AI/ML). Only present when filters are applied."
          additionalProperties: true

    RevealResult:
      type: object
      required: [id, entity_type, status, credits_charged]
      properties:
        id: { type: string, description: "Entity identifier (UUID if input was UUID, slug if input was slug)" }
        entity_type: { type: string, enum: [company, investor] }
        status: { type: string, enum: [revealed, cached, insufficient_credits, not_found] }
        credits_charged: { type: integer }
        data:
          type: object
          description: Full entity data (present when status is revealed or cached)

    RevealSummary:
      type: object
      required: [total_requested, total_revealed, total_cached, total_failed, total_credits_charged]
      properties:
        total_requested: { type: integer }
        total_revealed: { type: integer }
        total_cached: { type: integer }
        total_failed: { type: integer }
        total_credits_charged: { type: integer }

    UsageData:
      type: object
      properties:
        credits_included: { type: integer }
        credits_used: { type: integer }
        credits_remaining: { type: integer }
        pack_credits_remaining: { type: integer }
        overage_enabled: { type: boolean }
        overage_credits_used: { type: integer }
        period_start: { type: [string, "null"], format: date-time }
        period_end: { type: [string, "null"], format: date-time }
        tier: { type: string, enum: [free, silver, gold, platinum, enterprise] }
        limits:
          type: object
          properties:
            rate_limit_per_minute: { type: integer }
            api_keys_max: { type: integer }
            reveal_batch_size_max: { type: integer }
            reveal_hourly_max: { type: integer }

    IndustryFilter:
      type: object
      properties:
        label: { type: string, description: Canonical display label }
        aliases: { type: array, items: { type: string }, description: "Accepted alternative values (lowercase, slugs)" }
        db_variants_count: { type: integer, description: Number of DB industry values this label matches }

    ApiError:
      type: object
      required: [error, code]
      properties:
        error: { type: string, description: Human-readable error message }
        code: { type: string, description: Machine-readable error code }
        request_id: { type: string, format: uuid, description: Correlation ID (also in X-Request-Id header) }
        details:
          type: array
          description: Present for validation errors
          items:
            type: object
            properties:
              field: { type: string }
              message: { type: string }

  responses:
    ValidationError:
      description: Invalid request parameters
      headers:
        X-Request-Id: { schema: { type: string, format: uuid } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    AuthError:
      description: Missing, invalid, or insufficient API key
      headers:
        X-Request-Id: { schema: { type: string, format: uuid } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    NotFoundError:
      description: Entity not found
      headers:
        X-Request-Id: { schema: { type: string, format: uuid } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    PaymentRequired:
      description: >
        Credits exhausted or overage cap reached (code CREDIT_LIMIT_REACHED).
        This is a billing state, not a rate limit — do not retry; enable
        overage or purchase a credit pack.
      headers:
        X-Request-Id: { schema: { type: string, format: uuid } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    RateLimited:
      description: Rate limit exceeded (code RATE_LIMITED). Back off and retry after Retry-After.
      headers:
        X-Request-Id: { schema: { type: string, format: uuid } }
        X-RateLimit-Limit: { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
