openapi: 3.1.0

info:
  title: SohamRecruit Careers API
  version: "1.0.0"
  description: >
    Public integration API for a SohamRecruit tenant's branded career site.


    Two surfaces are covered here:


    1. The **Careers** surface — reading published jobs, accepting
    applications, checking candidate status, and receiving webhooks. Most reads
    need no auth; submitting applications server-side uses the **Careers API
    Key** (`CareersApiKey`).


    2. The **Data API** (`/api/v1/*`) — read/write access to your jobs,
    candidates, applications, and clients, authenticated with a **Data API
    Key** (`Authorization: Bearer sk_live_…`, the `ApiKey` scheme). Generate one
    under Settings → Data API keys; write endpoints need a key with the 'write'
    scope.


    Keep both keys server-side — never ship them in browser code.
  contact:
    name: SohamRecruit Support
    email: support@sohamrecruit.com
    url: https://sohamrecruit.com/support

servers:
  - url: https://api.sohamrecruit.com
    description: Production

tags:
  - name: Jobs
    description: Read published (open) jobs for a tenant.
  - name: Applications
    description: Submit candidate applications to a job.
  - name: Candidate Status
    description: Anonymous, token-gated candidate application status.
  - name: Webhooks
    description: Signed event payloads SohamRecruit sends to your endpoint.
  - name: Data API — Jobs
    description: Read/write your jobs (Bearer sk_live_ key).
  - name: Data API — Candidates
    description: Read/write your candidates (Bearer sk_live_ key).
  - name: Data API — Applications
    description: Read applications and update their stage (Bearer sk_live_ key).
  - name: Data API — Clients
    description: Read/write your client companies (Bearer sk_live_ key).

