openapi: 3.1.0
info:
  title: HackathonRadar API
  version: 1.0.0
  description: |
    Public API for inbound hackathon lead enrichment and suppression management.

    Authenticate every request with `Authorization: Bearer <api_key>`.
servers:
  - url: https://www.hackathonradar.com
    description: Production
  - url: http://localhost:3000
    description: Local development
tags:
  - name: Analytics
    description: Summary stats and trend endpoints for hackathon data.
  - name: Data
    description: Core hackathon, organizer, and sponsor record APIs.
  - name: Exports
    description: CSV exports for hackathons, organizers, and sponsors.
  - name: Inbound
    description: Submit and review inbound lead enrichment jobs.
  - name: Suppression
    description: Check or manage suppression rules before outreach.
  - name: Saved Views
    description: >-
      Reusable ICP searches. Create and list the views that define which
      hackathons matter to you. Scoped to your account.
  - name: Webhooks
    description: >-
      Outbound event delivery. HackathonRadar POSTs to your registered URL when
      hackathons are discovered or updated. These are requests we send to you,
      not endpoints you call.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
  parameters:
    StatusFilter:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
          - pending
          - matched
          - unmatched
          - duplicate
          - suppressed
      description: Filter submissions by processing status.
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
      description: 1-based page number.
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: Number of records per page, capped at 100.
    SubmissionId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Submission UUID.
    RecordId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Resource identifier.
    Search:
      name: search
      in: query
      required: false
      schema:
        type: string
      description: Free-text search term.
    Domain:
      name: domain
      in: query
      required: false
      schema:
        type: string
      description: Filter by exact domain.
    SortBy:
      name: sortBy
      in: query
      required: false
      schema:
        type: string
      description: Sort field.
    SortOrder:
      name: sortOrder
      in: query
      required: false
      schema:
        type: string
        enum:
          - asc
          - desc
      description: Sort direction.
    Columns:
      name: columns
      in: query
      required: false
      schema:
        type: string
      description: Comma-separated column keys to include in CSV export.
    TimeRange:
      name: timeRange
      in: query
      required: false
      schema:
        type: string
      description: Time window used by stats endpoints, for example `year` or `all`.
    Country:
      name: country
      in: query
      required: false
      schema:
        type: string
      description: Filter by country.
    HackathonType:
      name: type
      in: query
      required: false
      schema:
        type: string
        enum:
          - online
          - in-person
          - hybrid
      description: Filter by hackathon format.
    Currency:
      name: currency
      in: query
      required: false
      schema:
        type: string
      description: Currency code used by prize pool analytics.
    Category:
      name: category
      in: query
      required: false
      schema:
        type: string
      description: Filter by category tag.
    StudentOnly:
      name: studentOnly
      in: query
      required: false
      schema:
        type: boolean
      description: Limit results to student-only hackathons.
    Upcoming:
      name: upcoming
      in: query
      required: false
      schema:
        type: boolean
      description: Limit results to upcoming hackathons.
    Ongoing:
      name: ongoing
      in: query
      required: false
      schema:
        type: boolean
      description: Limit results to ongoing hackathons.
    Active:
      name: active
      in: query
      required: false
      schema:
        type: boolean
      description: Limit results to active hackathons (upcoming or ongoing).
    Ended:
      name: ended
      in: query
      required: false
      schema:
        type: boolean
      description: Limit results to ended hackathons.
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
    Pagination:
      type: object
      required:
        - page
        - limit
        - total
        - totalPages
        - hasNext
        - hasPrev
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer
        hasNext:
          type: boolean
        hasPrev:
          type: boolean
    StatsOverviewResponse:
      type: object
      additionalProperties: true
      properties:
        upcoming:
          type: integer
        ongoing:
          type: integer
        ended:
          type: integer
        total:
          type: integer
        newThisWeek:
          type: integer
        generatedAt:
          type:
            - string
            - 'null'
          format: date-time
      example:
        upcoming: 512
        ongoing: 24
        ended: 9321
        total: 9857
        newThisWeek: 37
        generatedAt: '2026-03-03T15:45:38.000Z'
    TrendsResponse:
      type: object
      additionalProperties: true
      properties:
        timeSeries:
          type:
            - object
            - 'null'
          additionalProperties: true
        categoryAnalytics:
          type: object
          additionalProperties: true
        basicStats:
          $ref: '#/components/schemas/StatsOverviewResponse'
        generatedAt:
          type:
            - string
            - 'null'
          format: date-time
      example:
        timeSeries:
          hackathonsOverTime:
            - date: '2026-01-01'
              count: 42
              totalPrizePool: 125000
              averagePrizePool: 2976
        categoryAnalytics:
          mostPopularCategories:
            - category: ai
              count: 220
              percentage: 31.2
        basicStats:
          upcoming: 512
          ongoing: 24
          ended: 9321
          total: 9857
          newThisWeek: 37
          generatedAt: '2026-03-03T15:45:38.000Z'
        generatedAt: '2026-03-03T15:45:38.000Z'
    GeographicResponse:
      type: object
      additionalProperties: true
      properties:
        geographic:
          type:
            - object
            - 'null'
          additionalProperties: true
        generatedAt:
          type:
            - string
            - 'null'
          format: date-time
      example:
        geographic:
          totalCountries: 84
          topCountries:
            - country: USA
              count: 201
              percentage: 22.4
        generatedAt: '2026-03-03T15:45:38.000Z'
    PrizePoolsResponse:
      type: object
      additionalProperties: true
      properties:
        prizePool:
          type:
            - object
            - 'null'
          additionalProperties: true
        timeSeries:
          type:
            - object
            - 'null'
          additionalProperties: true
        generatedAt:
          type:
            - string
            - 'null'
          format: date-time
      example:
        prizePool:
          totalPrizePool: 13500000
          averagePrizePool: 7200
          maxPrizePool: 250000
        timeSeries:
          growthRate:
            hackathons: 0.11
            prizePools: 0.19
        generatedAt: '2026-03-03T15:45:38.000Z'
    HackathonRecord:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
        title:
          type: string
        start_at:
          type:
            - string
            - 'null'
          format: date-time
        end_at:
          type:
            - string
            - 'null'
          format: date-time
        type:
          type:
            - string
            - 'null'
        country:
          type:
            - string
            - 'null'
        city:
          type:
            - string
            - 'null'
        website_url:
          type:
            - string
            - 'null'
        registration_url:
          type:
            - string
            - 'null'
        prize_pool:
          type:
            - number
            - 'null'
          description: >-
            The record's own value. For a programme hosted on HackathonRadar
            this is the programme row's own value, never a total of its
            chapters (programme and chapter rows carry none today), so summing
            a programme and its chapters never counts anything twice.
        participant_count:
          type:
            - integer
            - 'null'
          description: >-
            The record's own value. For a programme hosted on HackathonRadar
            this is the programme row's own value, never a total of its
            chapters (programme and chapter rows carry none today), so summing
            a programme and its chapters never counts anything twice.
        parent_event_id:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            The programme's event id for a chapter of a programme hosted on
            HackathonRadar; null otherwise. A programme and each of its
            chapters are separate records, so group or filter chapters by this
            field.
      example:
        id: 6fa487e4-c891-456f-81f1-2a4bedc12967
        title: TreeLine Hacks
        start_at: '2026-03-06T16:45:00.000Z'
        end_at: '2026-03-08T16:45:00.000Z'
        type: online
        country: null
        city: null
        website_url: https://treelinehacks.com
        registration_url: https://treelinehacks.com
        prize_pool: 5000
        participant_count: 250
        parent_event_id: null
    HackathonDetail:
      allOf:
        - $ref: '#/components/schemas/HackathonRecord'
        - type: object
          properties:
            sponsors:
              type: array
              items:
                type: object
                additionalProperties: true
            organizers:
              type: array
              items:
                type: object
                additionalProperties: true
            quality_score:
              type:
                - object
                - 'null'
              additionalProperties: true
    OrganizerRecord:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
        name:
          type: string
        domain:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        most_recent_hackathon_title:
          type:
            - string
            - 'null'
        most_recent_hackathon_start_at:
          type:
            - string
            - 'null'
          format: date-time
      example:
        id: 76ee2e74-a0b0-4f19-85d9-30f9d2c3f181
        name: Treeline Hacks
        domain: treelinehacks.com
        email: treelinehacks@gmail.com
        most_recent_hackathon_title: TreeLine Hacks
        most_recent_hackathon_start_at: '2026-03-06T16:45:00.000Z'
    OrganizerDetail:
      allOf:
        - $ref: '#/components/schemas/OrganizerRecord'
        - type: object
          properties:
            events:
              type: array
              items:
                type: object
                additionalProperties: true
            quality_score:
              type:
                - object
                - 'null'
              additionalProperties: true
    SponsorRecord:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
        name:
          type: string
        domain:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
      example:
        id: sponsor_123
        name: Acme Cloud
        domain: acme.com
        email: partnerships@acme.com
    SponsorDetail:
      allOf:
        - $ref: '#/components/schemas/SponsorRecord'
        - type: object
          properties:
            events:
              type: array
              items:
                type: object
                additionalProperties: true
    HackathonListResponse:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/HackathonRecord'
        pagination:
          $ref: '#/components/schemas/Pagination'
    OrganizerListResponse:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/OrganizerRecord'
        pagination:
          $ref: '#/components/schemas/Pagination'
    SponsorListResponse:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SponsorRecord'
        pagination:
          $ref: '#/components/schemas/Pagination'
    MappedFields:
      type: object
      additionalProperties: false
      properties:
        event_url:
          type: string
          format: uri
        event_title:
          type: string
        event_start_month:
          type: string
          pattern: '^\d{4}-\d{2}$'
          description: Event month in `YYYY-MM` format.
        event_country:
          type: string
        organizer_email:
          type: string
          format: email
        organizer_name:
          type: string
        organizer_linkedin:
          type: string
          format: uri
    InboundSubmitRequest:
      type: object
      description: |
        Send any JSON object. Unknown top-level fields are stored as the raw source payload.
        If `mapped` is provided, those normalized fields are used for matching, dedupe, and suppression.
      additionalProperties: true
      properties:
        mapped:
          $ref: '#/components/schemas/MappedFields'
      example:
        lead_id: sf-000123
        source: partner-form
        submitted_by: growth-ops
        eventLink: https://www.hackathonparty.com/hackathons/29
        eventName: HuddleHive Wit Hack
        when: '2026-03-07'
        locationText: London
        contactEmail: sam@huddlehive.co.uk
        contactName: Sam Larsen-Disney
        linkedinProfile: https://www.linkedin.com/in/samuel-larsen-disney
        notes: Inbound sponsorship request from website form.
        mapped:
          event_url: https://www.hackathonparty.com/hackathons/29
          event_title: HuddleHive Wit Hack
          event_start_month: '2026-03'
          event_country: London
          organizer_email: sam@huddlehive.co.uk
          organizer_name: Sam Larsen-Disney
          organizer_linkedin: https://www.linkedin.com/in/samuel-larsen-disney
    MatchSummary:
      type: object
      required:
        - confidence
        - explanation
        - event_id
        - organizer_id
        - event_match_method
        - organizer_match_method
        - organizer_inferred_from_event
      properties:
        confidence:
          type: number
          format: float
        explanation:
          type: array
          items:
            type: string
        event_id:
          type:
            - string
            - 'null'
          format: uuid
        organizer_id:
          type:
            - string
            - 'null'
          format: uuid
        event_match_method:
          type:
            - string
            - 'null'
          description: Matching strategy used for the event side.
        organizer_match_method:
          type:
            - string
            - 'null'
          description: Matching or inference strategy used for the organizer side.
        organizer_inferred_from_event:
          type: boolean
          description: True when the organizer was inferred from the matched event's linked organizers.
    EventEnrichment:
      type:
        - object
        - 'null'
      properties:
        title:
          type: string
        start_at:
          type:
            - string
            - 'null'
          format: date-time
        country:
          type:
            - string
            - 'null'
        city:
          type:
            - string
            - 'null'
        format:
          type:
            - string
            - 'null'
        quality_score:
          type:
            - integer
            - 'null'
        quality_breakdown:
          type:
            - object
            - 'null'
          additionalProperties: true
        parent_event_id:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            The programme's event id for a chapter of a programme hosted on
            HackathonRadar; null otherwise. A programme and each of its
            chapters are separate records, so group or filter chapters by this
            field.
    OrganizerEnrichment:
      type:
        - object
        - 'null'
      properties:
        name:
          type: string
        email:
          type:
            - string
            - 'null'
          format: email
        num_events_seen:
          type:
            - integer
            - 'null'
        domain_events_seen:
          type:
            - integer
            - 'null'
          description: Returns `0` for personal mailbox domains such as gmail.com.
        first_seen_at:
          type:
            - string
            - 'null'
          format: date-time
        last_seen_at:
          type:
            - string
            - 'null'
          format: date-time
    SuppressionSummary:
      type: object
      required:
        - is_duplicate_inbound
        - is_suppressed
        - reasons
      properties:
        is_duplicate_inbound:
          type: boolean
        is_suppressed:
          type: boolean
        reasons:
          type: array
          items:
            type: string
    InboundSubmitResponse:
      type: object
      required:
        - submission_id
        - match
        - event
        - organizer
        - suppression
      properties:
        submission_id:
          type: string
          format: uuid
        match:
          $ref: '#/components/schemas/MatchSummary'
        event:
          $ref: '#/components/schemas/EventEnrichment'
        organizer:
          $ref: '#/components/schemas/OrganizerEnrichment'
        suppression:
          $ref: '#/components/schemas/SuppressionSummary'
      example:
        submission_id: 76950fb0-2657-4e2d-9ebd-8003c4d34d70
        match:
          confidence: 0.62
          explanation:
            - Event matched via title_date (confidence: 0.62)
            - Organizer inferred from matched event via event_inferred (confidence: 0.62)
          event_id: 6fa487e4-c891-456f-81f1-2a4bedc12967
          organizer_id: 76ee2e74-a0b0-4f19-85d9-30f9d2c3f181
          event_match_method: title_date
          organizer_match_method: event_inferred
          organizer_inferred_from_event: true
        event:
          title: TreeLine Hacks
          start_at: '2026-03-06T16:45:00.000Z'
          country: null
          city: null
          format: online
          quality_score: 55
          quality_breakdown:
            buckets: []
        organizer:
          name: Treeline Hacks
          email: treelinehacks@gmail.com
          num_events_seen: 1
          domain_events_seen: 0
          first_seen_at: '2026-03-01T12:00:00.000Z'
          last_seen_at: '2026-03-06T16:45:00.000Z'
        suppression:
          is_duplicate_inbound: false
          is_suppressed: false
          reasons: []
    InboundSubmissionRecord:
      type: object
      additionalProperties: true
      description: Stored inbound submission row, returned directly from the database model.
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
        key_id:
          type: string
        submitted_at:
          type: string
          format: date-time
        raw_payload:
          type: object
          additionalProperties: true
        normalized_json:
          type:
            - object
            - 'null'
          additionalProperties: true
        event_url:
          type:
            - string
            - 'null'
        event_title:
          type:
            - string
            - 'null'
        event_start_month:
          type:
            - string
            - 'null'
        event_country:
          type:
            - string
            - 'null'
        organizer_email:
          type:
            - string
            - 'null'
        organizer_name:
          type:
            - string
            - 'null'
        organizer_linkedin:
          type:
            - string
            - 'null'
        dedupe_key:
          type:
            - string
            - 'null'
        is_duplicate:
          type: boolean
        duplicate_of_id:
          type:
            - string
            - 'null'
        matched_event_id:
          type:
            - string
            - 'null'
        matched_organizer_id:
          type:
            - string
            - 'null'
        match_confidence:
          type:
            - number
            - 'null'
        match_method:
          type:
            - string
            - 'null'
        match_explanation:
          type:
            - array
            - 'null'
          items:
            type: string
        event_quality_score:
          type:
            - integer
            - 'null'
        organizer_quality_score:
          type:
            - integer
            - 'null'
        organizer_event_count:
          type:
            - integer
            - 'null'
        organizer_domain_event_count:
          type:
            - integer
            - 'null'
        is_suppressed:
          type: boolean
        suppression_reasons:
          type:
            - array
            - 'null'
          items:
            type: string
        status:
          type: string
        processed_at:
          type:
            - string
            - 'null'
          format: date-time
      example:
        id: 76950fb0-2657-4e2d-9ebd-8003c4d34d70
        user_id: user_123
        key_id: api_key_456
        submitted_at: '2026-03-03T15:45:38.000Z'
        raw_payload:
          lead_id: sf-000123
          source: partner-form
          eventName: TreeLine Hacks
        normalized_json:
          event_url: null
          event_title: TreeLine Hacks
          event_start_month: '2026-03'
          event_country: null
          organizer_email: null
          organizer_name: null
          organizer_linkedin: null
        event_url: null
        event_title: TreeLine Hacks
        event_start_month: '2026-03'
        event_country: null
        organizer_email: null
        organizer_name: null
        organizer_linkedin: null
        dedupe_key: hash:abc123
        is_duplicate: false
        duplicate_of_id: null
        matched_event_id: 6fa487e4-c891-456f-81f1-2a4bedc12967
        matched_organizer_id: 76ee2e74-a0b0-4f19-85d9-30f9d2c3f181
        match_confidence: 0.62
        match_method: title_date
        match_explanation:
          - Event matched via title_date (confidence: 0.62)
          - Organizer inferred from matched event via event_inferred (confidence: 0.62)
        event_quality_score: 55
        organizer_quality_score: 48
        organizer_event_count: 1
        organizer_domain_event_count: 0
        is_suppressed: false
        suppression_reasons: null
        status: matched
        processed_at: '2026-03-03T15:45:38.200Z'
    PaginatedSubmissions:
      type: object
      required:
        - data
        - page
        - limit
        - total
        - totalPages
        - hasNext
        - hasPrev
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/InboundSubmissionRecord'
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer
        hasNext:
          type: boolean
        hasPrev:
          type: boolean
      example:
        data:
          - id: 76950fb0-2657-4e2d-9ebd-8003c4d34d70
            user_id: user_123
            key_id: api_key_456
            submitted_at: '2026-03-03T15:45:38.000Z'
            raw_payload:
              lead_id: sf-000123
              source: partner-form
              eventName: TreeLine Hacks
            normalized_json:
              event_title: TreeLine Hacks
              event_start_month: '2026-03'
            event_url: null
            event_title: TreeLine Hacks
            event_start_month: '2026-03'
            event_country: null
            organizer_email: null
            organizer_name: null
            organizer_linkedin: null
            dedupe_key: hash:abc123
            is_duplicate: false
            duplicate_of_id: null
            matched_event_id: 6fa487e4-c891-456f-81f1-2a4bedc12967
            matched_organizer_id: 76ee2e74-a0b0-4f19-85d9-30f9d2c3f181
            match_confidence: 0.62
            match_method: title_date
            match_explanation:
              - Event matched via title_date (confidence: 0.62)
            event_quality_score: 55
            organizer_quality_score: 48
            organizer_event_count: 1
            organizer_domain_event_count: 0
            is_suppressed: false
            suppression_reasons: null
            status: matched
            processed_at: '2026-03-03T15:45:38.200Z'
        page: 1
        limit: 25
        total: 1
        totalPages: 1
        hasNext: false
        hasPrev: false
    SuppressionCheckRequest:
      type: object
      description: Partial normalized fields to evaluate for suppression.
      additionalProperties: false
      properties:
        event_url:
          type: string
          format: uri
        event_title:
          type: string
        event_start_month:
          type: string
          pattern: '^\d{4}-\d{2}$'
        event_country:
          type: string
        organizer_email:
          type: string
          format: email
        organizer_name:
          type: string
        organizer_linkedin:
          type: string
          format: uri
      example:
        event_url: https://www.hackathonparty.com/hackathons/29
        organizer_email: sam@huddlehive.co.uk
    SuppressionCheckResponse:
      type: object
      required:
        - is_suppressed
        - reasons
      properties:
        is_suppressed:
          type: boolean
        reasons:
          type: array
          items:
            type: string
    SuppressionAddRequest:
      type: object
      required:
        - suppression_type
        - value
      properties:
        suppression_type:
          type: string
          enum:
            - email
            - domain
            - organizer_id
            - event_url
        value:
          type: string
        reason:
          type:
            - string
            - 'null'
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
      example:
        suppression_type: domain
        value: gmail.com
        reason: Ignore personal inbox domains for outbound sponsorship.
    SuppressionEntry:
      type: object
      required:
        - id
        - user_id
        - suppression_type
        - value
        - created_at
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
        suppression_type:
          type: string
        value:
          type: string
        reason:
          type:
            - string
            - 'null'
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    WebhookOrganizer:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
        name:
          type: string
        domain:
          type:
            - string
            - 'null'
        website_url:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        email_confidence:
          type:
            - number
            - 'null'
          description: Confidence score (0–1) that the organizer email is valid.
        twitter:
          type:
            - string
            - 'null'
        linkedin:
          type:
            - string
            - 'null'
        instagram:
          type:
            - string
            - 'null'
        image_url:
          type:
            - string
            - 'null'
    WebhookEventPayload:
      type: object
      additionalProperties: true
      description: >-
        The JSON body POSTed to your endpoint for each hackathon event. Contains
        the full hackathon record plus delivery metadata, organizers, and the
        data quality score.
      required:
        - id
        - title
        - event_type
        - timestamp
        - first_seen_at
        - last_seen_at
        - organizers
      properties:
        event_type:
          type: string
          enum:
            - created
            - updated
          description: >-
            `created` for newly discovered hackathons; `updated` for changes to
            an existing record.
        timestamp:
          type: string
          format: date-time
          description: When this delivery was generated (ISO 8601, UTC).
        id:
          type: string
          format: uuid
        source:
          type:
            - string
            - 'null'
          description: Ingestion source the record was discovered from.
        source_id:
          type:
            - string
            - 'null'
        title:
          type: string
        start_at:
          type:
            - string
            - 'null'
          format: date-time
        end_at:
          type:
            - string
            - 'null'
          format: date-time
        type:
          type:
            - string
            - 'null'
          enum:
            - online
            - in-person
            - hybrid
            - null
          description: Event format.
        timezone:
          type:
            - string
            - 'null'
        categories:
          type:
            - array
            - 'null'
          items:
            type: string
        prize_pool:
          type:
            - number
            - 'null'
          description: >-
            The record's own value. For a programme hosted on HackathonRadar
            this is the programme row's own value, never a total of its
            chapters (programme and chapter rows carry none today), so summing
            a programme and its chapters never counts anything twice.
        prize_currency:
          type:
            - string
            - 'null'
        prize_pool_usd:
          type:
            - number
            - 'null'
        venue_name:
          type:
            - string
            - 'null'
        address:
          type:
            - string
            - 'null'
        city:
          type:
            - string
            - 'null'
        country:
          type:
            - string
            - 'null'
        lat:
          type:
            - number
            - 'null'
        lng:
          type:
            - number
            - 'null'
        website_url:
          type:
            - string
            - 'null'
        registration_url:
          type:
            - string
            - 'null'
        cash_prizes:
          type:
            - boolean
            - 'null'
        student_only:
          type:
            - boolean
            - 'null'
        participant_count:
          type:
            - integer
            - 'null'
          description: >-
            The record's own value. For a programme hosted on HackathonRadar
            this is the programme row's own value, never a total of its
            chapters (programme and chapter rows carry none today), so summing
            a programme and its chapters never counts anything twice.
        description:
          type:
            - string
            - 'null'
        description_html:
          type:
            - string
            - 'null'
        description_markdown:
          type:
            - string
            - 'null'
        description_plain_text:
          type:
            - string
            - 'null'
        language:
          type:
            - string
            - 'null'
        data_quality_score:
          type: number
          description: Overall completeness/quality score for the record.
        first_seen_at:
          type: string
          format: date-time
          description: >-
            When the hackathon first entered the dataset. Equal to
            `last_seen_at` on newly discovered events.
        last_seen_at:
          type: string
          format: date-time
          description: When the record was last seen or updated.
        featured:
          type: boolean
        source_urls:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              source:
                type: string
              source_id:
                type:
                  - string
                  - 'null'
              url:
                type: string
        parent_event_id:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            The programme's event id for a chapter of a programme hosted on
            HackathonRadar; null otherwise. A programme and each of its
            chapters are separate records, so group or filter chapters by this
            field.
        organizers:
          type: array
          items:
            $ref: '#/components/schemas/WebhookOrganizer'
        quality_score:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: >-
            Detailed quality breakdown (`score_total`, `confidence`,
            `score_version`, `breakdown_json`, `reasons_json`, `computed_at`).
            Null if not yet computed.
      example:
        event_type: created
        timestamp: '2026-03-01T09:12:04.000Z'
        id: 6fa487e4-c891-456f-81f1-2a4bedc12967
        source: web
        source_id: treelinehacks
        title: TreeLine Hacks
        start_at: '2026-03-06T16:45:00.000Z'
        end_at: '2026-03-08T16:45:00.000Z'
        type: online
        timezone: America/Los_Angeles
        categories:
          - AI/ML
          - Sustainability
        prize_pool: 5000
        prize_currency: USD
        prize_pool_usd: 5000
        venue_name: null
        address: null
        city: null
        country: null
        lat: null
        lng: null
        website_url: https://treelinehacks.com
        registration_url: https://treelinehacks.com
        cash_prizes: true
        student_only: false
        participant_count: 250
        description: A weekend hackathon focused on climate tech.
        language: en
        data_quality_score: 0.78
        first_seen_at: '2026-03-01T09:12:04.000Z'
        last_seen_at: '2026-03-01T09:12:04.000Z'
        featured: false
        source_urls:
          - source: web
            source_id: treelinehacks
            url: https://treelinehacks.com
        organizers:
          - id: 76ee2e74-a0b0-4f19-85d9-30f9d2c3f181
            name: Treeline Hacks
            domain: treelinehacks.com
            website_url: https://treelinehacks.com
            email: treelinehacks@gmail.com
            email_confidence: 0.92
            twitter: null
            linkedin: null
            instagram: null
            image_url: null
        quality_score:
          score_total: 0.78
          confidence: 0.81
          score_version: v2
    SavedViewFilters:
      type: object
      description: >-
        ICP filters defining which hackathons a view matches. All optional;
        omit a field to leave it unconstrained.
      properties:
        country:
          type: string
          description: Case-insensitive partial match on country.
        region:
          type: string
          description: >-
            One of the regional buckets (e.g. "North America", "Europe",
            "APAC", "Latin America", "Middle East & Africa", "United Kingdom").
            Expands to that region's countries.
        type:
          type: string
          enum: [online, in-person, hybrid]
        category:
          type: string
        studentOnly:
          type: boolean
        minPrizeUsd:
          type: number
        minQualityScore:
          type: number
          description: Minimum data quality score (0–100).
        minParticipants:
          type: integer
        excludeSources:
          type: array
          items:
            type: string
          description: Listing platforms to exclude, by key.
      additionalProperties: false
    SavedView:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        query:
          type: string
          nullable: true
          description: Optional natural-language query used for semantic matching.
        filters:
          $ref: '#/components/schemas/SavedViewFilters'
        created_at:
          type: string
          format: date-time
      required: [id, name, filters, created_at]
    SavedViewCreateRequest:
      type: object
      properties:
        name:
          type: string
        query:
          type: string
          description: Optional natural-language query for semantic matching.
        filters:
          $ref: '#/components/schemas/SavedViewFilters'
        notify:
          type: boolean
          default: true
          description: >-
            Whether to email you when newly listed hackathons match. Set false
            to save the view paused; turn alerts on later at /settings/saved-searches.
      required: [name]
    SavedViewList:
      type: object
      properties:
        saved_views:
          type: array
          items:
            $ref: '#/components/schemas/SavedView'
      required: [saved_views]
