openapi: 3.0.3
info:
  title: Fundable People API
  description: |
    API for searching and retrieving people (founders, executives, investors) with cross-type filtering.

    ## Authentication
    All API requests require authentication using an API key in the Authorization header.

    ## Rate Limits
    API usage is tracked and may be subject to monthly limits depending on your API key tier.

    ## Endpoint shape (POST)
    `POST /people` accepts a JSON body with an optional `person_type` selector and four filter
    sections. Filter intent can also be inferred from which sections have data, so cross-type
    queries (e.g. "founders at fintech companies who also invested in AI startups") work natively.

    Filter sections:
    - `identifiers` — look up specific people by UUID, LinkedIn, Crunchbase, or Twitter
    - `person` — role, contact info available, job title, education, prior employers
    - `company` — current employer attributes (industry, location, IPO status, funding, etc.).
      Includes nested `company.latest_deal` for the current employer's most recent round.
    - `investor` — investor firm attributes for people who invest. Includes nested
      `investor.deals` covering both the person's deal activity (size, date, lead-only,
      angel/institutional) AND the attributes of the portfolio companies they've invested in
      (industry, location, IPO status, semantic search).

    Plus pagination/sort at the top level: `page`, `page_size`, `sort_by`.

    ## Implicit constraints
    The endpoint inspects which sections have data and emits implicit WHERE clauses:

    | When you set | Backend constraint |
    |---|---|
    | Any `company.*` field (including `company.latest_deal.*`) | Person must have a current employer |
    | Any `investor.*` field (including `investor.deals.*`) | Person must be an investor |
    | Both sides | Both apply (true cross-type query) |
    | Neither | No constraint — paginated everyone |

    The top-level `person_type` field (`"company"`, `"investor"`, or `"any"` — default `"any"`)
    explicitly forces one population regardless of which filter blocks are present. Use it when
    you want only company-people (founders/employees) or only investor-people without having to
    add a filter block to imply it.

    ## Semantic search
    Two independent natural-language search fields. First use /industry/search and apply a
    structured taxonomy filter when an appropriate industry or super category exists. Use these
    fields only for a **niche industry or specific product type** the taxonomy cannot express,
    describing what a company does — its product, technology, or business model — rather than
    attributes like stage, location, or funding (apply dedicated filter fields instead):
    - `company.search_query` — semantic match against the person's current employer
    - `investor.deals.search_query` — semantic match against companies the person has invested in

    Both can fire simultaneously. Each accepts an optional `min_relevance` (0..1) to filter out
    weak matches. When semantic search is active, results are ordered by relevance and any
    explicit `sort_by` is ignored. Each semantic result set is capped at 150 matches.

    ## Response shape
    Every row carries every field. Investor-context aggregates (deal counts, top industries / locations / round types,
    most recent deal date) live under a single `investor_highlights` object. For non-investors,
    `investor_highlights` is `null` and `is_investor` / `is_angel` / `has_led_deal` are `false`.
    Pure employees and pure angels have the same shape — only the values differ.

    ## Person identifiers (for `/person`)
    Detail endpoint uses query parameters — exactly one of `?id=`, `?linkedin=`,
    `?crunchbase=`, or `?twitter=`. Examples:
    - `GET /person?id=550e8400-e29b-41d4-a716-446655440000`
    - `GET /person?linkedin=https://linkedin.com/in/jane-doe-12345`
    - `GET /person?crunchbase=https://crunchbase.com/person/jane-doe`
    - `GET /person?twitter=https://x.com/janedoe`

    ## Money units
    All monetary values in request and response are in **actual USD** (not millions). For example,
    `total_raised_min: 10000000` means $10M.

  version: 1.0.0
  contact:
    name: Fundable API Support
    url: jacob@tryfundable.ai
  license:
    name: Proprietary
    url: https://www.tryfundable.ai/terms/privacy/

servers:
  - url: https://www.tryfundable.ai/api/v1
    description: Production server

security:
  - bearerAuth: []

