openapi: 3.1.0
info:
  title: 501see API
  description: |
    REST API for IRS 990 nonprofit filing data. Provides access to
    organizations, filings, officers, grants, full-text search, and
    authenticated user features (saved searches, recent searches, onboarding).

    **Authentication**: All endpoints work without authentication. Officer
    compensation is the only field stripped for unauthenticated and free-plan
    callers; grant amounts, financials, and portfolio aggregates are visible
    to everyone.

    Authenticate with `Authorization: Bearer YOUR_API_KEY` using an API key
    (`sk_live_...`) generated from your account settings page.

    **Account endpoints** (`/api/v1/me`, `/api/v1/account`, `/api/v1/onboarding`)
    are session-authenticated (Clerk JWT) and not included in this spec.
    They support org context linking (`organization_ein`) and account settings.

    **Field selection** (filing endpoints only):
    - `?preset=summary` (default) — ~30 high-population fields
    - `?preset=financials` — summary + ~60 Part VIII/IX/X detail fields
    - `?preset=all` — every registered field
    - `?fields=total_revenue,total_expenses` — cherry-pick specific fields (overrides preset)

    **Sorting**: `?sort=field` (ascending) or `?sort=-field` (descending).

    **Pagination**: offset-based with `?offset=0&limit=50`. Max limit is 500.

    **Plan-aware field gating**: Grant amounts, multi-year financials, and
    funder portfolio aggregates are visible to all plans. Officer compensation
    is the one dollar field that is stripped for unauthenticated and free-plan
    users (named comp matrix gated to paid plans). The `plan` field in the
    response envelope indicates the caller's plan level (`anonymous`, `free`,
    or a paid plan name).

    **MCP server**: An MCP-over-SSE endpoint is available at `/mcp` for
    AI agent integrations (Claude Code, Claude Desktop, Cursor, etc.).
    Authenticate the same way: `Authorization: Bearer YOUR_API_KEY`.
  version: 0.3.0
  license:
    name: MIT

servers:
- url: https://api.501see.app
  description: Production

security:
  - {}
  - bearerAuth: []