security:
  - bearerAuth: []
paths:
  /api/stats:
    get:
      tags:
        - Analytics
      summary: Get summary hackathon stats
      responses:
        '200':
          description: Summary counts and stats.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsOverviewResponse'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stats request failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/stats/trends:
    get:
      tags:
        - Analytics
      summary: Get trend and category analytics
      parameters:
        - $ref: '#/components/parameters/TimeRange'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/HackathonType'
      responses:
        '200':
          description: Trend analytics payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrendsResponse'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Trend request failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/stats/geographic:
    get:
      tags:
        - Analytics
      summary: Get geographic hackathon distribution
      parameters:
        - $ref: '#/components/parameters/TimeRange'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/HackathonType'
      responses:
        '200':
          description: Geographic analytics payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GeographicResponse'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Geographic request failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/stats/prize-pools:
    get:
      tags:
        - Analytics
      summary: Get prize pool analytics
      parameters:
        - $ref: '#/components/parameters/TimeRange'
        - $ref: '#/components/parameters/Currency'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/HackathonType'
      responses:
        '200':
          description: Prize pool analytics payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrizePoolsResponse'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Prize pool request failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/hackathons/{id}:
    get:
      tags:
        - Data
      summary: Fetch a single hackathon
      parameters:
        - $ref: '#/components/parameters/RecordId'
      responses:
        '200':
          description: Hackathon detail with relations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HackathonDetail'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Hackathon not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Fetch failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/hackathons/export:
    get:
      tags:
        - Exports
      summary: Export hackathons as CSV
      parameters:
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/HackathonType'
        - $ref: '#/components/parameters/Category'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/StudentOnly'
        - $ref: '#/components/parameters/Upcoming'
        - $ref: '#/components/parameters/Ongoing'
        - $ref: '#/components/parameters/Active'
        - $ref: '#/components/parameters/Ended'
        - $ref: '#/components/parameters/Columns'
      responses:
        '200':
          description: Hackathon CSV export.
          content:
            text/csv:
              schema:
                type: string
                format: binary
              example: |
                Title,Start Date,End Date,Type,Country
                "TreeLine Hacks","Mar 6, 2026","Mar 8, 2026","online",""
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Export failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/organizers:
    get:
      tags:
        - Data
      summary: List organizers
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/Domain'
        - $ref: '#/components/parameters/SortBy'
        - $ref: '#/components/parameters/SortOrder'
      responses:
        '200':
          description: Paginated organizer list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizerListResponse'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Listing failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/organizers/{id}:
    get:
      tags:
        - Data
      summary: Fetch a single organizer
      parameters:
        - $ref: '#/components/parameters/RecordId'
      responses:
        '200':
          description: Organizer detail with event history and quality score.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizerDetail'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Organizer not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Fetch failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/organizers/export:
    get:
      tags:
        - Exports
      summary: Export organizers as CSV
      parameters:
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/Domain'
        - $ref: '#/components/parameters/Columns'
      responses:
        '200':
          description: Organizer CSV export.
          content:
            text/csv:
              schema:
                type: string
                format: binary
              example: |
                Name,Domain,Email
                "Treeline Hacks","treelinehacks.com","treelinehacks@gmail.com"
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Export failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/sponsors:
    get:
      tags:
        - Data
      summary: List sponsors
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/Domain'
        - $ref: '#/components/parameters/SortBy'
        - $ref: '#/components/parameters/SortOrder'
      responses:
        '200':
          description: Paginated sponsor list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SponsorListResponse'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Listing failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/sponsors/{id}:
    get:
      tags:
        - Data
      summary: Fetch a single sponsor
      parameters:
        - $ref: '#/components/parameters/RecordId'
      responses:
        '200':
          description: Sponsor detail with sponsored events.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SponsorDetail'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Sponsor not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Fetch failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/sponsors/export:
    get:
      tags:
        - Exports
      summary: Export sponsors as CSV
      parameters:
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/Domain'
        - $ref: '#/components/parameters/Columns'
      responses:
        '200':
          description: Sponsor CSV export.
          content:
            text/csv:
              schema:
                type: string
                format: binary
              example: |
                Name,Domain,Email
                "Acme Cloud","acme.com","partnerships@acme.com"
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Export failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/inbound/submit:
    post:
      tags:
        - Inbound
      summary: Submit and process an inbound lead
      description: |
        Normalizes the inbound payload, checks dedupe and suppression rules, attempts event and organizer matching,
        snapshots quality signals, and returns enrichment in a single round-trip.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InboundSubmitRequest'
      responses:
        '201':
          description: Lead processed successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboundSubmitResponse'
        '400':
          description: Invalid JSON object body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Processing failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/inbound/submissions:
    get:
      tags:
        - Inbound
      summary: List stored inbound submissions
      parameters:
        - $ref: '#/components/parameters/StatusFilter'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Paginated submission list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedSubmissions'
              examples:
                matchedSubmissionPage:
                  summary: Single matched submission
                  value:
                    data:
                      - id: 76950fb0-2657-4e2d-9ebd-8003c4d34d70
                        user_id: user_123
                        key_id: api_key_456
                        submitted_at: '2026-03-03T15:45:38.000Z'
                        raw_payload:
                          lead_id: sf-000123
                          source: partner-form
                          eventName: TreeLine Hacks
                        normalized_json:
                          event_title: TreeLine Hacks
                          event_start_month: '2026-03'
                        event_url: null
                        event_title: TreeLine Hacks
                        event_start_month: '2026-03'
                        event_country: null
                        organizer_email: null
                        organizer_name: null
                        organizer_linkedin: null
                        dedupe_key: hash:abc123
                        is_duplicate: false
                        duplicate_of_id: null
                        matched_event_id: 6fa487e4-c891-456f-81f1-2a4bedc12967
                        matched_organizer_id: 76ee2e74-a0b0-4f19-85d9-30f9d2c3f181
                        match_confidence: 0.62
                        match_method: title_date
                        match_explanation:
                          - Event matched via title_date (confidence: 0.62)
                        event_quality_score: 55
                        organizer_quality_score: 48
                        organizer_event_count: 1
                        organizer_domain_event_count: 0
                        is_suppressed: false
                        suppression_reasons: null
                        status: matched
                        processed_at: '2026-03-03T15:45:38.200Z'
                    page: 1
                    limit: 25
                    total: 1
                    totalPages: 1
                    hasNext: false
                    hasPrev: false
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Listing failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/inbound/submissions/{id}:
    get:
      tags:
        - Inbound
      summary: Fetch a single inbound submission
      parameters:
        - $ref: '#/components/parameters/SubmissionId'
      responses:
        '200':
          description: Submission record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboundSubmissionRecord'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Submission not found for this account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Fetch failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/inbound/submissions/export:
    get:
      tags:
        - Inbound
      summary: Export submissions as CSV
      parameters:
        - $ref: '#/components/parameters/StatusFilter'
      responses:
        '200':
          description: CSV export of submissions.
          content:
            text/csv:
              schema:
                type: string
                format: binary
              example: |
                submission_id,status,submitted_at,event_title,event_url,event_country,event_start_month,organizer_name,organizer_email,match_confidence,match_method,matched_event_id,matched_organizer_id,event_quality_score,organizer_quality_score,organizer_event_count,organizer_domain_event_count,is_duplicate,is_suppressed
                "76950fb0-2657-4e2d-9ebd-8003c4d34d70","matched","2026-03-03T15:45:38.000Z","TreeLine Hacks","","","2026-03","","","0.62","title_date","6fa487e4-c891-456f-81f1-2a4bedc12967","76ee2e74-a0b0-4f19-85d9-30f9d2c3f181","55","48","1","0","false","false"
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Export failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/suppression/check:
    post:
      tags:
        - Suppression
      summary: Check suppression signals
      description: |
        Evaluates whether the provided normalized fields are currently suppressed,
        including recent outbound contact suppression.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuppressionCheckRequest'
      responses:
        '200':
          description: Suppression evaluation result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuppressionCheckResponse'
        '400':
          description: Invalid JSON object body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Check failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/suppression/add:
    post:
      tags:
        - Suppression
      summary: Create or update a suppression rule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuppressionAddRequest'
      responses:
        '201':
          description: Suppression rule created or updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuppressionEntry'
        '400':
          description: Missing required fields or invalid suppression type.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Creation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/saved-views:
    get:
      tags:
        - Saved Views
      summary: List saved views
      description: Returns the saved views (reusable ICP searches) for your account.
      responses:
        '200':
          description: Your saved views.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedViewList'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      tags:
        - Saved Views
      summary: Create a saved view
      description: >-
        Creates a reusable ICP search. A view holds filters only — the company a
        report is for is supplied separately, so one view can serve many.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SavedViewCreateRequest'
      responses:
        '201':
          description: Saved view created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  saved_view:
                    $ref: '#/components/schemas/SavedView'
                required: [saved_view]
        '400':
          description: Missing name or invalid body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
