openapi: 3.1.0
info:
  title: 501see API
  description: |
    Public Developer API for IRS 990 nonprofit filing data. Provides access to
    organizations, filings, officers, grants, full-text search, exports, and
    other documented public developer endpoints.

    **Audience**: This spec is the public Developer API contract. Internal
    Grant Research app endpoints are documented separately in
    `openapi.internal.yaml` and are not part of the external API product.

    **Authentication**: Use `Authorization: Bearer YOUR_API_KEY` with an API key
    formatted as `sk_live_...`. Some read endpoints may respond without
    authentication for website/demo use, but production integrations should use
    API keys.

    **Developer API tiers**: Developer Free is limited to verification-oriented
    organization lookup, latest-filing summary lookup, and bounded organization
    name search. Developer Pro includes the broader public data-backed API
    surface in this spec. Developer Free API-key callers receive
    `403 developer_api_pro_required` when calling Developer Pro-only endpoints
    or using Pro-only filters/field selection. Grant Research Pro API keys keep
    Pro-level public API access during the standalone developer product rollout.

    **Private app endpoints** (`/api/v1/me`, `/api/v1/account`,
    `/api/v1/onboarding`, saved searches, recent searches, billing, team,
    Funder Projects, Prospects, Funder Reports, and API-key management) require
    Clerk session authentication and are intentionally excluded from this spec.

    **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**: Multi-year financials are visible broadly.
    Officer compensation remains paid-access only. Grant amount visibility
    follows the current public data policy.

    **MCP server**: An MCP-over-SSE endpoint is available at `/mcp` for
    AI agent integrations. 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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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, formation_year, -formation_year, employees, -employees]
        - 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
        - name: mission
          in: query
          description: Full-text keyword search against the latest mission statement.
          schema:
            type: string
      responses:
        "200":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          description: Organization details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/orgs/{ein}/grants-received:
    get:
      operationId: listGrantsReceived
      summary: List grants received by an organization
      description: |
        Returns a summary of grants received by the organization from Schedule I,
        990-PF Part XV, and Federal Audit Clearinghouse sources.
      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: Max grants to return (1-100, default 20).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          description: Grants received summary and recent grant records.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GrantsReceivedSummary"
        "400":
          $ref: "#/components/responses/BadRequest"

  /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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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: recipient_city
          in: query
          description: Case-insensitive recipient city substring.
          schema:
            type: string
        - 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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          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"

  /api/v1/exports:
    post:
      operationId: createExport
      tags: [Exports]
      summary: Create a CSV export job
      description: |
        Creates an asynchronous export job for the given resource type and filters.
        Poll `GET /api/v1/exports/{id}` until `status=completed`, then download the
        file from `file_url`.

        Requires a paid plan.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resource_type]
              properties:
                resource_type:
                  type: string
                  enum: [organizations, grants, officers]
                filters:
                  type: object
                  additionalProperties: true
                  description: Same filter params as the corresponding list/search endpoint.
                notify_email:
                  type: string
                  description: Optional email address for completion notification.
      responses:
        "202":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          description: Export job accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Export"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

    get:
      operationId: listExports
      tags: [Exports]
      summary: List exports for the authenticated account
      security:
        - bearerAuth: []
      responses:
        "200":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          description: Exports for the current account
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Export"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/exports/{id}:
    get:
      operationId: getExport
      tags: [Exports]
      summary: Get an export job by ID
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          description: Export job
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Export"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /mcp:
    get:
      operationId: openMcpSession
      tags: [MCP]
      summary: Open MCP SSE session
      description: |
        Opens a remote MCP-over-SSE session using the same API key auth and
        endpoint gates as the public REST API. The exposed toolset is limited
        to public developer API capabilities.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: SSE stream established.
        "401":
          description: Missing or invalid API key.
        "403":
          description: API key is valid but the requested MCP capability is not available.
        "429":
          $ref: "#/components/responses/TooManyRequests"
    post:
      operationId: postMcpMessage
      tags: [MCP]
      summary: Send MCP message
      description: |
        Sends an MCP message to an existing SSE session. Uses the same API key
        auth and endpoint gates as the public REST API.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Message accepted.
        "401":
          description: Missing or invalid API key.
        "403":
          description: API key is valid but the requested MCP capability is not available.
        "429":
          $ref: "#/components/responses/TooManyRequests"

    delete:
      operationId: deleteExport
      tags: [Exports]
      summary: Delete an export job by ID
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
        "429":
          $ref: "#/components/responses/TooManyRequests"
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |
        API key (format: sk_live_...). Generate one from your 501see account
        settings. Some read endpoints may work without authentication for
        website/demo use, but production integrations should use an API key.
        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
              deprecated: true
              description: Legacy cached signal. New similarity rows do not use grant overlap for scoring.
            ntee:
              type: number
            geography:
              type: number
            revenue_band:
              type: number
            adjacent_revenue_band:
              type: number
              deprecated: true
              description: Legacy cached signal. Adjacent revenue now contributes through revenue_band.
            mode:
              type: number
            grants_received:
              type: number
            scoring_profile:
              type: string
              enum: [default, international]
            candidate_lanes:
              type: array
              items:
                type: string
            relationship_class:
              type: string
              enum: [peer]
            shared_funders:
              type: array
              description: Shared funder EINs, included as metadata only.
              items:
                type: string
          example:
            ntee: 0.35
            geography: 0.05
            revenue_band: 0.10
            mode: 0.15
            grants_received: 0.05
            scoring_profile: international
            candidate_lanes: ["exact_ntee_revenue", "exact_ntee_state_revenue"]
            relationship_class: peer
            shared_funders: ["46-2626883"]
        rank:
          type: integer
          description: Precomputed rank (1 = most similar).
          example: 1

    GrantReceived:
      type: object
      properties:
        funder_name:
          type: string
          example: Bay Area Tech Foundation
        funder_ein:
          type: string
          example: "22-2222222"
        amount:
          type: number
          nullable: true
          example: 50000
        purpose:
          type: string
          example: General operating support
        tax_year:
          type: integer
          nullable: true
          example: 2023

    GrantsReceivedSummary:
      type: object
      properties:
        grants:
          type: array
          items:
            $ref: "#/components/schemas/GrantReceived"
        total_grant_count:
          type: integer
          example: 42
        total_amount:
          type: number
          nullable: true
          example: 1250000
        funder_count:
          type: integer
          example: 12
        most_recent_year:
          type: integer
          nullable: true
          example: 2023

    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.

    Export:
      type: object
      required: [id, account_id, status, resource_type, filters, fields, created_at]
      properties:
        id:
          type: string
          description: Export job ID (UUID).
        account_id:
          type: integer
          format: int64
        status:
          type: string
          enum: [pending, processing, completed, failed]
        resource_type:
          type: string
          enum: [organizations, grants, officers]
        filters:
          type: object
          additionalProperties: true
        fields:
          type: array
          items: { type: string }
        row_count:
          type: integer
          nullable: true
        file_url:
          type: string
          nullable: true
        file_size_bytes:
          type: integer
          format: int64
          nullable: true
        error_message:
          type: string
          nullable: true
        notify_email:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [bad_request, not_found, internal_error, unauthorized, plan_required, developer_api_pro_required, rate_limited, 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"

    Forbidden:
      description: Plan or Developer API tier is insufficient for this operation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: developer_api_pro_required
              message: "This endpoint requires Developer Pro API access"

    TooManyRequests:
      description: Rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: rate_limited
              message: "Rate limit exceeded"

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: Exports
    description: Asynchronous CSV exports.
