openapi: 3.1.0
info:
  title: RecruitScreen Integration API
  version: "1.0"
  description: |
    One-way outbound integration API for RecruitScreen.

    Campaigns and candidate screening happen in RecruitScreen; this API pushes
    results **out** to your HR / ATS system via signed **webhooks**, with a
    pollable **read API** as a fallback for systems that cannot receive webhooks.

    - Webhooks follow the [Standard Webhooks](https://www.standardwebhooks.com/) specification.
    - All timestamps are RFC 3339 UTC.
    - Errors use RFC 9457 Problem Details (`application/problem+json`).
    - Use the `external_ref` passthrough (add `?ref=YOUR-ID` to a recruit link)
      to match RecruitScreen candidates to records in your own system.
  contact:
    name: RecruitScreen Support
    url: https://recruitscreen.app
servers:
  - url: https://api.recruitscreen.app/v1
    description: Production
security:
  - apiKey: []
tags:
  - name: Meta
  - name: Campaigns
  - name: Candidates
  - name: Events
  - name: Webhook endpoints

paths:
  /ping:
    get:
      tags: [Meta]
      operationId: ping
      summary: Verify authentication
      description: Returns the organization your API key belongs to. Call this first.
      responses:
        "200":
          description: Authenticated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  organization:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      name: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /campaigns:
    get:
      tags: [Campaigns]
      operationId: listCampaigns
      summary: List campaigns
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/after"
        - name: status
          in: query
          schema: { type: string, enum: [draft, active, closed] }
      responses:
        "200":
          description: Paginated campaigns
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Page"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Campaign" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /campaigns/{campaign_id}/candidates:
    get:
      tags: [Candidates]
      operationId: listCandidates
      summary: List candidates in a campaign
      parameters:
        - name: campaign_id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/after"
        - name: status
          in: query
          description: Filter by screen status.
          schema: { type: string, enum: [started, completed] }
        - name: updated_since
          in: query
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Paginated candidates
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Page"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Candidate" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /candidates/{candidate_id}:
    get:
      tags: [Candidates]
      operationId: getCandidate
      summary: Get a candidate
      description: Full candidate detail including the latest evaluation and a review link.
      parameters:
        - name: candidate_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Candidate
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CandidateDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /events:
    get:
      tags: [Events]
      operationId: listEvents
      summary: List events (polling fallback)
      description: |
        The same event stream delivered by webhooks, as a cursor-paginated feed.
        Poll with `?after=<last event id you processed>` on a schedule. Events are
        strictly ordered and never mutate, so this feed cannot miss or duplicate
        data if you persist your cursor.
      parameters:
        - $ref: "#/components/parameters/limit"
        - name: after
          in: query
          description: Return events after this event id.
          schema: { type: string }
        - name: type
          in: query
          schema:
            type: string
            enum: [candidate.screen_completed, candidate.decision_made]
      responses:
        "200":
          description: Paginated events
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Page"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Event" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /webhook-endpoints:
    get:
      tags: [Webhook endpoints]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      responses:
        "200":
          description: Endpoints
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Webhook endpoints]
      operationId: createWebhookEndpoint
      summary: Create a webhook endpoint
      description: |
        The signing secret (`whsec_...`) is returned **only on creation**. Store it —
        it is required to verify webhook signatures.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS only.
                events:
                  type: array
                  description: Event types to deliver. Defaults to all.
                  items:
                    type: string
                    enum: [candidate.screen_completed, candidate.decision_made]
                description: { type: string, maxLength: 200 }
      responses:
        "201":
          description: Created (includes the signing secret — shown once)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/WebhookEndpoint"
                  - type: object
                    properties:
                      secret:
                        type: string
                        description: Signing secret, `whsec_` prefixed. Shown once.
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /webhook-endpoints/{endpoint_id}:
    delete:
      tags: [Webhook endpoints]
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      parameters:
        - name: endpoint_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "204": { description: Deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhook-endpoints/{endpoint_id}/test:
    post:
      tags: [Webhook endpoints]
      operationId: testWebhookEndpoint
      summary: Send a test event
      description: Queues a signed `test.ping` event to this endpoint.
      parameters:
        - name: endpoint_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "202": { description: Test event queued }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

webhooks:
  candidate.screen_completed:
    post:
      summary: Candidate finished their video screen
      description: |
        Fired when a candidate completes all questions. Signed per the
        Standard Webhooks spec — headers `webhook-id`, `webhook-timestamp`,
        `webhook-signature`. Respond 2xx within 10s; anything else is retried
        with exponential backoff (7 attempts over ~24h).
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScreenCompletedEvent" }
      responses:
        "200": { description: Acknowledged }
  candidate.decision_made:
    post:
      summary: Recruiter recorded a decision
      description: Fired when a recruiter records shortlist / maybe / reject (with rating and notes).
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DecisionMadeEvent" }
      responses:
        "200": { description: Acknowledged }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: rs_live_...
      description: |
        Per-organization API key, created in the portal under **Settings → API**.
        Send as `Authorization: Bearer rs_live_...`.

  parameters:
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    after:
      name: after
      in: query
      description: Cursor — the `id` of the last item from the previous page.
      schema: { type: string }

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    NotFound:
      description: Resource not found (or belongs to another organization)
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    BadRequest:
      description: Invalid request
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }

  schemas:
    Problem:
      type: object
      description: RFC 9457 Problem Details
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }

    Page:
      type: object
      properties:
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"] }

    Campaign:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        status: { type: string, enum: [draft, active, closed] }
        created_at: { type: string, format: date-time }
        close_at: { type: [string, "null"], format: date-time }
        candidate_count: { type: integer }
        completed_count: { type: integer }

    Candidate:
      type: object
      properties:
        id: { type: string, format: uuid }
        campaign_id: { type: string, format: uuid }
        full_name: { type: string }
        email: { type: string, format: email }
        external_ref:
          type: [string, "null"]
          description: Your system's identifier, passed on the recruit link as `?ref=...`.
        status:
          type: string
          enum: [started, completed]
          description: Screen recording status.
        stage:
          type: string
          enum: [applied, reviewed, interview_scheduled, interviewed, offer, hired]
          description: Pipeline stage.
        rejected_at: { type: [string, "null"], format: date-time }
        completed_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }

    Evaluation:
      type: object
      properties:
        rating: { type: [integer, "null"], minimum: 1, maximum: 5 }
        decision: { type: [string, "null"], enum: [shortlist, maybe, reject, null] }
        notes: { type: [string, "null"] }
        decided_at: { type: string, format: date-time }

    CandidateDetail:
      allOf:
        - $ref: "#/components/schemas/Candidate"
        - type: object
          properties:
            campaign:
              type: object
              properties:
                id: { type: string, format: uuid }
                name: { type: string }
            evaluation:
              oneOf:
                - $ref: "#/components/schemas/Evaluation"
                - type: "null"
            review_url:
              type: [string, "null"]
              format: uri
              description: Tokenised link to review the candidate's screen (expires per your org settings).

    Event:
      type: object
      properties:
        id:
          type: string
          description: Stable event id (`evt_...`). Identical across webhook retries — dedupe on it.
        type:
          type: string
          enum: [candidate.screen_completed, candidate.decision_made, test.ping]
        created_at: { type: string, format: date-time }
        data: { type: object }

    ScreenCompletedEvent:
      allOf:
        - $ref: "#/components/schemas/Event"
        - type: object
          properties:
            type: { const: candidate.screen_completed }
            data:
              type: object
              properties:
                candidate: { $ref: "#/components/schemas/Candidate" }
                campaign:
                  type: object
                  properties:
                    id: { type: string, format: uuid }
                    name: { type: string }
                review_url: { type: string, format: uri }

    DecisionMadeEvent:
      allOf:
        - $ref: "#/components/schemas/Event"
        - type: object
          properties:
            type: { const: candidate.decision_made }
            data:
              type: object
              properties:
                candidate: { $ref: "#/components/schemas/Candidate" }
                campaign:
                  type: object
                  properties:
                    id: { type: string, format: uuid }
                    name: { type: string }
                evaluation: { $ref: "#/components/schemas/Evaluation" }
                review_url: { type: string, format: uri }

    WebhookEndpoint:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        description: { type: [string, "null"] }
        status: { type: string, enum: [active, disabled] }
        created_at: { type: string, format: date-time }