webhooks:
  hackathonEvent:
    post:
      tags:
        - Webhooks
      summary: Hackathon discovered or updated
      operationId: hackathonEventWebhook
      description: |
        Sent by HackathonRadar to the URL you register, once per hackathon event.

        **This is an outbound request we make to you** — not an endpoint you call.

        **Subscription modes** (chosen when the webhook is registered):
        - `newly_seen` — fires only on newly discovered hackathons.
        - `all` — fires on new hackathons **and** every subsequent update.

        Use the `event_type` field (`created` / `updated`) and the
        `first_seen_at` vs `last_seen_at` timestamps to distinguish new
        discoveries from updates.

        **Delivery** is near-real-time and batched. Each event is a single
        `POST` with `Content-Type: application/json`.

        **Authentication:** any custom headers configured at registration
        (e.g. a shared secret in `Authorization` or `X-Webhook-Token`) are sent
        on every request. Verify them on your endpoint. Payloads are not
        HMAC-signed.

        **Acknowledgement:** respond with a `2xx` status promptly. Non-2xx
        responses are treated as failures. Failed deliveries are not
        automatically retried, so keep your endpoint highly available.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEventPayload'
      responses:
        '200':
          description: >-
            Return any 2xx status to acknowledge receipt. The response body is
            ignored.
        '4XX':
          description: >-
            Treated as a delivery failure. The event is not retried
            automatically.
        '5XX':
          description: >-
            Treated as a delivery failure. The event is not retried
            automatically.