components:
  securitySchemes:
    CareersApiKey:
      type: apiKey
      in: header
      name: X-Careers-Api-Key
      description: >
        Per-tenant secret generated by a company admin under
        Settings → Careers API Key. Present it **only from your server**
        (e.g. a Cloudflare Pages secret / backend proxy), never in browser
        JavaScript. A valid key skips the reCAPTCHA requirement on the apply
        endpoint; all other validation (consent, fields, résumé) still applies.
        The prefix is `careers_`. The legacy headers `X-Career-Site-Key` and
        `X-Partner-Apply-Key` are still accepted as aliases.
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: sk_live_
      description: >
        Data API key for `/api/v1/*`. Send as `Authorization: Bearer sk_live_…`.
        Generate and scope it (read, or read+write) under Settings → Data API
        keys — the raw key is shown once. Server-side only. Write endpoints
        require a key with the 'write' scope, else 403.

  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: API key lacks the required scope.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    BadRequest:
      description: Invalid or missing fields.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: Resource not found for this tenant.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  schemas:
    Error:
      type: object
      description: Standard error envelope returned on 4xx/5xx.
      properties:
        error:
          type: string
          description: Human-readable error message.
          example: Job not found
      required:
        - error

    PublicJob:
      type: object
      description: >
        A published job as exposed on the public career surface. All fields
        are strings (empty string when not set). Sensitive/internal job fields
        (placement fee, client, owner) are never included.
      properties:
        id:
          type: string
          format: uuid
          example: 8f14e45f-ceea-467a-9b3e-1c2d3e4f5a6b
        tenantId:
          type: string
          format: uuid
          description: The tenant (agency) that owns this job.
        tenantName:
          type: string
          example: Swaraaj Consultancy
        title:
          type: string
          example: Senior React Developer
        description:
          type: string
          description: Full job description (may contain plain text / basic markup).
        location:
          type: string
          example: Bengaluru, India
        type:
          type: string
          description: Employment type as entered by the tenant (free text).
          example: Full-time
        salary:
          type: string
          description: Display salary string as entered by the tenant.
          example: "₹18–24 LPA"
        salaryMin:
          type: string
          description: Lower bound if provided; empty string otherwise.
          example: "1800000"
        salaryMax:
          type: string
          description: Upper bound if provided; empty string otherwise.
          example: "2400000"
        salaryPeriod:
          type: string
          description: e.g. year / month; empty string if unset.
          example: year
        status:
          type: string
          description: Always `open` on the public surface.
          example: open
        createdAt:
          type: string
          format: date-time
          example: "2026-07-01T09:30:00.000Z"
        expiresAt:
          type: string
          description: ISO-8601 expiry, or empty string if the job never expires.
          example: "2026-09-01T00:00:00.000Z"
        currency:
          type: string
          description: ISO-4217 code for the tenant's currency (defaults to USD).
          example: INR
      required:
        - id
        - tenantId
        - tenantName
        - title
        - description
        - location
        - type
        - salary
        - status
        - createdAt
        - currency

    V1Job:
      type: object
      description: A job as returned by the Data API. Numeric salary bounds are strings.
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string }
        location: { type: string }
        type: { type: string, example: full-time }
        status: { type: string, enum: [draft, open, closed], example: open }
        salary: { type: string }
        salaryMin: { type: [string, "null"] }
        salaryMax: { type: [string, "null"] }
        salaryPeriod: { type: string }
        clientId: { type: [string, "null"], format: uuid }
        clientName: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        closedAt: { type: [string, "null"], format: date-time }
        expiresAt: { type: [string, "null"], format: date-time }

    V1Candidate:
      type: object
      description: A candidate. Résumé text/keys, AI summaries, notes, and demographics are never returned.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        email: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        location: { type: [string, "null"] }
        experience: { type: [string, "null"] }
        currentCompany: { type: [string, "null"] }
        ctc: { type: [string, "null"] }
        noticePeriod: { type: [string, "null"] }
        skills: { type: [string, "null"] }
        status: { type: string, example: new }
        source: { type: [string, "null"] }
        tags: { type: array, items: { type: string } }
        linkedinUrl: { type: [string, "null"] }
        jobId: { type: [string, "null"], format: uuid }
        jobTitle: { type: [string, "null"] }
        rating: { type: [integer, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    V1Application:
      type: object
      description: An application. The status token, placement fees, AI briefs/fit, and client feedback are never returned.
      properties:
        id: { type: string, format: uuid }
        jobId: { type: string, format: uuid }
        jobTitle: { type: [string, "null"] }
        candidateId: { type: [string, "null"], format: uuid }
        name: { type: [string, "null"] }
        email: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        coverLetter: { type: [string, "null"] }
        status: { type: string, example: new }
        source: { type: [string, "null"] }
        appliedAt: { type: string, format: date-time }
        stageEnteredAt: { type: [string, "null"], format: date-time }
        consentedAt: { type: [string, "null"], format: date-time }
        consentWithdrawnAt: { type: [string, "null"], format: date-time }
        updatedAt: { type: string, format: date-time }

    V1Client:
      type: object
      description: A client company. Fee agreements, BD-pipeline fields, notes, and the PII-masking flag are never returned.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        contactName: { type: [string, "null"] }
        contactEmail: { type: [string, "null"] }
        contactPhone: { type: [string, "null"] }
        industry: { type: [string, "null"] }
        website: { type: [string, "null"] }
        status: { type: [string, "null"], example: active }
        billingAddress: { type: [string, "null"] }
        taxId: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    WebhookEnvelope:
      type: object
      description: >
        Every outbound webhook body shares this envelope. The `data` shape
        depends on `event`. Verify the `X-SohamRecruit-Signature-256` header
        (HMAC-SHA256 of the raw body with your endpoint secret) before trusting it.
      properties:
        event:
          type: string
          enum:
            - candidate.created
            - candidate.updated
            - candidate.stage_changed
            - placement.created
            - interview.scheduled
            - application.received
        tenantId:
          type: string
          format: uuid
        timestamp:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: true
          description: Event-specific payload.
      required: [event, tenantId, timestamp, data]

# This file is the machine-readable contract behind the /developers portal page.
paths:
  /api/jobs/public:
    get:
      tags: [Jobs]
      summary: List published jobs
      description: >
        Returns the tenant's open, non-expired jobs, newest first. No auth.
        Pass `tenantId` to scope to one tenant (the normal career-site case).
        Safe to call from the browser to render a jobs list.
      operationId: listPublicJobs
      security: []
      parameters:
        - name: tenantId
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: The tenant (agency) whose jobs to return.
      responses:
        "200":
          description: Open jobs for the tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs:
                    type: array
                    items:
                      $ref: "#/components/schemas/PublicJob"
                required: [jobs]

  /api/jobs/public/{id}:
    get:
      tags: [Jobs]
      summary: Get one published job
      description: >
        Returns a single open job by id. Returns 404 if the job does not exist
        or is no longer open. No auth.
      operationId: getPublicJob
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The job.
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    $ref: "#/components/schemas/PublicJob"
                required: [job]
        "404":
          description: Job not found or not open.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/jobs/public/{id}/apply:
    post:
      tags: [Applications]
      summary: Submit an application
      description: >
        Submit a candidate application to an open job as `multipart/form-data`.


        **Authentication is one of two modes:**

        - **Career site (server-side):** send your `X-Careers-Api-Key`. A valid
          key matching the job's tenant skips reCAPTCHA. Use this from your
          backend proxy — never the browser.

        - **Anonymous (browser):** omit the key and include a `captchaToken`
          from reCAPTCHA v2.


        Either way, consent, field, and résumé validation all apply. A `consent`
        of `true` is **required** (GDPR). On success the candidate is created /
        matched in the tenant's pipeline and a confirmation email with a status
        link is sent.
      operationId: submitApplication
      security:
        - {}
        - CareersApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Candidate full name.
                email:
                  type: string
                  format: email
                phone:
                  type: string
                consent:
                  type: string
                  description: >
                    Must be `true` (also accepts `on`/`1`). GDPR consent to
                    process the applicant's data.
                  example: "true"
                coverLetter:
                  type: string
                  description: Optional cover letter.
                resume:
                  type: string
                  format: binary
                  description: Optional PDF or DOCX résumé (max 10 MB).
                source:
                  type: string
                  maxLength: 50
                  description: Optional provenance tag (e.g. `linkedin`).
                captchaToken:
                  type: string
                  description: >
                    reCAPTCHA v2 token. Required unless a valid
                    `X-Careers-Api-Key` is sent.
              required: [name, email, phone, consent]
      responses:
        "201":
          description: Application accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Application submitted successfully
                  statusUrl:
                    type: string
                    description: Relative path to the candidate's status page.
                    example: /status/9f8c7b6a5d4e3f2a1b0c
                required: [message, statusUrl]
        "400":
          description: >
            Missing required field, missing consent, captcha required/failed,
            or invalid résumé (type/size).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Job not found or no longer accepting applications.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: This email has already applied for this job.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429":
          description: Rate limited. Includes `retryAfterSeconds`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string, example: rate_limited }
                  message: { type: string }
                  retryAfterSeconds: { type: integer, example: 120 }
                required: [error]

  /api/jobs/feed/{tenantId}:
    get:
      tags: [Jobs]
      summary: XML job feed
      description: >
        Public XML feed of the tenant's open jobs in the standard job-wrapping
        format (consumable by LinkedIn / Indeed once syndication is arranged).
        Apply URLs carry `?src=linkedin`. Rate limited to 30 requests / 5 min / IP.
      operationId: jobFeed
      security: []
      parameters:
        - name: tenantId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: An XML `<source>` document wrapping `<job>` entries.
          content:
            application/xml:
              schema:
                type: string
        "429":
          description: Rate limited.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/status/{token}:
    get:
      tags: [Candidate Status]
      summary: Candidate application status
      description: >
        Anonymous, token-gated status for a single application. The token is
        emailed to the candidate on apply (see `statusUrl` from the apply call).
        The response is **sanitized** — it never exposes recruiter PII, internal
        notes, fit scores, or the raw stage vocabulary.
      operationId: candidateStatus
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Sanitized application status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  companyName: { type: string, example: Swaraaj Consultancy }
                  jobTitle: { type: string, example: Senior React Developer }
                  appliedAt: { type: string, format: date-time }
                  updatedAt: { type: string, format: date-time }
                  status:
                    type: object
                    properties:
                      label: { type: string, example: Under review }
                      description: { type: string }
                      step: { type: integer, minimum: 1, maximum: 4, example: 2 }
                      steps:
                        type: array
                        items: { type: string }
                        example: [Application received, Under review, Interview, Decision]
                      outcome:
                        type: string
                        enum: [in_progress, hired, not_selected]
                      closed: { type: boolean }
                    required: [label, step, steps, outcome, closed]
                required: [companyName, jobTitle, appliedAt, status]
        "404":
          description: 'Invalid or expired token. Body: {"error":"invalid_or_expired"}.'
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  # ─── Data API v1 (Bearer sk_live_ key) ────────────────────────────────────
  /api/v1/jobs:
    get:
      tags: [Data API — Jobs]
      summary: List jobs
      operationId: v1ListJobs
      security: [{ ApiKey: [] }]
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: q, in: query, description: Search title/location, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Matching jobs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs: { type: array, items: { $ref: "#/components/schemas/V1Job" } }
                  limit: { type: integer }
                  offset: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Data API — Jobs]
      summary: Create a job
      operationId: v1CreateJob
      security: [{ ApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                description: { type: string }
                location: { type: string }
                type: { type: string }
                status: { type: string, enum: [draft, open, closed] }
                salary: { type: string }
                salaryMin: { type: number }
                salaryMax: { type: number }
                salaryPeriod: { type: string }
                clientId: { type: string }
                clientName: { type: string }
                expiresAt: { type: string, format: date-time }
      responses:
        "201":
          description: Created.
          content: { application/json: { schema: { type: object, properties: { job: { $ref: "#/components/schemas/V1Job" } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/v1/jobs/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Data API — Jobs]
      summary: Get a job
      operationId: v1GetJob
      security: [{ ApiKey: [] }]
      responses:
        "200":
          description: The job.
          content: { application/json: { schema: { type: object, properties: { job: { $ref: "#/components/schemas/V1Job" } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Data API — Jobs]
      summary: Update or close a job
      operationId: v1UpdateJob
      security: [{ ApiKey: [] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                description: { type: string }
                location: { type: string }
                type: { type: string }
                status: { type: string, enum: [draft, open, closed] }
                salaryMin: { type: number }
                salaryMax: { type: number }
                salaryPeriod: { type: string }
                clientName: { type: string }
                expiresAt: { type: string, format: date-time }
      responses:
        "200":
          description: Updated.
          content: { application/json: { schema: { type: object, properties: { job: { $ref: "#/components/schemas/V1Job" } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: The job is a pending client position request; decide it in-app first.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  /api/v1/candidates:
    get:
      tags: [Data API — Candidates]
      summary: List / search candidates
      operationId: v1ListCandidates
      security: [{ ApiKey: [] }]
      parameters:
        - { name: q, in: query, description: Search name/email/skills, schema: { type: string } }
        - { name: status, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Matching candidates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates: { type: array, items: { $ref: "#/components/schemas/V1Candidate" } }
                  limit: { type: integer }
                  offset: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Data API — Candidates]
      summary: Create a candidate
      operationId: v1CreateCandidate
      security: [{ ApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                email: { type: string, format: email }
                phone: { type: string }
                location: { type: string }
                experience: { type: string }
                currentCompany: { type: string }
                ctc: { type: string }
                noticePeriod: { type: string }
                skills: { type: string }
                source: { type: string }
                linkedinUrl: { type: string }
                rating: { type: integer }
                tags: { type: array, items: { type: string } }
                status: { type: string }
      responses:
        "201":
          description: Created.
          content: { application/json: { schema: { type: object, properties: { candidate: { $ref: "#/components/schemas/V1Candidate" } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409":
          description: A candidate with this email already exists (candidateId is returned).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  /api/v1/candidates/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Data API — Candidates]
      summary: Get a candidate
      operationId: v1GetCandidate
      security: [{ ApiKey: [] }]
      responses:
        "200":
          description: The candidate.
          content: { application/json: { schema: { type: object, properties: { candidate: { $ref: "#/components/schemas/V1Candidate" } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Data API — Candidates]
      summary: Update a candidate
      operationId: v1UpdateCandidate
      security: [{ ApiKey: [] }]
      requestBody:
        content:
          application/json:
            schema: { type: object, description: Any subset of the create fields. }
      responses:
        "200":
          description: Updated.
          content: { application/json: { schema: { type: object, properties: { candidate: { $ref: "#/components/schemas/V1Candidate" } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/applications:
    get:
      tags: [Data API — Applications]
      summary: List applications
      operationId: v1ListApplications
      security: [{ ApiKey: [] }]
      parameters:
        - { name: jobId, in: query, description: Filter to one job, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Matching applications.
          content:
            application/json:
              schema:
                type: object
                properties:
                  applications: { type: array, items: { $ref: "#/components/schemas/V1Application" } }
                  limit: { type: integer }
                  offset: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/applications/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Data API — Applications]
      summary: Get an application
      operationId: v1GetApplication
      security: [{ ApiKey: [] }]
      responses:
        "200":
          description: The application.
          content: { application/json: { schema: { type: object, properties: { application: { $ref: "#/components/schemas/V1Application" } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Data API — Applications]
      summary: Update the application stage
      operationId: v1UpdateApplication
      security: [{ ApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [status], properties: { status: { type: string, example: interview } } }
      responses:
        "200":
          description: Updated (a stage-history row is written on a change).
          content: { application/json: { schema: { type: object, properties: { application: { $ref: "#/components/schemas/V1Application" } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/clients:
    get:
      tags: [Data API — Clients]
      summary: List / search clients
      operationId: v1ListClients
      security: [{ ApiKey: [] }]
      parameters:
        - { name: q, in: query, description: Search name/contact/industry, schema: { type: string } }
        - { name: status, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Matching clients.
          content:
            application/json:
              schema:
                type: object
                properties:
                  clients: { type: array, items: { $ref: "#/components/schemas/V1Client" } }
                  limit: { type: integer }
                  offset: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Data API — Clients]
      summary: Create a client
      operationId: v1CreateClient
      security: [{ ApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                contactName: { type: string }
                contactEmail: { type: string }
                contactPhone: { type: string }
                industry: { type: string }
                website: { type: string }
                status: { type: string }
                billingAddress: { type: string }
                taxId: { type: string }
      responses:
        "201":
          description: Created.
          content: { application/json: { schema: { type: object, properties: { client: { $ref: "#/components/schemas/V1Client" } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/v1/clients/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Data API — Clients]
      summary: Get a client
      operationId: v1GetClient
      security: [{ ApiKey: [] }]
      responses:
        "200":
          description: The client.
          content: { application/json: { schema: { type: object, properties: { client: { $ref: "#/components/schemas/V1Client" } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Data API — Clients]
      summary: Update a client
      operationId: v1UpdateClient
      security: [{ ApiKey: [] }]
      requestBody:
        content:
          application/json:
            schema: { type: object, description: Any subset of the create fields. }
      responses:
        "200":
          description: Updated.
          content: { application/json: { schema: { type: object, properties: { client: { $ref: "#/components/schemas/V1Client" } } } } }
        "404": { $ref: "#/components/responses/NotFound" }

# ─── Webhooks (OpenAPI 3.1) ──────────────────────────────────────────────────
# SohamRecruit POSTs these to endpoints a tenant registers under
# Settings → Webhooks. Each carries the WebhookEnvelope body + signature headers.
webhooks:
  candidate.created:
    post: &webhookOp
      summary: Outbound event delivery
      description: >
        POSTed to your registered endpoint. Body is a WebhookEnvelope; verify
        `X-SohamRecruit-Signature-256` before trusting it. Respond with any 2xx
        to acknowledge (non-2xx is retried up to 3×).
      parameters: &webhookHeaders
        - in: header
          name: X-SohamRecruit-Event
          schema: { type: string }
          description: The event type (mirrors the body `event`).
        - in: header
          name: X-SohamRecruit-Signature-256
          schema: { type: string, example: "sha256=a1b2c3…" }
          description: HMAC-SHA256 of the raw JSON body, `sha256=<hex>`.
        - in: header
          name: X-SohamRecruit-Delivery
          schema: { type: string }
          description: Delivering endpoint id (for de-duplication).
      requestBody: &webhookBody
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEnvelope" }
      responses:
        "2XX": { description: Acknowledged. }
  candidate.updated:
    post:
      <<: *webhookOp
      parameters: *webhookHeaders
      requestBody: *webhookBody
  candidate.stage_changed:
    post:
      <<: *webhookOp
      parameters: *webhookHeaders
      requestBody: *webhookBody
  placement.created:
    post:
      <<: *webhookOp
      parameters: *webhookHeaders
      requestBody: *webhookBody
  interview.scheduled:
    post:
      <<: *webhookOp
      parameters: *webhookHeaders
      requestBody: *webhookBody
  application.received:
    post:
      <<: *webhookOp
      parameters: *webhookHeaders
      requestBody: *webhookBody