paths:
  /people:
    post:
      summary: Discover founders, employees, angels, and lead investors
      description: |
        Use this endpoint when the requested result is a list of people.

        With `person_type: company`, it returns founders, executives, or employees based on
        their current employer. Filters under `company.latest_deal` apply only to that
        employer's latest funding round, like `POST /companies`; funding information is used
        for filtering but is not included in the returned person record. For example, use
        this mode to find founders of AI companies in San Francisco whose latest round was
        Series A.

        With `person_type: investor`, it applies investment and portfolio filters in the same
        way that `POST /investors` does, but returns individual angel investors or lead
        partners attached to their firms rather than firm records. Use `POST /investors`
        when the desired result is firms, and `POST /deals` when it is funding rounds.

        ## Request Body Structure
        Send a JSON body with up to 4 filter sections plus pagination/sort at the top level:
        - `identifiers` - Look up specific people by UUID, LinkedIn, Crunchbase, or Twitter
        - `person` - Filter by person attributes (role, contact info, job title, education, prior employers)
        - `company` - Filter by current employer attributes (industry, location, funding, IPO status, etc.)
        - `investor` - Filter by investor activity (firm, deal characteristics, portfolio companies)
      operationId: searchPeople
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              example:
                page_size: 10
              properties:
                person_type:
                  type: string
                  enum: [company, investor, any]
                  default: any
                  description: |
                    Restrict results to one population. `company` returns only people with a
                    current employer (founders, executives, employees). `investor` returns only
                    people who invest. `any` (default) returns the union — anyone matching the
                    other filter blocks regardless of population.
                  example: company
                identifiers:
                  type: object
                  description: Look up specific people by identifier
                  properties:
                    ids:
                      type: array
                      description: Person UUIDs
                      items:
                        type: string
                        format: uuid
                      example: ["550e8400-e29b-41d4-a716-446655440000"]
                    linkedin_urls:
                      type: array
                      description: LinkedIn person URLs (max 100)
                      items:
                        type: string
                      example: ["https://linkedin.com/in/jane-doe-12345", "https://linkedin.com/in/john-smith"]
                    crunchbase_urls:
                      type: array
                      description: Crunchbase person URLs (max 100)
                      items:
                        type: string
                      example: ["https://crunchbase.com/person/jane-doe", "https://www.crunchbase.com/person/john-smith"]
                    twitter_urls:
                      type: array
                      description: Twitter/X URLs (max 100)
                      items:
                        type: string
                      example: ["https://x.com/janedoe", "https://x.com/johnsmith"]
                person:
                  type: object
                  description: Person attribute filters
                  properties:
                    roles:
                      type: array
                      description: Person's role at their current employer
                      items:
                        type: string
                        enum: ['founder', 'ceo', 'key_person']
                      example: ["founder", "ceo"]
                    contact_types:
                      type: array
                      description: Available contact info on the person
                      items:
                        type: string
                        enum: ['linkedin', 'email', 'phone', 'twitter']
                      example: ["email", "linkedin"]
                    job_titles:
                      type: array
                      description: Normalized job-title keys
                      items:
                        type: string
                      example: ["ceo", "cto", "vp-engineering"]
                    education_schools:
                      type: array
                      description: Education institution IDs
                      items:
                        type: string
                      example: ["stanford-university", "mit"]
                    linkedin_companies:
                      type: array
                      description: Filter by previous/current employer LinkedIn company IDs
                      items:
                        type: string
                      example: ["google", "meta"]
                company:
                  type: object
                  description: Current-employer attribute filters. Setting any field implicitly requires the person to have a current employer.
                  properties:
                    search_query:
                      type: string
                      description: >-
                        AI-powered semantic search against the person's current employer —
                        a flexible replacement for an industry tag. First use /industry/search
                        and apply an `industries` or `super_categories` filter when the taxonomy
                        contains the intended category. Use semantic search only for a niche
                        industry or product type the taxonomy cannot express, describing what
                        that company does — its product, technology, or business model (e.g.
                        "climate tech carbon accounting software"). Do NOT use it for stage,
                        location, or funding — apply dedicated filters instead. Results are
                        ordered by relevance and capped at 150 semantic matches.
                      example: "climate tech carbon accounting software"
                    min_relevance:
                      type: number
                      minimum: 0
                      maximum: 1
                      description: Minimum similarity threshold (0-1). Only applies with search_query.
                      example: 0.5
                    ids:
                      type: array
                      description: Filter by current-employer UUIDs
                      items:
                        type: string
                        format: uuid
                      example: ["660e8400-e29b-41d4-a716-446655440000"]
                    locations:
                      type: array
                      description: Location permalinks. Use /location/search to find permalinks. A value that is not an exact permalink is silently ignored (returns zero results, no error).
                      items:
                        type: string
                      example: ["san-francisco-california", "new-york-new-york"]
                    industries:
                      type: array
                      description: Industry permalinks. Use /industry/search to find permalinks. A value that is not an exact permalink is silently ignored (returns zero results, no error).
                      items:
                        type: string
                      example: ["artificial-intelligence", "fintech-e067"]
                    super_categories:
                      type: array
                      description: Super category permalinks (auto-includes related industries). Use /industry/search to find permalinks. A value that is not an exact permalink is silently ignored (returns zero results, no error).
                      items:
                        type: string
                      example: ["financial-services-d7b6"]
                    employee_count:
                      type: array
                      description: Current-employer headcount ranges
                      items:
                        type: string
                        enum: ['1-10', '11-50', '51-100', '101-250', '251-500', '501-1000', '1001-5000', '5001-10000', '10001+']
                      example: ["11-50", "51-100"]
                    ipo_status:
                      type: array
                      description: Current-employer IPO status
                      items:
                        type: string
                        enum: ['public', 'private', 'acquired', 'delisted']
                      example: ["private"]
                    total_raised_min:
                      type: number
                      minimum: 0
                      description: Minimum total raised by current employer (USD)
                      example: 1000000
                    total_raised_max:
                      type: number
                      minimum: 0
                      description: Maximum total raised by current employer (USD)
                      example: 100000000
                    investor_people_ids:
                      type: array
                      description: |
                        Person UUIDs — filter to people whose current employer has ever raised
                        from any of these people as an **angel investor** OR as a **lead partner**
                        of an investing firm. Resolve via `/people/{id}`.
                      items:
                        type: string
                        format: uuid
                      example: ["b3e9c7d1-2f4a-4d5e-9c1b-7a8f3e2d1c6a"]
                    latest_deal:
                      type: object
                      description: Nested. Filters on the current employer's most recent funding round.
                      properties:
                        financing_types:
                          type: array
                          description: Financing types with optional modifiers (pre, extension)
                          items:
                            type: object
                            required: [type]
                            properties:
                              type:
                                type: string
                                enum: [
                                  'SERIES_A', 'SERIES_B', 'SERIES_C', 'SERIES_D', 'SERIES_E', 'SERIES_F',
                                  'SERIES_G', 'SERIES_H', 'SERIES_I', 'SERIES_J', 'SERIES_K', 'SERIES_L',
                                  'SERIES_M', 'SEED', 'SAFE', 'CONVERTIBLE_NOTE', 'EQUITY', 'PREFERRED',
                                  'SECONDARY_MARKET', 'DEBT_FINANCING', 'GRANT', 'NON_EQUITY_ASSISTANCE',
                                  'CROWDFUNDING', 'INITIAL_COIN_OFFERING', 'FUNDING_ROUND'
                                ]
                              pre:
                                type: boolean
                                default: false
                              extension:
                                type: boolean
                                default: false
                          example:
                            - { "type": "SEED", "pre": true }
                            - { "type": "SERIES_A" }
                        size_min:
                          type: number
                          minimum: 0
                          description: Minimum latest-deal size (USD)
                          example: 1000000
                        size_max:
                          type: number
                          minimum: 0
                          description: Maximum latest-deal size (USD)
                          example: 50000000
                        date_start:
                          type: string
                          format: date
                          example: "2024-01-01"
                        date_end:
                          type: string
                          format: date
                          example: "2024-12-31"
                        investor_ids:
                          type: array
                          description: Filter to deals where these investor UUIDs participated
                          items:
                            type: string
                            format: uuid
                          example: ["770e8400-e29b-41d4-a716-446655440000"]
                        investor_locations:
                          type: array
                          description: Filter to deals with investors HQ'd in these locations
                          items:
                            type: string
                          example: ["san-francisco-california"]
                investor:
                  type: object
                  description: |
                    Filters on the investor entity (firm) and on the person's deal activity.
                    Setting any field (including any nested `deals.*` field) implicitly requires
                    the person to be an investor.
                  properties:
                    ids:
                      type: array
                      description: Firm (parent investor) UUIDs
                      items:
                        type: string
                        format: uuid
                      example: ["c5a3f6ac-f6c9-4686-aa6e-12aeb7419b82"]
                    domains:
                      type: array
                      description: Firm domains (max 100)
                      items:
                        type: string
                      example: ["sequoiacap.com", "a16z.com"]
                    linkedin_urls:
                      type: array
                      description: Firm LinkedIn URLs (max 100)
                      items:
                        type: string
                      example: ["https://linkedin.com/company/sequoia-capital"]
                    crunchbase_urls:
                      type: array
                      description: Firm Crunchbase URLs (max 100)
                      items:
                        type: string
                      example: ["https://crunchbase.com/organization/sequoia-capital"]
                    permalinks:
                      type: array
                      description: Fundable firm permalinks (max 100)
                      items:
                        type: string
                      example: ["sequoia-capital", "andreessen-horowitz"]
                    locations:
                      type: array
                      description: Firm HQ location permalinks
                      items:
                        type: string
                      example: ["menlo-park-california"]
                    employee_count:
                      type: array
                      description: Firm headcount ranges
                      items:
                        type: string
                        enum: ['1-10', '11-50', '51-100', '101-250', '251-500', '501-1000', '1001-5000', '5001-10000', '10001+']
                    deals:
                      type: object
                      description: |
                        Nested. Combined deal-activity and portfolio-company filters. Includes:
                        deal-level controls (`investment_type`, `min_matching_deals`, `only_lead_deals`),
                        round-level filters (`financing_types`, `size_min/max`, `date_start/end`), and
                        portfolio-company attributes (`industries`, `super_categories`,
                        `portfolio_locations`, `portfolio_employee_count`, `ipo_status`,
                        `total_raised_min/max`, plus semantic `search_query` / `min_relevance`).
                      properties:
                        search_query:
                          type: string
                          description: >-
                            AI-powered semantic search against the person's portfolio
                            companies — a flexible replacement for an industry tag. First use
                            /industry/search and apply an `industries` or `super_categories`
                            filter when the taxonomy contains the intended category. Use
                            semantic search only for a niche industry or product type the
                            taxonomy cannot express, describing what those companies do (e.g.
                            "AI infrastructure for model training"). Do NOT use it for stage,
                            location, or funding — apply dedicated filters instead. Results are
                            ordered by relevance and capped at 150 semantic matches.
                          example: "AI infrastructure for model training"
                        min_relevance:
                          type: number
                          minimum: 0
                          maximum: 1
                          description: Minimum similarity threshold (0-1). Only applies with search_query.
                          example: 0.5
                        investment_type:
                          type: string
                          enum: ['all', 'angel', 'institutional']
                          default: 'all'
                          description: Filter by whether the person invests as an angel, under a firm, or both
                          example: "angel"
                        only_lead_deals:
                          type: boolean
                          default: false
                          description: Only count deals where the person/firm was a lead investor
                        min_matching_deals:
                          type: integer
                          minimum: 1
                          description: Require at least this many deals to match the deal filters
                          example: 3
                        financing_types:
                          type: array
                          description: Financing types for the person's investment activity
                          items:
                            type: object
                            required: [type]
                            properties:
                              type:
                                type: string
                                enum: [
                                  'SERIES_A', 'SERIES_B', 'SERIES_C', 'SERIES_D', 'SERIES_E', 'SERIES_F',
                                  'SERIES_G', 'SERIES_H', 'SERIES_I', 'SERIES_J', 'SERIES_K', 'SERIES_L',
                                  'SERIES_M', 'SEED', 'SAFE', 'CONVERTIBLE_NOTE', 'EQUITY', 'PREFERRED',
                                  'SECONDARY_MARKET', 'DEBT_FINANCING', 'GRANT', 'NON_EQUITY_ASSISTANCE',
                                  'CROWDFUNDING', 'INITIAL_COIN_OFFERING', 'FUNDING_ROUND'
                                ]
                              pre:
                                type: boolean
                                default: false
                              extension:
                                type: boolean
                                default: false
                        size_min:
                          type: number
                          minimum: 0
                          description: Minimum deal size (USD) for the person's deal activity
                          example: 500000
                        size_max:
                          type: number
                          minimum: 0
                          description: Maximum deal size (USD) for the person's deal activity
                          example: 10000000
                        date_start:
                          type: string
                          format: date
                          example: "2023-01-01"
                        date_end:
                          type: string
                          format: date
                          example: "2024-12-31"
                        industries:
                          type: array
                          description: Portfolio company industry permalinks
                          items:
                            type: string
                          example: ["artificial-intelligence"]
                        super_categories:
                          type: array
                          description: Portfolio company super-category permalinks
                          items:
                            type: string
                        portfolio_locations:
                          type: array
                          description: Portfolio company location permalinks
                          items:
                            type: string
                        portfolio_employee_count:
                          type: array
                          description: Portfolio company headcount ranges
                          items:
                            type: string
                            enum: ['1-10', '11-50', '51-100', '101-250', '251-500', '501-1000', '1001-5000', '5001-10000', '10001+']
                        ipo_status:
                          type: array
                          description: Portfolio company IPO status
                          items:
                            type: string
                            enum: ['public', 'private']
                        total_raised_min:
                          type: number
                          minimum: 0
                          description: Minimum total raised by the portfolio company (USD)
                          example: 1000000
                        total_raised_max:
                          type: number
                          minimum: 0
                          description: Maximum total raised by the portfolio company (USD)
                page:
                  type: integer
                  minimum: 0
                  default: 0
                  description: Page number (0-based)
                page_size:
                  type: integer
                  minimum: 1
                  maximum: 500
                  default: 10
                  description: Number of results per page (max 500)
                sort_by:
                  type: string
                  enum:
                    - 'most_recent_deal_date'
                    - 'total_raised'
                    - 'latest_deal_size'
                    - 'name'
                    - 'total_deal_count'
                    - 'deal_count_last_12_months'
                    - 'lead_deal_count'
                    - 'lead_deal_count_last_12_months'
                    - 'filtered_deal_count'
                  default: 'most_recent_deal_date'
                  description: |
                    Sort order. All values are universally accepted — non-applicable values (e.g. `lead_deal_count`
                    when no investor filter is set) resolve to 0 / null via COALESCE and naturally sort to the bottom (DESC).
                    Ignored when `company.search_query` or `investor.deals.search_query` is set (results are then
                    ordered by semantic relevance).
            examples:
              empty:
                summary: Empty request (no filters)
                value:
                  page_size: 5
              filter_only:
                summary: Founders at private financial-services companies
                value:
                  person:
                    roles: ["founder"]
                  company:
                    ipo_status: ["private"]
                    super_categories: ["financial-services-d7b6"]
                  page_size: 25
              batch_lookup:
                summary: Look up specific people by LinkedIn URL
                value:
                  identifiers:
                    linkedin_urls: ["https://www.linkedin.com/in/joshua-kushner-711b45230/", "https://www.linkedin.com/in/eladgil/"]
              angel_investors_active_2024:
                summary: People who made at least 3 angel investments in 2024
                value:
                  investor:
                    deals:
                      investment_type: "angel"
                      min_matching_deals: 3
                      date_start: "2024-01-01"
                      date_end: "2024-12-31"
                  sort_by: "lead_deal_count"
                  page_size: 50

      responses:
        '200':
          description: Successful search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    description: Optional friendly message (e.g. "No people found matching your filters" when empty)
                  data:
                    type: object
                    properties:
                      people:
                        type: array
                        items:
                          $ref: '#/components/schemas/PersonSearchResult'
                  meta:
                    type: object
                    properties:
                      total_count:
                        type: integer
                        description: Total number of people matching the filter criteria
                        example: 1250
                      page:
                        type: integer
                        description: Current page number (0-based)
                        example: 0
                      page_size:
                        type: integer
                        description: Number of results per page
                        example: 25
                      credits_used:
                        type: integer
                        description: Number of credits consumed by this request
                        example: 25
                      credit_source:
                        type: string
                        nullable: true
                        enum: ['monthly', 'purchased']
                        description: Source of credits used (only included for non-API tier keys)
                        example: "monthly"
                      monthly_credits_remaining:
                        type: integer
                        nullable: true
                        description: Remaining monthly credits (only included for non-API tier keys)
                        example: 990
                      purchased_credits_remaining:
                        type: integer
                        nullable: true
                        description: Remaining purchased credits (only included for non-API tier keys)
                        example: 500
                required:
                  - success
                  - data
                  - meta
        '400':
          description: Validation error (invalid types, dates, enums, or empty `search_query`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '401':
          description: Authentication error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
        '402':
          description: Insufficient credits to complete request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsufficientCreditsError'
        '422':
          description: Unknown parameter (per-block whitelist enforced)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnknownParamError'
        '429':
          description: |
            Two distinct cases share this status:
            - `RATE_LIMIT_EXCEEDED` — per-minute API rate limit
            - `USAGE_LIMIT_EXCEEDED` — semantic-search monthly usage limit (only when `company.search_query` or `investor.deals.search_query` was set)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/RateLimitError'
                  - $ref: '#/components/schemas/SemanticUsageLimitError'
          headers:
            Retry-After:
              description: Seconds to wait before retrying (sent for `RATE_LIMIT_EXCEEDED` only)
              schema:
                type: integer
                example: 60
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerError'
        '504':
          description: Semantic search timed out (only when `company.search_query` or `investor.deals.search_query` was set)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SemanticTimeoutError'

  /person:
    get:
      summary: Get person details by identifier
      description: |
        Use this endpoint when one person is already identified and their full profile is
        required. Prefer a LinkedIn URL when available; it can be passed directly without
        calling `/person/search` first.

        Use `/person/search` for fuzzy name resolution or an uncertain identifier. Use
        `POST /people` to discover or batch-retrieve multiple people.

        Provide ONE of the following query parameters to identify the person:
        - `id` - Person UUID
        - `linkedin` - LinkedIn person URL
        - `crunchbase` - Crunchbase person URL
        - `twitter` - Twitter/X URL

        Returns the same constant unified shape as the list endpoint, with **full**
        employment and education history (no per-section limit).
      operationId: getPerson
      parameters:
        - name: id
          in: query
          required: false
          description: Person UUID
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: linkedin
          in: query
          required: false
          description: LinkedIn person URL
          schema:
            type: string
          example: "https://www.linkedin.com/in/eladgil/"
        - name: crunchbase
          in: query
          required: false
          description: Crunchbase person URL
          schema:
            type: string
          example: "https://crunchbase.com/person/jane-doe"
        - name: twitter
          in: query
          required: false
          description: Twitter/X URL
          schema:
            type: string
          example: "https://x.com/janedoe"
      responses:
        '200':
          description: Successful response with person data
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    required: [person]
                    properties:
                      person:
                        $ref: '#/components/schemas/PersonDetail'
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                        example: 0
                      page_size:
                        type: integer
                        example: 1
                      credits_used:
                        type: integer
                        example: 1
                      credit_source:
                        type: string
                        nullable: true
                        enum: ['monthly', 'purchased']
                      monthly_credits_remaining:
                        type: integer
                        nullable: true
                      purchased_credits_remaining:
                        type: integer
                        nullable: true
        '400':
          description: Missing or invalid identifier (no query param provided, or invalid UUID / URL)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvalidIdentifierError'
        '401':
          description: Authentication error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
        '402':
          description: Insufficient credits to complete request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsufficientCreditsError'
        '404':
          description: Person not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotFoundError'
        '429':
          description: Rate limit exceeded (per-minute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
          headers:
            Retry-After:
              schema:
                type: integer
                example: 60
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerError'

  /person/email:
    get:
      summary: Unlock a person's verified email by identifier
      description: |
        Use this endpoint only when a verified email is explicitly required for an identified
        person. Return a verified email for a person identified by UUID, LinkedIn, Crunchbase,
        or Twitter/X. Provide one identifier. If multiple identifiers are supplied, the
        endpoint uses the first non-empty value in this order: `id`, `linkedin`,
        `crunchbase`, `twitter`.

        A newly created unlock costs 5 credits. Repeating the request for an email the
        same user already unlocked costs 0 credits. Unlock ownership persists across API
        keys belonging to that account. Provider attribution is internal and is not returned.

        Available on non-trial Pro+, Enterprise, and API plans.
      operationId: getPersonEmail
      parameters:
        - name: id
          in: query
          required: false
          description: Fundable person UUID
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: linkedin
          in: query
          required: false
          description: LinkedIn person URL, slug, or supported normalized identifier
          schema:
            type: string
          example: "https://www.linkedin.com/in/eladgil/"
        - name: crunchbase
          in: query
          required: false
          description: Crunchbase person URL or supported normalized identifier
          schema:
            type: string
          example: "https://www.crunchbase.com/person/jane-doe"
        - name: twitter
          in: query
          required: false
          description: Twitter/X URL or handle
          schema:
            type: string
          example: "https://x.com/janedoe"
      responses:
        '200':
          description: Verified email returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonEmailResponse'
        '400':
          description: Missing or invalid person identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonEmailError'
              example:
                success: false
                error:
                  code: MISSING_IDENTIFIER
                  message: Person identifier is required
                  details:
                    help: Provide id, linkedin, crunchbase, or twitter.
                statusCode: 400
        '401':
          description: Authentication error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
        '402':
          description: Insufficient credits or monthly email-unlock allowance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonEmailError'
              example:
                success: false
                error:
                  code: INSUFFICIENT_CREDITS
                  message: Not enough credits to unlock this email
                  details:
                    credits_needed: 5
                    monthly_credits_remaining: 0
                    purchased_credits_remaining: 0
                statusCode: 402
        '403':
          description: Current subscription or trial status cannot unlock emails
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonEmailError'
              example:
                success: false
                error:
                  code: UPGRADE_REQUIRED
                  message: Email unlocks require a Pro+ or Enterprise plan.
                  details:
                    current_tier: Pro
                statusCode: 403
        '404':
          description: Person not found or no verified email available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonEmailError'
              examples:
                person_not_found:
                  summary: Person not found
                  value:
                    success: false
                    error:
                      code: PERSON_NOT_FOUND
                      message: Person not found
                    statusCode: 404
                email_not_found:
                  summary: No verified email found
                  value:
                    success: false
                    error:
                      code: EMAIL_NOT_FOUND
                      message: No verified email was found for this person
                    statusCode: 404
        '429':
          description: Rate limit exceeded (per-minute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
          headers:
            Retry-After:
              description: Seconds to wait before retrying
              schema:
                type: integer
                example: 60
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerError'

  /person/phone:
    get:
      summary: Unlock a person's verified phone number by identifier
      description: |
        Use this endpoint only when a verified phone number is explicitly required for an identified
        person. Return a verified phone number for a person identified by UUID, LinkedIn, Crunchbase,
        or Twitter/X. Provide one identifier. If multiple identifiers are supplied, the
        endpoint uses the first non-empty value in this order: `id`, `linkedin`,
        `crunchbase`, `twitter`.

        A newly created unlock costs 5 credits. Repeating the request for a phone number the
        same account already unlocked costs 0 credits. Unlock ownership persists across API
        keys belonging to that account. Provider attribution is internal and is not returned.

        Available on non-trial Pro+, Enterprise, and API plans.
      operationId: getPersonPhone
      parameters:
        - name: id
          in: query
          required: false
          description: Fundable person UUID
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: linkedin
          in: query
          required: false
          description: LinkedIn person URL, slug, or supported normalized identifier
          schema:
            type: string
          example: "https://www.linkedin.com/in/eladgil/"
        - name: crunchbase
          in: query
          required: false
          description: Crunchbase person URL or supported normalized identifier
          schema:
            type: string
          example: "https://www.crunchbase.com/person/jane-doe"
        - name: twitter
          in: query
          required: false
          description: Twitter/X URL or handle
          schema:
            type: string
          example: "https://x.com/janedoe"
      responses:
        '200':
          description: Verified phone returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonPhoneResponse'
        '400':
          description: Missing or invalid person identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonPhoneError'
              example:
                success: false
                error:
                  code: MISSING_IDENTIFIER
                  message: Person identifier is required
                  details:
                    help: Provide id, linkedin, crunchbase, or twitter.
                statusCode: 400
        '401':
          description: Authentication error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
        '402':
          description: Insufficient credits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonPhoneError'
              example:
                success: false
                error:
                  code: INSUFFICIENT_CREDITS
                  message: Not enough credits to unlock this phone number
                  details:
                    credits_needed: 5
                    monthly_credits_remaining: 0
                    purchased_credits_remaining: 0
                statusCode: 402
        '403':
          description: Current subscription or trial status cannot unlock phones
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonPhoneError'
              example:
                success: false
                error:
                  code: UPGRADE_REQUIRED
                  message: Phone unlocks require a Pro+, Enterprise, or API plan.
                  details:
                    current_tier: Pro
                statusCode: 403
        '404':
          description: Person not found or no verified phone number available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonPhoneError'
              examples:
                person_not_found:
                  summary: Person not found
                  value:
                    success: false
                    error:
                      code: PERSON_NOT_FOUND
                      message: Person not found
                    statusCode: 404
                phone_not_found:
                  summary: No verified phone number found
                  value:
                    success: false
                    error:
                      code: PHONE_NOT_FOUND
                      message: No verified phone was found for this person
                    statusCode: 404
        '429':
          description: Rate limit exceeded (per-minute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
          headers:
            Retry-After:
              description: Seconds to wait before retrying
              schema:
                type: integer
                example: 60
        '503':
          description: Phone enrichment or credit accounting is temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonPhoneError'
              examples:
                enrichment_unavailable:
                  value:
                    success: false
                    error:
                      code: CONTACT_ENRICHMENT_UNAVAILABLE
                      message: Phone enrichment is temporarily unavailable. Try again shortly.
                    statusCode: 503
                credit_accounting_unavailable:
                  value:
                    success: false
                    error:
                      code: CREDIT_ACCOUNTING_UNAVAILABLE
                      message: Credit accounting is temporarily unavailable. Try again shortly.
                    statusCode: 503
          headers:
            Retry-After:
              description: Seconds to wait before retrying
              schema:
                type: integer
                example: 5
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerError'

  /person/deals:
    get:
      summary: Get deals a person participated in (as an investor) by identifier
      description: |
        Use this endpoint when a person is already identified and their investment history
        is required. It returns deals the person joined as an angel plus lead or firm deals.
        Prefer a LinkedIn URL when available; it can be passed directly without first calling
        `/person/search`.

        Provide ONE of the following query parameters to identify the person:
        - `id` - Person UUID
        - `linkedin` - LinkedIn person URL
        - `crunchbase` - Crunchbase person URL
        - `twitter` - Twitter/X URL

        The response includes the person's investment deals with pagination.
      operationId: getPersonDeals
      parameters:
        - name: id
          in: query
          description: Person UUID
          required: false
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: linkedin
          in: query
          description: LinkedIn person URL
          required: false
          schema:
            type: string
          example: "https://www.linkedin.com/in/eladgil/"
        - name: crunchbase
          in: query
          description: Crunchbase person URL
          required: false
          schema:
            type: string
          example: "https://crunchbase.com/person/jane-doe"
        - name: twitter
          in: query
          description: Twitter/X URL
          required: false
          schema:
            type: string
          example: "https://x.com/janedoe"
        - name: page
          in: query
          description: Page number (0-based)
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
        - name: page_size
          in: query
          description: Number of results per page (1-500)
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 10
          example: 10
      responses:
        '200':
          description: Successful response with the person's deals
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      deals:
                        type: array
                        items:
                          $ref: '#/components/schemas/PersonDeal'
                  meta:
                    type: object
                    properties:
                      total_count:
                        type: integer
                        example: 11
                      page:
                        type: integer
                        example: 0
                      page_size:
                        type: integer
                        example: 10
                required:
                  - success
                  - data
                  - meta
        '400':
          description: Validation error (missing or invalid identifier)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        enum: [MISSING_IDENTIFIER, INVALID_ID]
                        example: "MISSING_IDENTIFIER"
                      message:
                        type: string
                      details:
                        type: object
        '401':
          description: Authentication error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
        '404':
          description: No person found matching the provided identifier
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        enum: [PERSON_NOT_FOUND]
                        example: "PERSON_NOT_FOUND"
                      message:
                        type: string
                        example: "Person not found"
        '402':
          description: Insufficient credits to complete request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsufficientCreditsError'
        '429':
          description: Rate limit exceeded (per-minute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerError'

  /person/search:
    get:
      summary: Search for a person by name or identifier
      description: |
        Use this endpoint primarily to resolve a fuzzy person name or uncertain identifier.
        If a reliable LinkedIn URL is already available, skip this search and pass it directly
        to `GET /person` for one full profile or to `POST /people` for a batch lookup.

        Exactly one parameter must be provided.

        **Search types:**
        - `name` — Fuzzy name search across both investors AND non-investor people (founders, employees). Returns up to 10 results.
        - `id` — Person UUID lookup (returns 0 or 1)
        - `linkedin` — LinkedIn person URL or slug
        - `crunchbase` — Crunchbase person URL or slug
        - `twitter` — Twitter/X URL or handle

        `person_type` on each result indicates which pool the row came from
        (`investor` wins when a person is in both). For full person detail, use `/person`.
      operationId: searchPerson
      parameters:
        - name: name
          in: query
          required: false
          description: Fuzzy search by person name
          schema:
            type: string
          example: "jane doe"
        - name: id
          in: query
          required: false
          description: Person UUID
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: linkedin
          in: query
          required: false
          description: LinkedIn person URL
          schema:
            type: string
          example: "https://www.linkedin.com/in/eladgil/"
        - name: crunchbase
          in: query
          required: false
          description: Crunchbase person URL
          schema:
            type: string
          example: "https://crunchbase.com/person/jane-doe"
        - name: twitter
          in: query
          required: false
          description: Twitter/X URL
          schema:
            type: string
          example: "https://x.com/janedoe"
        - name: person_type
          in: query
          required: false
          description: |
            Optional filter restricting results to one pool. `investor` keeps only
            people tracked as investors/angels; `company` keeps only non-investor
            people (founders, CEOs, employees). Omit to search both. Applies to
            both name and identifier modes — identifier lookups return empty if
            the resolved person doesn't match the requested type.
          schema:
            type: string
            enum: [investor, company]
          example: "investor"
      responses:
        '200':
          description: Successful search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      people:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              format: uuid
                            name:
                              type: string
                            linkedin_url:
                              type: string
                              nullable: true
                              format: uri
                            crunchbase_url:
                              type: string
                              nullable: true
                              format: uri
                            twitter_url:
                              type: string
                              nullable: true
                              format: uri
                            person_type:
                              type: string
                              enum: [investor, company]
                              description: Which pool the result came from. Mirrors the request-body discriminator on POST /people.
                  meta:
                    type: object
                    properties:
                      total_count:
                        type: integer
                        example: 7
                required:
                  - success
                  - data
                  - meta
        '400':
          description: Validation error (missing or multiple parameters)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        enum: [MISSING_SEARCH_PARAMS, MULTIPLE_SEARCH_PARAMS, INVALID_ID]
                        example: "MISSING_SEARCH_PARAMS"
                      message:
                        type: string
                      details:
                        type: object
        '401':
          description: Authentication error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
        '402':
          description: Insufficient credits to complete request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsufficientCreditsError'
        '429':
          description: Rate limit exceeded (per-minute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
          headers:
            Retry-After:
              description: Number of seconds to wait before retrying
              schema:
                type: integer
                example: 60
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServerError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        API key authentication using Bearer token format.
        Format: `Authorization: Bearer vg_your_api_key_here`

  schemas:
    PersonSearchResult:
      type: object
      description: |
        Constant unified person shape — every row carries every field. Investor aggregates live
        under `investor_highlights` (null for non-investors); discriminator flags `is_investor` /
        `is_angel` / `has_led_deal` and `investment_firms` stay top-level. `employment_history`
        and `education_history` are capped at 3 in the search response; the detail endpoint
        returns the full lists.
      properties:
        id:
          type: string
          format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
        name:
          type: string
          nullable: true
          example: "Jane Doe"
        title:
          type: string
          nullable: true
          description: Current job title
          example: "CEO"
        linkedin_url:
          type: string
          nullable: true
          format: uri
          description: Canonical LinkedIn person URL
        crunchbase_url:
          type: string
          nullable: true
          format: uri
          description: Canonical Crunchbase person URL
        twitter_url:
          type: string
          nullable: true
          format: uri
          description: Canonical Twitter/X profile URL
        location:
          type: string
          nullable: true
          example: "San Francisco, CA"
        city:
          type: string
          nullable: true
        country_code:
          type: string
          nullable: true
          example: "US"
        about:
          type: string
          nullable: true
          description: LinkedIn bio/about section
        is_founder:
          type: boolean
        current_company:
          $ref: '#/components/schemas/PersonCurrentCompany'
        employment_history:
          type: array
          description: Recent employment (max 3 items in search; full list on detail endpoint)
          maxItems: 3
          items:
            $ref: '#/components/schemas/EmploymentRecord'
        education_history:
          type: array
          description: Education records (max 3 items in search; full list on detail endpoint)
          maxItems: 3
          items:
            $ref: '#/components/schemas/EducationRecord'
        is_investor:
          type: boolean
          description: Whether the person has any tracked investment activity
        is_angel:
          type: boolean
          description: Whether the person makes angel investments
        has_led_deal:
          type: boolean
          description: Whether the person has ever led a deal
        investment_firms:
          type: array
          description: Firms the person invests under (empty for pure angels)
          items:
            $ref: '#/components/schemas/Firm'
        investor_highlights:
          nullable: true
          description: Aggregated investor activity. Null when `is_investor` is false.
          $ref: '#/components/schemas/InvestorHighlights'
        filtered_deal_count:
          type: integer
          nullable: true
          description: Count of deals matching the `investor.deals.*` filters. Populated only when any `investor.deals.*` filter is present; otherwise null.
        filtered_lead_count:
          type: integer
          nullable: true
          description: Of the matching deals, how many the person/firm led. Populated only when any `investor.deals.*` filter is present.
        filtered_most_recent_date:
          type: string
          format: date
          nullable: true
          description: Most recent matching-deal date. Populated only when any `investor.deals.*` filter is present.

    PersonDetail:
      allOf:
        - $ref: '#/components/schemas/PersonSearchResult'
        - type: object
          description: Detail endpoint returns the same shape as search but with full (unbounded) employment and education history.
          properties:
            employment_history:
              type: array
              description: Full employment history (no item cap)
              items:
                $ref: '#/components/schemas/EmploymentRecord'
            education_history:
              type: array
              description: Full education history (no item cap)
              items:
                $ref: '#/components/schemas/EducationRecord'

    PersonEmailResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          required: [person_id, email, already_unlocked]
          properties:
            person_id:
              type: string
              format: uuid
              example: "550e8400-e29b-41d4-a716-446655440000"
            email:
              type: string
              format: email
              example: "jane@example.com"
            already_unlocked:
              type: boolean
              description: True when this user previously unlocked the same email.
              example: false
        meta:
          type: object
          required: [page, page_size, credits_used]
          properties:
            page:
              type: integer
              enum: [0]
            page_size:
              type: integer
              enum: [1]
            credits_used:
              type: integer
              enum: [0, 5]
              description: Five for a new unlock; zero when already unlocked.
            credit_source:
              type: string
              nullable: true
              enum: [monthly, purchased]
              description: Omitted for API-tier keys.
            monthly_credits_remaining:
              type: integer
              nullable: true
              description: Omitted for API-tier keys.
            purchased_credits_remaining:
              type: integer
              nullable: true
              description: Omitted for API-tier keys.

    PersonEmailError:
      type: object
      required: [success, error, statusCode]
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - MISSING_IDENTIFIER
                - INVALID_ID
                - INVALID_IDENTIFIER
                - INSUFFICIENT_CREDITS
                - UPGRADE_REQUIRED
                - PERSON_NOT_FOUND
                - EMAIL_NOT_FOUND
            message:
              type: string
            details:
              type: object
              additionalProperties: true
              properties:
                help:
                  type: string
                current_tier:
                  type: string
                credits_needed:
                  type: integer
                  nullable: true
                monthly_credits_remaining:
                  type: integer
                  nullable: true
                purchased_credits_remaining:
                  type: integer
                  nullable: true
                email_unlock_limit_exceeded:
                  type: boolean
                email_unlock_limit:
                  type: integer
                  nullable: true
                email_unlock_used:
                  type: integer
                  nullable: true
        statusCode:
          type: integer
          enum: [400, 402, 403, 404]

    PersonPhoneResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          required: [person_id, phone, already_unlocked]
          properties:
            person_id:
              type: string
              format: uuid
              example: "550e8400-e29b-41d4-a716-446655440000"
            phone:
              type: string
              example: "+14155550123"
              description: Phone number in E.164 format.
            already_unlocked:
              type: boolean
              description: True when this account previously unlocked the same phone number.
              example: false
        meta:
          type: object
          required: [page, page_size, credits_used]
          properties:
            page:
              type: integer
              enum: [0]
            page_size:
              type: integer
              enum: [1]
            credits_used:
              type: integer
              enum: [0, 5]
              description: Five for a new unlock; zero when already unlocked.
            monthly_credits_remaining:
              type: integer
              nullable: true
              description: Optional; may be omitted.
            purchased_credits_remaining:
              type: integer
              nullable: true
              description: Optional; may be omitted.

    PersonPhoneError:
      type: object
      required: [success, error, statusCode]
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - MISSING_IDENTIFIER
                - INVALID_ID
                - INVALID_IDENTIFIER
                - INSUFFICIENT_CREDITS
                - UPGRADE_REQUIRED
                - PERSON_NOT_FOUND
                - PHONE_NOT_FOUND
                - CONTACT_ENRICHMENT_UNAVAILABLE
                - CREDIT_ACCOUNTING_UNAVAILABLE
                - RATE_LIMIT_EXCEEDED
                - INTERNAL_SERVER_ERROR
            message:
              type: string
            details:
              type: object
              additionalProperties: true
              properties:
                help:
                  type: string
                current_tier:
                  type: string
                credits_needed:
                  type: integer
                  nullable: true
                monthly_credits_remaining:
                  type: integer
                  nullable: true
                purchased_credits_remaining:
                  type: integer
                  nullable: true
        statusCode:
          type: integer
          enum: [400, 401, 402, 403, 404, 429, 500, 503]

    PersonCurrentCompany:
      type: object
      nullable: true
      description: Current employer summary. Null when the person has no current employer.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          nullable: true
        permalink:
          type: string
          nullable: true
          description: Fundable permalink for the company
        domain:
          type: string
          nullable: true

    EmploymentRecord:
      type: object
      properties:
        title:
          type: string
          nullable: true
        company_name:
          type: string
          nullable: true
        company_id:
          type: string
          nullable: true
        company_url:
          type: string
          nullable: true
          format: uri
        location:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        start_date:
          type: string
          format: date
          nullable: true
        end_date:
          type: string
          format: date
          nullable: true
          description: null if current position
        is_current:
          type: boolean

    EducationRecord:
      type: object
      properties:
        school_name:
          type: string
          nullable: true
        school_id:
          type: string
          nullable: true
        school_url:
          type: string
          nullable: true
          format: uri
        degree:
          type: string
          nullable: true
        field_of_study:
          type: string
          nullable: true
        start_date:
          type: string
          nullable: true
        end_date:
          type: string
          nullable: true
        description:
          type: string
          nullable: true

    Firm:
      type: object
      description: Firm (parent investor) the person invests under
      properties:
        id:
          type: string
          nullable: true
          format: uuid
        name:
          type: string
          nullable: true
        permalink:
          type: string
          nullable: true
        domain:
          type: string
          nullable: true
        deal_count:
          type: integer
          description: Number of deals this person has done under this firm
        last_deal_date:
          type: string
          format: date
          nullable: true

    InvestorHighlights:
      type: object
      description: Aggregated investor activity. Counts dedupe by deal across angel + firm contributions.
      properties:
        total_deal_count:
          type: integer
          description: Total tracked deals for this person
          example: 12
        lead_deal_count:
          type: integer
          description: Total deals the person led
        deal_count_last_12_months:
          type: integer
          description: Deals in the last 12 months (any role)
        lead_deal_count_last_12_months:
          type: integer
          description: Lead deals in the last 12 months
        most_recent_deal_date:
          type: string
          format: date
          nullable: true
        top_industries:
          type: array
          description: Top portfolio industries by deal count (max 5)
          items:
            type: object
            properties:
              name:
                type: string
              permalink:
                type: string
              count:
                type: integer
        top_locations:
          type: array
          description: Top portfolio company cities by deal count (max 5)
          items:
            type: object
            properties:
              name:
                type: string
              full_name:
                type: string
              permalink:
                type: string
              type:
                type: string
                example: "CITY"
              count:
                type: integer
        top_round_types:
          type: array
          description: Top financing round types by deal count (max 5)
          items:
            type: object
            properties:
              type:
                type: string
                example: "SERIES_A"
              count:
                type: integer

    PersonDeal:
      type: object
      description: Deal returned by the /person/deals endpoint. Same shape as deals returned by /company/deals.
      properties:
        id:
          type: string
          format: uuid
        round_type:
          type: string
          nullable: true
        extension:
          type: boolean
          nullable: true
        intermediate:
          type: string
          nullable: true
        pre:
          type: boolean
          nullable: true
        date:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        total_round_raised:
          type: number
          nullable: true
        deal_descriptions:
          type: object
          nullable: true
          properties:
            short_description:
              type: string
              nullable: true
            long_description:
              type: string
              nullable: true
        company_id:
          type: string
          format: uuid
        investor_ids:
          type: array
          description: UUIDs of firm investors participating in this deal. Angels are surfaced separately in `angel_investor_ids`.
          items:
            type: string
            format: uuid
        angel_investor_ids:
          type: array
          description: UUIDs of angel investors (people) on this deal. Resolve via `/people/{id}`. Empty array when no angels.
          items:
            type: string
            format: uuid
        financings:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              type:
                type: string
              size_usd:
                type: number
                nullable: true
              size_native:
                type: number
                nullable: true
              currency:
                type: string
                nullable: true
        valuation:
          type: object
          nullable: true
          properties:
            valuation_currency:
              type: string
              nullable: true
            valuation_usd:
              type: number
              nullable: true
            valuation_native:
              type: number
              nullable: true
            type:
              type: string
        articles:
          type: array
          description: Related articles/sources for this deal. Always included; empty when no sources exist.
          items:
            $ref: '#/components/schemas/DealArticle'
      required:
        - id
        - round_type
        - extension
        - intermediate
        - pre
        - date
        - created_at
        - total_round_raised
        - deal_descriptions
        - company_id
        - investor_ids
        - angel_investor_ids
        - financings
        - valuation
        - articles

    DealArticle:
      type: object
      description: An article or source related to a deal
      properties:
        link:
          type: string
          nullable: true
          description: URL of the article
          example: "https://techcrunch.com/2024/01/15/company-raises-series-a"
        date:
          type: string
          nullable: true
          description: Date of the article
          example: "2024-01-15"
        is_primary:
          type: boolean
          description: Whether this is the primary source article for the deal
          example: true
      required:
        - is_primary

    PaginationMeta:
      type: object
      properties:
        total_count:
          type: integer
          description: Total matching results across all pages
          example: 1250
        page:
          type: integer
          example: 0
        page_size:
          type: integer
          example: 25
        credits_used:
          type: integer
          example: 25
        credit_source:
          type: string
          nullable: true
          enum: ['monthly', 'purchased']
        monthly_credits_remaining:
          type: integer
          nullable: true
          example: 990
        purchased_credits_remaining:
          type: integer
          nullable: true
          example: 500

    AuthError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              enum: [AUTH_ERROR, INVALID_API_KEY, INACTIVE_API_KEY]
            message:
              type: string
            details:
              type: object
              properties:
                help:
                  type: string

    ValidationError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: "VALIDATION_ERROR"
            message:
              type: string
            details:
              type: object
              properties:
                errors:
                  type: array
                  items:
                    type: object
                    properties:
                      field:
                        type: string
                      value:
                        type: string
                      message:
                        type: string
                      code:
                        type: string
                      validOptions:
                        type: array
                        items:
                          type: string

    UnknownParamError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: "UNKNOWN_PARAMETER"
            message:
              type: string
            details:
              type: object
              properties:
                unknownParameters:
                  type: array
                  items:
                    type: string
                allowedParameters:
                  type: array
                  items:
                    type: string

    InvalidIdentifierError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: "INVALID_IDENTIFIER"
            message:
              type: string
            details:
              type: object
              properties:
                help:
                  type: string

    NotFoundError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: "PERSON_NOT_FOUND"
            message:
              type: string
              example: "Person not found"
            details:
              type: object
              properties:
                help:
                  type: string

    RateLimitError:
      type: object
      description: Per-minute API rate limit exceeded
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: 'RATE_LIMIT_EXCEEDED'
            message:
              type: string
              example: "Rate limit exceeded. Maximum 200 requests per minute."
            details:
              type: object
              properties:
                limit:
                  type: integer
                  example: 200
                window:
                  type: string
                  example: "60 seconds"
                help:
                  type: string
      required:
        - error

    SemanticUsageLimitError:
      type: object
      description: |
        Monthly semantic-search usage limit exceeded. Only returned when the request included
        `company.search_query` or `investor.deals.search_query`.
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: 'USAGE_LIMIT_EXCEEDED'
            message:
              type: string
              example: "Search limit exceeded"
            details:
              type: object
              properties:
                failed_block:
                  type: string
                  enum: ['company', 'investor.deals']
                  description: Which semantic call hit the limit. Optional — only present on the unified people endpoint, which can issue two semantic calls per request.
                currentUsage:
                  type: integer
                  description: Current monthly semantic-search count
                  example: 1000
                limit:
                  type: integer
                  description: Monthly semantic-search ceiling for this API key tier
                  example: 1000
                help:
                  type: string
                  example: "You have reached your search usage limit. Upgrade your plan or wait for the next cycle."
      required:
        - error

    SemanticTimeoutError:
      type: object
      description: |
        Semantic search timed out. Only returned when the request included
        `company.search_query` or `investor.deals.search_query`.
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: 'TIMEOUT'
            message:
              type: string
              example: "The semantic search operation timed out. Please try a simpler query."
            details:
              type: object
              properties:
                failed_block:
                  type: string
                  enum: ['company', 'investor.deals']
                  description: Which semantic call timed out
                help:
                  type: string
                  example: "Try a simpler query or fewer filters"
      required:
        - error

    InsufficientCreditsError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: 'INSUFFICIENT_CREDITS'
            message:
              type: string
            details:
              type: object
              properties:
                credits_needed:
                  type: integer
                  nullable: true
                monthly_credits_remaining:
                  type: integer
                purchased_credits_remaining:
                  type: integer
                help:
                  type: string
      required:
        - error

    ServerError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: "INTERNAL_SERVER_ERROR"
            message:
              type: string

tags:
  - name: people
    description: People search and detail operations