paths:
  /healthz:
    get:
      operationId: healthCheck
      summary: Health check
      tags: [System]
      responses:
        "200":
          description: Server is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /api/v1/orgs:
    get:
      operationId: listOrganizations
      summary: List organizations
      description: |
        List and filter nonprofit organizations. Joins to the latest filing for
        financial sort and filter fields. Name search uses trigram similarity.
      tags: [Organizations]
      parameters:
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: sort
          in: query
          description: Sort field. Prefix with `-` for descending.
          schema:
            type: string
            default: name
            enum: [name, -name, state, -state, total_revenue, -total_revenue, total_expenses, -total_expenses, net_assets_eoy, -net_assets_eoy]
        - name: state
          in: query
          description: Filter by two-letter state code. Repeat to match multiple states.
          schema:
            type: array
            items:
              type: string
        - name: city
          in: query
          description: Filter by city using a case-insensitive substring match.
          schema:
            type: string
            example: Atlanta
        - name: ntee
          in: query
          description: Filter by NTEE code prefix.
          schema:
            type: string
            example: K
        - name: subsection
          in: query
          description: Filter by 501(c) subsection numeric suffix, such as `03` for 501(c)(3).
          schema:
            type: string
            example: "03"
        - name: name
          in: query
          description: Search by organization name.
          schema:
            type: string
            example: food bank
        - name: min_revenue
          in: query
          description: Minimum total revenue from the latest filing.
          schema:
            type: number
            example: 1000000
        - name: max_revenue
          in: query
          description: Maximum total revenue from the latest filing.
          schema:
            type: number
            example: 50000000
        - name: min_expenses
          in: query
          description: Minimum total expenses from the latest filing.
          schema:
            type: number
        - name: max_expenses
          in: query
          description: Maximum total expenses from the latest filing.
          schema:
            type: number
        - name: min_net_assets
          in: query
          description: Minimum end-of-year net assets from the latest filing.
          schema:
            type: number
        - name: max_net_assets
          in: query
          description: Maximum end-of-year net assets from the latest filing.
          schema:
            type: number
        - name: form_type
          in: query
          description: Filter by filing form type.
          schema:
            type: string
            enum: ["990", "990-EZ", "990-PF"]
        - name: min_program_expense_pct
          in: query
          description: Minimum program expense ratio from 0 to 100.
          schema:
            type: number
        - name: max_program_expense_pct
          in: query
          description: Maximum program expense ratio from 0 to 100.
          schema:
            type: number
        - name: min_contributions_pct
          in: query
          description: Minimum contributions as a percent of revenue from 0 to 100.
          schema:
            type: number
        - name: max_contributions_pct
          in: query
          description: Maximum contributions as a percent of revenue from 0 to 100.
          schema:
            type: number
        - name: min_formation_year
          in: query
          description: Minimum founding year.
          schema:
            type: integer
        - name: max_formation_year
          in: query
          description: Maximum founding year.
          schema:
            type: integer
        - name: min_employees
          in: query
          description: Minimum total employees. Self-reported and often sparse.
          schema:
            type: integer
        - name: max_employees
          in: query
          description: Maximum total employees.
          schema:
            type: integer
      responses:
        "200":
          description: Paginated list of organizations.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/OrganizationListItem"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/orgs/{ein}:
    get:
      operationId: getOrganization
      summary: Get organization by EIN
      tags: [Organizations]
      parameters:
        - name: ein
          in: path
          required: true
          description: Employer Identification Number formatted as `XX-XXXXXXX`.
          schema:
            type: string
            pattern: '^\d{2}-\d{7}$'
            example: "12-3456789"
      responses:
        "200":
          description: Organization details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/orgs/{ein}/recommended-funders:
    get:
      operationId: listRecommendedFunders
      summary: List recommended funders for an organization
      description: |
        Returns funders ranked by how many similar organizations they fund,
        weighted by similarity score. Uses precomputed shared_funders data
        from the similarity engine. Rebuilt monthly.
      tags: [Organizations]
      parameters:
        - name: ein
          in: path
          required: true
          schema:
            type: string
          example: "62-1358654"
        - name: limit
          in: query
          description: Max funders to return (1-100, default 20).
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: Ranked list of recommended funders.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        ein:
                          type: string
                        name:
                          type: string
                        city:
                          type: string
                        state:
                          type: string
                        ntee_code:
                          type: string
                        orgs_funded:
                          type: integer
                          description: Number of similar orgs this funder funds.
                        weighted_score:
                          type: number
                          description: Sum of similarity scores of funded similar orgs.
                        total_grants:
                          type: integer
                          description: Total grants from funder portfolio. Null if no portfolio data.
                          nullable: true
                        avg_grant:
                          type: number
                          description: Average grant amount. Null if no portfolio data.
                          nullable: true
                        median_grant:
                          type: number
                          description: Median grant amount. Null if no portfolio data.
                          nullable: true
                        years_active:
                          type: integer
                          description: Number of years with grant activity. Null if no portfolio data.
                          nullable: true
                        top_sectors:
                          type: array
                          items:
                            type: string
                          description: Top 3 NTEE major group codes by grant count.
                  total:
                    type: integer
        "401":
          description: Authentication required. Anonymous requests are not permitted.

  /api/v1/orgs/{ein}/portfolio:
    get:
      operationId: getFunderPortfolio
      summary: Get funder giving portfolio
      description: Precomputed foundation portfolio with yearly trends, sector mix, geography, revenue bands, and top grantees. Rebuilt monthly. Open to anonymous and paid callers.
      tags: [Organizations]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: ein
          in: path
          required: true
          schema:
            type: string
          example: "38-1359217"
      responses:
        "200":
          description: Funder portfolio data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  funder_ein:
                    type: string
                  summary:
                    type: object
                    properties:
                      total_grants:
                        type: integer
                      total_giving:
                        type: number
                        nullable: true
                        description: Null if no portfolio data.
                      unique_recipients:
                        type: integer
                      years_active:
                        type: integer
                      first_year:
                        type: integer
                      last_year:
                        type: integer
                      avg_grant:
                        type: number
                        nullable: true
                      median_grant:
                        type: number
                        nullable: true
                  yearly_stats:
                    type: array
                    items:
                      type: object
                      properties:
                        year:
                          type: integer
                        grant_count:
                          type: integer
                        total_giving:
                          type: number
                          nullable: true
                        avg_grant:
                          type: number
                          nullable: true
                        median_grant:
                          type: number
                          nullable: true
                        min_grant:
                          type: number
                          nullable: true
                        max_grant:
                          type: number
                          nullable: true
                        unique_recipients:
                          type: integer
                        new_grantees:
                          type: integer
                        repeat_grantees:
                          type: integer
                  sectors:
                    type: array
                    items:
                      type: object
                      properties:
                        ntee_major:
                          type: string
                        label:
                          type: string
                        count:
                          type: integer
                        total:
                          type: number
                          nullable: true
                  geography:
                    type: array
                    items:
                      type: object
                      properties:
                        state:
                          type: string
                        count:
                          type: integer
                        total:
                          type: number
                          nullable: true
                  revenue_bands:
                    type: array
                    items:
                      type: object
                      properties:
                        band:
                          type: string
                        label:
                          type: string
                        count:
                          type: integer
                  top_grantees:
                    type: array
                    items:
                      type: object
                      properties:
                        recipient_ein:
                          type: string
                          nullable: true
                        name:
                          type: string
                        state:
                          type: string
                        total_received:
                          type: number
                          nullable: true
                        grant_count:
                          type: integer
        "404":
          description: No portfolio data for this organization.

  /api/v1/orgs/{ein}/matching-grants:
    get:
      operationId: getMatchingGrants
      summary: Get grants from a foundation that align with a Funder Project brief
      description: |
        Returns up to 5 grants paid by the specified foundation whose purpose
        text matches the Funder Project brief, ranked by full-text relevance. Only
        available for paid-plan users. Requires a funder_project_id whose brief is
        non-empty.
      tags: [Organizations]
      security:
        - bearerAuth: []
      parameters:
        - name: ein
          in: path
          required: true
          schema:
            type: string
          example: "38-1359217"
        - name: funder_project_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Matching grants.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        recipient_name:
                          type: string
                        amount:
                          type: number
                          nullable: true
                        purpose:
                          type: string
                        year:
                          type: integer
                          nullable: true
        "400":
          description: funder_project_id missing or invalid.
        "401":
          description: Authentication required.
        "403":
          description: Paid plan required.
        "404":
          description: Funder Project not found.

  /api/v1/orgs/{ein}/similar:
    get:
      operationId: listSimilarOrgs
      summary: List organizations similar to a given org
      description: |
        Returns precomputed similar organizations based on grant co-occurrence,
        NTEE code, geography, and revenue band.
      tags: [Organizations]
      parameters:
        - name: ein
          in: path
          required: true
          description: Employer Identification Number formatted as `XX-XXXXXXX`.
          schema:
            type: string
            pattern: '^\d{2}-\d{7}$'
            example: "12-3456789"
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: min_score
          in: query
          description: Minimum composite similarity score from 0.0 to 1.0.
          schema:
            type: number
            format: float
        - name: signal
          in: query
          description: Limit matches to a specific similarity signal.
          schema:
            type: string
            enum: [grant_cooccurrence, ntee, geography, revenue_band]
      responses:
        "200":
          description: List of similar organizations with scores and signal breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SimilarOrg"
                  total:
                    type: integer
                  offset:
                    type: integer
                  limit:
                    type: integer
        "401":
          description: Authentication required. Anonymous requests are not permitted.
      security:
        - bearerAuth: []

  /api/v1/orgs/{ein}/filings:
    get:
      operationId: listFilings
      summary: List filings for an organization
      description: |
        Returns filings for the given EIN. Response fields are dynamic and
        driven by the `preset` or `fields` parameter.
      tags: [Filings]
      parameters:
        - name: ein
          in: path
          required: true
          description: Employer Identification Number formatted as `XX-XXXXXXX`.
          schema:
            type: string
            pattern: '^\d{2}-\d{7}$'
            example: "12-3456789"
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: preset
          in: query
          description: Field preset for filing responses. Ignored if `fields` is provided.
          schema:
            type: string
            default: summary
            enum: [summary, financials, all]
        - name: fields
          in: query
          description: Comma-separated list of fields to return. Overrides `preset`.
          schema:
            type: string
            example: total_revenue,total_expenses,mission
        - name: sort
          in: query
          description: Sort field. Prefix with `-` for descending.
          schema:
            type: string
            default: "-tax_year"
            enum: [tax_year, -tax_year, total_revenue, -total_revenue, total_expenses, -total_expenses]
        - name: tax_year
          in: query
          description: Filter by tax year.
          schema:
            type: integer
            example: 2023
        - name: form_type
          in: query
          description: Filter by form type.
          schema:
            type: string
            enum: ["990", "990EZ", "990PF"]
      responses:
        "200":
          description: Paginated list of filings with dynamic field set.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Filing"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/orgs/{ein}/filings/latest:
    get:
      operationId: getLatestFiling
      summary: Get latest filing for an organization
      description: Returns the most recent filing by tax year for the given EIN.
      tags: [Filings]
      parameters:
        - name: ein
          in: path
          required: true
          description: Employer Identification Number formatted as `XX-XXXXXXX`.
          schema:
            type: string
            pattern: '^\d{2}-\d{7}$'
            example: "12-3456789"
        - name: preset
          in: query
          description: Field preset for filing responses. Ignored if `fields` is provided.
          schema:
            type: string
            default: summary
            enum: [summary, financials, all]
        - name: fields
          in: query
          description: Comma-separated list of fields to return. Overrides `preset`.
          schema:
            type: string
            example: total_revenue,total_expenses,mission
      responses:
        "200":
          description: Latest filing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Filing"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/filings/{object_id}:
    get:
      operationId: getFiling
      summary: Get filing by object ID
      tags: [Filings]
      parameters:
        - name: object_id
          in: path
          required: true
          description: IRS e-file object ID.
          schema:
            type: string
            example: "202303559349300000"
        - name: preset
          in: query
          description: Field preset for filing responses. Ignored if `fields` is provided.
          schema:
            type: string
            default: summary
            enum: [summary, financials, all]
        - name: fields
          in: query
          description: Comma-separated list of fields to return. Overrides `preset`.
          schema:
            type: string
            example: total_revenue,total_expenses,mission
      responses:
        "200":
          description: Filing details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Filing"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/filings/{object_id}/officers:
    get:
      operationId: listOfficers
      summary: List officers for a filing
      description: Returns Part VII officers, directors, and trustees for one filing.
      tags: [Officers]
      parameters:
        - name: object_id
          in: path
          required: true
          description: IRS e-file object ID.
          schema:
            type: string
            example: "202303559349300000"
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
      responses:
        "200":
          description: Paginated list of officers.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Officer"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/filings/{object_id}/grants:
    get:
      operationId: listFilingGrants
      summary: List Schedule I grants for a filing
      description: Returns Schedule I grants paid from a 990 filing.
      tags: [Grants]
      parameters:
        - name: object_id
          in: path
          required: true
          description: IRS e-file object ID.
          schema:
            type: string
            example: "202303559349300000"
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
      responses:
        "200":
          description: Paginated list of Schedule I grants.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Grant"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/filings/{object_id}/pf:
    get:
      operationId: getPFFiling
      summary: Get 990-PF financials for a filing
      description: Returns 990-PF-specific financial data for one filing.
      tags: [Private Foundations]
      parameters:
        - name: object_id
          in: path
          required: true
          description: IRS e-file object ID.
          schema:
            type: string
            example: "202303559349300000"
        - name: preset
          in: query
          description: Field preset for filing responses. Ignored if `fields` is provided.
          schema:
            type: string
            default: summary
            enum: [summary, financials, all]
        - name: fields
          in: query
          description: Comma-separated list of fields to return. Overrides `preset`.
          schema:
            type: string
            example: total_revenue,total_expenses,mission
      responses:
        "200":
          description: PF filing data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object_id:
                    type: string
                  ein:
                    type: string
                  form_type:
                    type: string
                    enum: ["990-PF"]
                  tax_year:
                    type: integer
                  org_name:
                    type: string
                  total_assets_eoy:
                    type: number
                    nullable: true
                  total_revenue:
                    type: number
                    nullable: true
                  total_expenses:
                    type: number
                    nullable: true
                  grants_paid:
                    type: number
                    nullable: true
                  contributions:
                    type: number
                    nullable: true
                  net_assets_eoy:
                    type: number
                    nullable: true
                additionalProperties: true
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/filings/{object_id}/pf/grants:
    get:
      operationId: listPFGrants
      summary: List Part XV foundation grants paid
      description: Returns grants paid from 990-PF Part XV.
      tags: [Private Foundations]
      parameters:
        - name: object_id
          in: path
          required: true
          description: IRS e-file object ID.
          schema:
            type: string
            example: "202303559349300000"
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
      responses:
        "200":
          description: Paginated list of PF grants paid.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/PFGrant"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/grants:
    get:
      operationId: searchGrants
      summary: Search grants across all filings
      description: |
        Cross-filing grant search across Schedule I, 990-PF Part XV, and
        related funder metadata.
      tags: [Grants]
      parameters:
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: sort
          in: query
          description: Sort field. Prefix with `-` for descending.
          schema:
            type: string
            default: "-amount"
            enum: [amount, -amount, tax_year, -tax_year, funder_name, -funder_name, recipient_name, -recipient_name]
        - name: funder_ein
          in: query
          description: Filter by funder EIN.
          schema:
            type: string
            example: "13-1837418"
        - name: funder_name
          in: query
          description: Search by funder organization name.
          schema:
            type: string
        - name: funder_ntee
          in: query
          description: Filter by funder NTEE major category.
          schema:
            type: string
            example: "B"
        - name: funder_state
          in: query
          description: Filter by funder home state. Repeat for multiple states.
          schema:
            type: array
            items:
              type: string
        - name: recipient_ntee
          in: query
          description: Filter by recipient NTEE major category or full code.
          schema:
            type: string
            example: "D"
        - name: recipient_ein
          in: query
          description: Filter by recipient EIN.
          schema:
            type: string
        - name: similar_to
          in: query
          description: Filter to funders that also fund orgs similar to this EIN.
          schema:
            type: string
            example: "62-1358654"
        - name: recipient_name
          in: query
          description: Search by recipient name.
          schema:
            type: string
            example: habitat
        - name: min_amount
          in: query
          description: Minimum grant amount.
          schema:
            type: number
            example: 10000
        - name: max_amount
          in: query
          description: Maximum grant amount.
          schema:
            type: number
            example: 1000000
        - name: state
          in: query
          description: Filter by recipient state. Repeat for multiple states.
          schema:
            type: array
            items:
              type: string
        - name: tax_year
          in: query
          description: Filter by exact tax year.
          schema:
            type: integer
            example: 2023
        - name: min_year
          in: query
          description: Minimum tax year, inclusive.
          schema:
            type: integer
            example: 2020
        - name: max_year
          in: query
          description: Maximum tax year, inclusive.
          schema:
            type: integer
            example: 2023
        - name: foundation_type
          in: query
          description: Filter by foundation type. Use `990-PF` for private foundations or `990` for charitable foundations.
          schema:
            type: string
            enum: ["990", "990-PF"]
        - name: hide_preselected
          in: query
          description: When true, excludes grants from funders whose latest portfolio data explicitly marks them as closed to unsolicited applications. Funders with unknown application status are kept.
          schema:
            type: boolean
        - name: purpose
          in: query
          description: Full-text search against grant purpose.
          schema:
            type: string
            example: "affordable housing"
        - name: query
          in: query
          description: Keyword search across recipient name and grant purpose.
          schema:
            type: string
            example: "india"
      responses:
        "200":
          description: Paginated grant search results.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/GrantSearchResult"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/officers:
    get:
      operationId: searchOfficers
      summary: Search officers across all filings
      description: Search officers by role, title, organization, year, revenue, and compensation. Defaults to paid staff with non-zero pay. Compensation fields are hidden for anonymous and free-plan callers.
      tags: [Officers]
      parameters:
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: sort
          in: query
          description: Sort field. Prefix with `-` for descending.
          schema:
            type: string
            default: "-compensation"
            enum: [compensation, -compensation, tax_year, -tax_year, name, -name]
        - name: name
          in: query
          description: Search by officer name.
          schema:
            type: string
        - name: title
          in: query
          description: Free-text title keyword.
          schema:
            type: string
            example: director
        - name: role
          in: query
          description: Role category mapped to curated title keywords.
          schema:
            type: string
            enum: [development, finance, executive, programs, operations, marketing, hr, legal]
        - name: role_type
          in: query
          description: Position type filter.
          schema:
            type: string
            default: staff
            enum: [staff, board, former, all]
        - name: ein
          in: query
          description: Filter by organization EIN.
          schema:
            type: string
        - name: state
          in: query
          description: Filter by two-letter organization state code.
          schema:
            type: string
            example: TN
        - name: ntee
          in: query
          description: Filter by organization NTEE code or major-group prefix.
          schema:
            type: string
            example: P
        - name: min_revenue
          in: query
          description: Minimum organization revenue.
          schema:
            type: number
            example: 500000
        - name: max_revenue
          in: query
          description: Maximum organization revenue.
          schema:
            type: number
            example: 5000000
        - name: min_year
          in: query
          description: Minimum tax year.
          schema:
            type: integer
        - name: max_year
          in: query
          description: Maximum tax year.
          schema:
            type: integer
        - name: min_compensation
          in: query
          description: Minimum total compensation.
          schema:
            type: number
            example: 100000
        - name: max_compensation
          in: query
          description: Maximum total compensation.
          schema:
            type: number
            example: 500000
      responses:
        "200":
          description: Paginated officer search results.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/SearchResult"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/compensation-benchmark:
    get:
      operationId: getCompensationBenchmark
      summary: Compensation percentile benchmarks by role
      description: |
        Returns P25, median, P75, and mean total compensation for a given role
        filtered by sector, state, and/or org revenue range. Data is pulled
        from the officer_search materialized view (paid staff on latest filings).
        No authentication required.
      tags: [Officers]
      parameters:
        - name: role
          in: query
          required: true
          description: Role group key (e.g. executive, development, finance, programs).
          schema:
            type: string
        - name: ntee
          in: query
          description: NTEE major category letter (e.g. B for Education, E for Health).
          schema:
            type: string
        - name: state
          in: query
          description: Two-letter US state code.
          schema:
            type: string
        - name: min_revenue
          in: query
          description: Minimum org revenue filter.
          schema:
            type: number
        - name: max_revenue
          in: query
          description: Maximum org revenue filter.
          schema:
            type: number
      responses:
        "200":
          description: Compensation benchmark statistics.
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                    description: Number of officer records matched.
                  p25:
                    type: number
                    nullable: true
                    description: 25th percentile total compensation.
                  median:
                    type: number
                    nullable: true
                    description: 50th percentile total compensation.
                  p75:
                    type: number
                    nullable: true
                    description: 75th percentile total compensation.
                  mean:
                    type: number
                    nullable: true
                    description: Mean total compensation.
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/search:
    get:
      operationId: search
      summary: Full-text search
      description: Search across organizations, officers, and grants.
      tags: [Search]
      parameters:
        - name: offset
          in: query
          description: Number of items to skip (for pagination).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          description: Maximum number of items to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: query
          in: query
          required: true
          description: Search query string.
          schema:
            type: string
            example: Atlanta Food Bank
        - name: type
          in: query
          description: Filter results by entity type.
          schema:
            type: string
            enum: [org, officer, grant]
      responses:
        "200":
          description: Paginated search results sorted by relevance.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/SearchResult"
        "400":
          $ref: "#/components/responses/BadRequest"

  # Internal endpoints (prospects, saved-searches, recent-searches, onboarding, me, api-keys, exports,
  # prior-funders, grants-received) are consumed by the SvelteKit frontend via Clerk JWT and are
  # intentionally excluded from the public OpenAPI spec. They are not part of the external API.

  /api/v1/foundations:
    get:
      operationId: searchFoundations
      tags: [Private Foundations]
      summary: Search foundations by giving behavior
      description: |
        Returns foundations from the funder_portfolio table, filtered by
        filer type, sector funded, geographic giving, grant size, years
        active, and grant purpose text. Backed by pre-computed monthly
        aggregations. Purpose search falls back to raw grant purpose and
        grantee-name tables when the denormalized portfolio profile text has
        not yet been backfilled.
      parameters:
        - name: name
          in: query
          schema: { type: string }
          description: Foundation name keyword (fuzzy).
        - name: state
          in: query
          schema: { type: string }
          description: Foundation's home state (2-letter code). Repeatable.
        - name: foundation_type
          in: query
          schema:
            type: string
            enum: ["990", "990-PF"]
          description: Filter by filer type. Use `990-PF` for private foundations or `990` for charitable foundations.
        - name: city
          in: query
          schema: { type: string }
          description: Foundation's home city (case-insensitive substring match).
        - name: ntee
          in: query
          schema: { type: string }
          description: Foundation's own NTEE code prefix. Repeatable.
        - name: funder_sector
          in: query
          schema: { type: string }
          description: NTEE major letter of sectors they fund (A-Z). Repeatable.
        - name: funder_geo_state
          in: query
          schema: { type: string }
          description: State(s) they give grants in (2-letter code). Repeatable.
        - name: min_avg_grant
          in: query
          schema: { type: number }
        - name: max_avg_grant
          in: query
          schema: { type: number }
        - name: min_median_grant
          in: query
          schema: { type: number }
        - name: max_median_grant
          in: query
          schema: { type: number }
        - name: min_total_grants
          in: query
          schema: { type: integer }
        - name: min_years_active
          in: query
          schema: { type: integer }
        - name: min_formation_year
          in: query
          schema: { type: integer }
        - name: max_formation_year
          in: query
          schema: { type: integer }
        - name: min_revenue
          in: query
          schema: { type: number }
          description: Minimum latest-year total revenue, with 990/990-PF/BMF fallback.
        - name: max_revenue
          in: query
          schema: { type: number }
          description: Maximum latest-year total revenue, with 990/990-PF/BMF fallback.
        - name: min_expenses
          in: query
          schema: { type: number }
          description: Minimum latest-year total expenses, with 990/990-PF fallback.
        - name: max_expenses
          in: query
          schema: { type: number }
          description: Maximum latest-year total expenses, with 990/990-PF fallback.
        - name: min_assets
          in: query
          schema: { type: number }
          description: Minimum latest-year total assets, with 990/990-PF/BMF fallback.
        - name: max_assets
          in: query
          schema: { type: number }
          description: Maximum latest-year total assets, with 990/990-PF/BMF fallback.
        - name: min_net_assets
          in: query
          schema: { type: number }
          description: Minimum latest-year net assets, with 990/990-PF/BMF fallback.
        - name: max_net_assets
          in: query
          schema: { type: number }
          description: Maximum latest-year net assets, with 990/990-PF/BMF fallback.
        - name: purpose
          in: query
          schema: { type: string }
          description: Full-text keyword search against portfolio profile text with fallback to raw grant purpose and grantee-name text.
        - name: debug_scores
          in: query
          schema: { type: boolean }
          description: When true, includes per-result lexical, semantic, and blended ranking scores in a `debug_scores` object for evaluation and tuning.
        - name: hide_preselected
          in: query
          schema: { type: boolean }
          description: When true, excludes funders whose latest 990-PF Part XV flagged "contributors are preselected" (i.e., funders that don't accept unsolicited applications). Funders with unknown application status are kept.
        - name: sort
          in: query
          schema: { type: string }
          description: "Sort field with optional direction prefix: name, total_grants, avg_grant, years_active, total_giving."
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 25 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: Paginated foundation results.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { type: object, additionalProperties: true }
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/me/notification-prefs:
    get:
      operationId: getNotificationPrefs
      tags: [Account]
      summary: Get notification preferences for the authenticated user
      description: |
        Returns the current email notification preferences for the authenticated user
        on their current account. Uses an opt-out model: email types not present in
        the response default to enabled (true).
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Notification preferences
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    additionalProperties:
                      type: boolean
                    example:
                      deadline_reminder: true
                      report_reminder: false
                      new_match: true
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      operationId: updateNotificationPref
      tags: [Account]
      summary: Update a notification preference
      description: |
        Enables or disables a specific email notification type for the authenticated
        user on their current account. Valid email_type values: `deadline_reminder`,
        `report_reminder`, `new_match`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email_type, enabled]
              properties:
                email_type:
                  type: string
                  enum: [deadline_reminder, report_reminder, new_match]
                enabled:
                  type: boolean
      responses:
        "200":
          description: Preference updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |
        API key (format: sk_live_...). Generate one from your account settings
        from your 501see account settings. Optional for read endpoints - they work without
        authentication but return limited data (dollar amounts hidden).
        Authenticated requests use account-based rate limiting instead of
        IP-based.

  parameters:
    ein:
      name: ein
      in: path
      required: true
      description: Employer Identification Number (formatted as `XX-XXXXXXX`).
      schema:
        type: string
        pattern: '^\d{2}-\d{7}$'
        example: "12-3456789"

    objectId:
      name: object_id
      in: path
      required: true
      description: IRS e-file object ID (unique identifier for a filing).
      schema:
        type: string
        example: "202303559349300000"

    offset:
      name: offset
      in: query
      description: Number of items to skip (for pagination).
      schema:
        type: integer
        minimum: 0
        default: 0

    limit:
      name: limit
      in: query
      description: Maximum number of items to return.
      schema:
        type: integer
        minimum: 1
        maximum: 500
        default: 50

    preset:
      name: preset
      in: query
      description: |
        Field preset for filing responses. Controls which fields are returned.
        Ignored if `fields` is provided.
      schema:
        type: string
        default: summary
        enum: [summary, financials, all]

    fields:
      name: fields
      in: query
      description: |
        Comma-separated list of specific fields to return. Overrides `preset`.
        See the field registry for available names.
      schema:
        type: string
        example: total_revenue,total_expenses,mission

  schemas:
    SimilarOrg:
      type: object
      properties:
        ein:
          type: string
          example: "13-1837418"
        name:
          type: string
          example: "Robin Hood Foundation"
        city:
          type: string
          example: "New York"
        state:
          type: string
          example: "NY"
        ntee_code:
          type: string
          example: "T70"
        score:
          type: number
          format: float
          description: Composite similarity score (0.0-1.0).
          example: 0.72
        signals:
          type: object
          description: Per-signal score breakdown for explainability.
          properties:
            grant_cooccurrence:
              type: number
            ntee:
              type: number
            geography:
              type: number
            revenue_band:
              type: number
          example:
            grant_cooccurrence: 0.47
            ntee: 0.20
            geography: 0.05
            revenue_band: 0.00
        rank:
          type: integer
          description: Precomputed rank (1 = most similar).
          example: 1

    PaginatedResponse:
      type: object
      required: [data, total, offset, limit]
      properties:
        data:
          type: array
          items: {}
        total:
          type: integer
          description: Total number of matching records.
          example: 4231
        offset:
          type: integer
          description: Current offset.
          example: 0
        limit:
          type: integer
          description: Current limit.
          example: 50
        plan:
          type: string
          description: |
            Caller's plan level. Present on endpoints with plan-aware field
            gating (officers, grants). Values: `anonymous` (no auth),
            `free`, or a paid plan name.

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [bad_request, not_found, internal_error, unauthorized, unavailable]
            message:
              type: string
              example: "No organization found with EIN 99-9999999"

    OrganizationListItem:
      type: object
      required: [ein, name]
      properties:
        ein:
          type: string
          example: "12-3456789"
        name:
          type: string
          example: Atlanta Food Bank
        city:
          type: string
          example: Atlanta
        state:
          type: string
          example: GA
        zip:
          type: string
          example: "30301"
        ntee_code:
          type: string
          example: K30
        website:
          type: string
        phone:
          type: string
          example: "4045550100"
        formation_year:
          type: integer
          example: 1990
        latest_tax_period_end:
          type: string
          format: date
          example: "2023-12-31"
        total_revenue:
          type: number
          description: Total revenue from the latest filing.
          example: 5000000
        total_expenses:
          type: number
          description: Total expenses from the latest filing.
          example: 4800000
        net_assets_eoy:
          type: number
          description: Net assets end of year from the latest filing.
          example: 6000000
        form_type:
          type: string
          description: IRS form type of the latest filing (990, 990EZ, or 990PF).
          example: "990"

    Organization:
      type: object
      required: [ein, name]
      properties:
        ein:
          type: string
          example: "12-3456789"
        name:
          type: string
          example: Atlanta Food Bank
        city:
          type: string
        state:
          type: string
        zip:
          type: string
        country:
          type: string
        phone:
          type: string
        website:
          type: string
        formation_year:
          type: integer
        domicile_state:
          type: string
        ntee_code:
          type: string
        subsection:
          type: string
          description: "501(c) subsection code (e.g. '03' for 501(c)(3))."
        latest_tax_period_end:
          type: string
          format: date
        latest_object_id:
          type: string
        operating_reserve_months:
          type: number
          nullable: true
          description: Net assets divided by monthly expenses. Null if expenses are zero or no filing.
        revenue_growth_pct:
          type: number
          nullable: true
          description: Year-over-year revenue change as a percentage.
        expense_growth_pct:
          type: number
          nullable: true
          description: Year-over-year expense change as a percentage.
        program_expense_pct:
          type: number
          nullable: true
          description: Program expenses as percentage of total expenses (Part IX). Null for 990-EZ filers.
        admin_expense_pct:
          type: number
          nullable: true
          description: Management/general expenses as percentage of total.
        fundraising_expense_pct:
          type: number
          nullable: true
          description: Fundraising expenses as percentage of total.
        contributions_pct:
          type: number
          nullable: true
          description: Contributions as percentage of total revenue.
        program_revenue_pct:
          type: number
          nullable: true
          description: Program revenue as percentage of total revenue.
        investment_income_pct:
          type: number
          nullable: true
          description: Investment income as percentage of total revenue.

    Filing:
      type: object
      description: |
        Filing response with dynamic fields driven by the `preset` or `fields`
        parameter. The summary preset includes the fields listed here; other
        presets add more. All values may be null if the filing doesn't report
        that field (especially for 990-EZ filers).
      properties:
        object_id:
          type: string
        ein:
          type: string
        form_type:
          type: string
          enum: ["990", "990EZ", "990PF"]
        tax_year:
          type: integer
        tax_period_begin:
          type: string
          format: date
        tax_period_end:
          type: string
          format: date
        org_name:
          type: string
        org_city:
          type: string
        org_state:
          type: string
        org_zip:
          type: string
        org_phone:
          type: number
        org_website:
          type: string
        mission:
          type: string
        total_revenue:
          type: number
        total_revenue_py:
          type: number
          description: Prior year total revenue.
        total_expenses:
          type: number
        total_expenses_py:
          type: number
          description: Prior year total expenses.
        revenue_less_expenses:
          type: number
        contributions:
          type: number
          description: "Contributions (COALESCE of 990 and 990-EZ variants)."
        program_revenue:
          type: number
        investment_income:
          type: number
        grants_paid:
          type: number
        salaries_and_wages:
          type: number
        other_expenses:
          type: number
          description: "Other expenses (COALESCE of 990 and 990-EZ variants)."
        total_assets_eoy:
          type: number
        total_liabilities_eoy:
          type: number
        net_assets_eoy:
          type: number
        net_assets_boy:
          type: number
        total_employees:
          type: integer
        total_volunteers:
          type: integer
        voting_members:
          type: integer
        signing_officer_name:
          type: string
        signing_officer_title:
          type: string
      additionalProperties: true

    Officer:
      type: object
      description: |
        Officer/director/trustee from Part VII. Compensation fields
        (compensation, other_compensation, related_compensation, benefits,
        total_compensation) are null for unauthenticated and free-plan users.
      required: [name]
      properties:
        name:
          type: string
          example: Jane Smith
        title:
          type: string
          example: Executive Director
        hours_per_week:
          type: number
          example: 40
        compensation:
          type: number
          description: Compensation from the organization (paid plans only).
          example: 120000
        other_compensation:
          type: number
          description: Compensation from other sources (paid plans only).
        related_compensation:
          type: number
          description: Compensation from related organizations (paid plans only).
        benefits:
          type: number
          description: Employee benefits and deferred compensation (paid plans only).
        is_officer:
          type: boolean
        is_former:
          type: boolean
        is_key_employee:
          type: boolean
        is_highest_compensated:
          type: boolean
        total_compensation:
          type: number
          description: Sum of compensation, related_compensation, other_compensation, and benefits. Null when all components are null or for non-paid users.
          example: 135000

    Grant:
      type: object
      description: |
        Schedule I grant to a U.S. organization or government.
      required: [recipient_name]
      properties:
        recipient_name:
          type: string
          example: Habitat for Humanity Atlanta
        recipient_city:
          type: string
          example: Atlanta
        recipient_state:
          type: string
          example: GA
        recipient_ein:
          type: string
        irc_section:
          type: string
          description: IRC section of the recipient (e.g. 501(c)(3)).
        cash_amount:
          type: number
          description: Cash grant amount.
          example: 250000
        non_cash_amount:
          type: number
          description: Non-cash grant amount.
        purpose:
          type: string
          example: Housing construction

    PFGrant:
      type: object
      description: |
        990-PF Part XV grant paid.
      required: [recipient_name]
      properties:
        recipient_name:
          type: string
        recipient_city:
          type: string
        recipient_state:
          type: string
        amount:
          type: number
          description: Grant amount.
        purpose:
          type: string
        relationship:
          type: string
        status:
          type: string

    GrantSearchResult:
      type: object
      description: |
        Cross-filing grant search result (Schedule I + PF Part XV).
      required: [funder_ein, funder_name, recipient_name, object_id, source]
      properties:
        funder_ein:
          type: string
          example: "22-2222222"
        funder_name:
          type: string
          example: Bay Area Tech Foundation
        recipient_name:
          type: string
          example: Code.org
        recipient_city:
          type: string
          example: Seattle
        recipient_state:
          type: string
          example: WA
        amount:
          type: number
          description: Grant amount.
          example: 5000000
        tax_year:
          type: integer
          example: 2023
        tax_period_begin:
          type: string
          format: date
          example: "2023-07-01"
        tax_period_end:
          type: string
          format: date
          example: "2024-06-30"
        object_id:
          type: string
        source:
          type: string
          description: Which form the grant came from.
          enum: ["990", "990-PF"]

    SearchResult:
      type: object
      description: Polymorphic search result ranked by trigram similarity.
      required: [type, score, record]
      properties:
        type:
          type: string
          enum: [org, officer, grant]
        score:
          type: number
          description: Trigram similarity score (0-1).
          example: 0.85
        record:
          type: object
          description: |
            Shape depends on `type`:
            - `org`: `{ein, name, city, state}`
            - `officer`: `{name, title, ein, org_name, object_id}`
            - `grant`: `{recipient_name, amount, funder_ein, funder_name, object_id}`
          additionalProperties: true

  responses:
    BadRequest:
      description: Invalid request parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: bad_request
              message: "invalid sort field: fake_field"

    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: not_found
              message: "No organization found with EIN 99-9999999"

    Unauthorized:
      description: Authentication required.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: unauthorized
              message: "Authentication required"

tags:
  - name: System
    description: Health and status endpoints.
  - name: Organizations
    description: Nonprofit organizations from the IRS Business Master File.
  - name: Filings
    description: IRS 990/990-EZ/990-PF tax filings with dynamic field selection.
  - name: Officers
    description: Officers, directors, trustees, and key employees (Part VII).
  - name: Grants
    description: Schedule I grants and cross-filing grant search.
  - name: Private Foundations
    description: 990-PF specific data and Part XV grants paid.
  - name: Search
    description: Full-text search across organizations, officers, and grants.
  - name: NL Router
    description: Natural language intent router for query classification.
