openapi: 3.1.0
info:
  title: HonestFit Public API
  version: v1
  description: |-
    Implementation-true spec for the shipped HonestFit public-safe partner API.

    This surface exposes only the candidate-controlled public projection and public-fit analysis already allowed on the public candidate page—shareable professional profile, explainable job fit, and trust-backed claim labels the candidate chose to surface. It is a server-to-server integration for tools and teams that already have a workflow; it is not a HonestFit recruiter dashboard product.

    Partner quickstart:
    - Base path: `https://honestfit.ai/api/v1`
    - Authorization: `Authorization: Bearer <api_key>`
    - Required scopes: `public_candidate.read` for profile reads and `public_candidate.fit` for fit analysis
    - Correlation: send `X-Request-Id` and HonestFit will echo it in the response header and body
    - Common errors: `401` for missing/invalid key, `403` for missing scope or fit disabled, `404` for a non-public slug, `429` for rate limiting, `503` when Public API key authentication is intentionally not configured

    Public candidate pages (`/c/{slug}`, `/c/{slug}.md`) and `GET /api/public-profile` do not require partner keys. This OpenAPI documents the **partner** `/api/v1` surface, which depends on Public API configuration.

    Narrative guide and longer examples live in `docs/api/public-api-v1.md`. The same markdown and this YAML are also served on the public site at `/docs/api/public-api-v1.md` and `/docs/api/public-api-v1.openapi.yaml` (HTML reference at `/docs/api/public-api-v1`); HTTP calls to `/api/v1` still require a partner API key. Public profile pages, public profile markdown, and `/api/public-profile` are separate public-profile surfaces and can remain available even when Public API v1 key authentication is not configured.
servers:
  - url: https://honestfit.ai/api/v1
    description: Production
security:
  - bearerAuth: []
paths:
  /candidates/{publicSlug}:
    get:
      operationId: getPublicCandidate
      summary: Get a public-safe candidate projection
      description: |-
        Returns the candidate-controlled public projection for a single public candidate slug.

        Required scope: `public_candidate.read`

        Use this first when a partner wants the public-safe profile artifact before deciding whether to run fit.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PublicSlug'
        - $ref: '#/components/parameters/XRequestId'
      responses:
        '200':
          description: Public-safe candidate projection
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateGetResponse'
              examples:
                success:
                  $ref: '#/components/examples/CandidateGetSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ReadScopeForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/PublicApiNotConfigured'
        '500':
          $ref: '#/components/responses/InternalError'
  /candidates/{publicSlug}/fit:
    post:
      operationId: analyzePublicCandidateFit
      summary: Analyze fit against a public candidate projection
      description: |-
        Runs synchronous fit analysis grounded only in the candidate-controlled public projection.

        Required scope: `public_candidate.fit`

        The request body is strict JSON with `jobDescription` only. Returned reasoning is public-safe and never relies on hidden private profile fields.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PublicSlug'
        - $ref: '#/components/parameters/XRequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CandidateFitRequest'
            examples:
              basic:
                value:
                  jobDescription: |-
                    Staff Frontend Engineer

                    Responsibilities
                    - Lead design systems and component libraries.
                    - Mentor frontend engineers.

                    Preferred
                    - Own accessibility program direction.
      responses:
        '200':
          description: Public-fit analysis
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateFitResponse'
              examples:
                success:
                  $ref: '#/components/examples/CandidateFitSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Missing fit scope or candidate fit disabled
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                scopeDenied:
                  $ref: '#/components/examples/FitScopeForbidden'
                fitDisabled:
                  $ref: '#/components/examples/FitDisabledForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/PublicApiNotConfigured'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |-
        Server-to-server Bearer API key.

        Format exactly as:
        `Authorization: Bearer <api_key>`

        Keys are scope-limited. Shipped scopes are `public_candidate.read` and `public_candidate.fit`.
  parameters:
    PublicSlug:
      name: publicSlug
      in: path
      required: true
      schema:
        type: string
      description: Canonical public candidate slug.
    XRequestId:
      name: X-Request-Id
      in: header
      required: false
      schema:
        type: string
        example: partner-read-001
      description: Partner-supplied correlation ID echoed back in the response header and body.
  headers:
    XRequestId:
      description: Request correlation ID echoed from the request or generated by HonestFit.
      schema:
        type: string
        example: partner-read-001
  responses:
    Unauthorized:
      description: Missing or invalid API key
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        WWW-Authenticate:
          description: Bearer challenge returned when authentication is missing or invalid.
          schema:
            type: string
            example: Bearer realm="HonestFit Public API"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            missingKey:
              $ref: '#/components/examples/Unauthorized'
    ReadScopeForbidden:
      description: API key lacks the public_candidate.read scope
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            scopeDenied:
              $ref: '#/components/examples/ReadScopeForbidden'
    NotFound:
      description: Candidate slug is not publicly addressable
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            missing:
              $ref: '#/components/examples/NotFound'
    PayloadTooLarge:
      description: Request body exceeded the supported size
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            tooLarge:
              $ref: '#/components/examples/PayloadTooLarge'
    UnsupportedMediaType:
      description: Content-Type must be application/json
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            unsupported:
              value:
                error:
                  code: UNSUPPORTED_MEDIA_TYPE
                  message: Content-Type must be application/json.
                  status: 415
                  requestId: partner-fit-001
                  retryable: false
    RateLimited:
      description: Per-key or per-slug limit exceeded
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          schema:
            type: string
            example: '60'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            limited:
              $ref: '#/components/examples/RateLimited'
    PublicApiNotConfigured:
      description: Public API v1 key authentication is not configured for this environment
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            notConfigured:
              $ref: '#/components/examples/PublicApiNotConfigured'
    InternalError:
      description: Unexpected server failure
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            internal:
              value:
                error:
                  code: INTERNAL_ERROR
                  message: Something went wrong. Please try again.
                  status: 500
                  requestId: partner-fit-001
                  retryable: true
  schemas:
    CandidateGetResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: object
          required: [candidate]
          properties:
            candidate:
              $ref: '#/components/schemas/PublicCandidate'
        meta:
          $ref: '#/components/schemas/CandidateGetMeta'
    CandidateGetMeta:
      type: object
      required: [apiVersion, projectionVersion, generatedAt, requestId]
      properties:
        apiVersion:
          type: string
          const: v1
        projectionVersion:
          type: integer
        generatedAt:
          type: string
          format: date-time
        requestId:
          type: string
    PublicCandidate:
      type: object
      required:
        - publicSlug
        - canonicalUrl
        - basics
        - location
        - links
        - featuredExperience
        - featuredClaims
        - publicState
      properties:
        publicSlug:
          type: string
        canonicalUrl:
          type: string
          format: uri
        basics:
          type: object
          required: [displayName, headline, summary]
          properties:
            displayName:
              type: [string, 'null']
            headline:
              type: [string, 'null']
            summary:
              type: [string, 'null']
        location:
          type: object
          required: [label, granularity]
          properties:
            label:
              type: [string, 'null']
            granularity:
              type: string
              enum: [city_region, region, hidden]
        links:
          type: array
          items:
            $ref: '#/components/schemas/PublicLink'
        featuredExperience:
          type: array
          items:
            $ref: '#/components/schemas/PublicExperience'
        featuredClaims:
          type: array
          items:
            $ref: '#/components/schemas/PublicClaim'
        publicState:
          type: object
          required: [visibility, publicFitEnabled, shareReadiness]
          properties:
            visibility:
              type: string
              const: public
            publicFitEnabled:
              type: boolean
            shareReadiness:
              type: string
              enum: [ready, needs_more]
    PublicLink:
      type: object
      required: [kind, label, url, hostname]
      properties:
        kind:
          type: string
          enum: [linkedin, github, portfolio]
        label:
          type: string
        url:
          type: string
          format: uri
        hostname:
          type: string
    PublicExperience:
      type: object
      required: [id, title, company, dateRangeLabel, scope, highlights, technologies]
      properties:
        id:
          type: string
        title:
          type: string
        company:
          type: string
        dateRangeLabel:
          type: [string, 'null']
        scope:
          type: [string, 'null']
        highlights:
          type: array
          items:
            $ref: '#/components/schemas/PublicHighlight'
        technologies:
          type: array
          items:
            type: string
    PublicHighlight:
      type: object
      required: [id, text, linkedClaimId]
      properties:
        id:
          type: string
        text:
          type: string
        linkedClaimId:
          type: [string, 'null']
    PublicClaim:
      type: object
      required: [id, text, category, kind, roleContext, trust]
      properties:
        id:
          type: string
        text:
          type: string
        category:
          type: string
          enum: [work_impact, professional_strength]
        kind:
          type: string
          enum: [fact, judgment]
        roleContext:
          type: object
          required: [roleId, roleLabel]
          properties:
            roleId:
              type: [string, 'null']
            roleLabel:
              type: [string, 'null']
        trust:
          type: object
          required: [status, supportLevel, explanation, fairnessNote, sourceLabels]
          properties:
            status:
              type: string
              enum: [verified, supported, self_reported, inferred]
            supportLevel:
              type: string
              enum: [strong, moderate, limited]
            explanation:
              type: string
            fairnessNote:
              type: [string, 'null']
            sourceLabels:
              type: array
              items:
                type: string
    CandidateFitRequest:
      type: object
      additionalProperties: false
      required: [jobDescription]
      properties:
        jobDescription:
          type: string
          minLength: 1
          maxLength: 32000
          description: >
            Plain-text job description. HonestFit trims the value server-side and rejects empty or oversized input.
    CandidateFitResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: object
          required: [fitAnalysis]
          properties:
            fitAnalysis:
              $ref: '#/components/schemas/FitAnalysis'
        meta:
          type: object
          required: [apiVersion, fitSchemaVersion, generatedAt, requestId]
          properties:
            apiVersion:
              type: string
              const: v1
            fitSchemaVersion:
              type: integer
            generatedAt:
              type: string
              format: date-time
            requestId:
              type: string
    FitAnalysis:
      type: object
      required:
        - publicSlug
        - summary
        - strongestVisibleMatch
        - strengths
        - lowerConfidence
        - missingEvidence
        - grounding
        - job
      properties:
        publicSlug:
          type: string
        summary:
          type: object
          required: [overall, evidenceBackedRead, missingEvidenceRead, inferredJudgment, recommendation]
          properties:
            overall:
              type: string
            evidenceBackedRead:
              type: string
            missingEvidenceRead:
              type: string
            inferredJudgment:
              type: string
            recommendation:
              type: string
        strongestVisibleMatch:
          anyOf:
            - $ref: '#/components/schemas/FitFinding'
            - type: 'null'
        strengths:
          type: array
          items:
            $ref: '#/components/schemas/FitFinding'
        lowerConfidence:
          type: array
          items:
            $ref: '#/components/schemas/FitFinding'
        missingEvidence:
          type: array
          items:
            $ref: '#/components/schemas/FitFinding'
        grounding:
          type: object
          required: [scope, fairnessNote, trustSemantics]
          properties:
            scope:
              type: string
              const: public_projection_only
            fairnessNote:
              type: string
            trustSemantics:
              type: string
              const: claim_level_resolved_labels_only
        job:
          type: object
          required: [inferredRole, requirementCount]
          properties:
            inferredRole:
              type: [string, 'null']
            requirementCount:
              type: integer
    FitFinding:
      type: object
      required:
        - id
        - label
        - importance
        - category
        - confidence
        - reasoningType
        - matchedKeywords
        - evidence
        - explanation
      properties:
        id:
          type: string
        label:
          type: string
        importance:
          type: string
          enum: [core, nice]
        category:
          type: string
          enum: [technical, domain, leadership, execution, seniority, location, other]
        confidence:
          type: string
          enum: [high, medium, low]
        reasoningType:
          type: string
          enum: [fact, inference, missing_evidence]
        matchedKeywords:
          type: array
          items:
            type: string
        evidence:
          type: array
          items:
            $ref: '#/components/schemas/FitEvidence'
        explanation:
          type: string
    FitEvidence:
      type: object
      required: [source, label, detail]
      properties:
        source:
          type: string
          enum: [headline, summary, role, skill, claim]
        label:
          type: string
        detail:
          type: string
    ErrorEnvelope:
      type: object
      description: Standard error envelope shared across the public API.
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, status, requestId, retryable]
          properties:
            code:
              type: string
              enum:
                - INVALID_REQUEST
                - PUBLIC_API_NOT_CONFIGURED
                - UNAUTHORIZED
                - FORBIDDEN
                - NOT_FOUND
                - PAYLOAD_TOO_LARGE
                - UNSUPPORTED_MEDIA_TYPE
                - RATE_LIMITED
                - INTERNAL_ERROR
            message:
              type: string
            status:
              type: integer
            requestId:
              type: string
            retryable:
              type: boolean
            details:
              type: object
              additionalProperties: true
  examples:
    CandidateGetSuccess:
      value:
        data:
          candidate:
            publicSlug: alex-rivera
            canonicalUrl: https://honestfit.ai/c/alex-rivera
            basics:
              displayName: Alex Rivera
              headline: Staff Frontend Engineer
              summary: Candidate-approved public summary.
            location:
              label: TX
              granularity: region
            links:
              - kind: linkedin
                label: LinkedIn
                url: https://www.linkedin.com/in/alex-rivera/?trk=private
                hostname: www.linkedin.com
              - kind: github
                label: GitHub
                url: https://github.com/alexrivera?tab=repositories
                hostname: github.com
              - kind: portfolio
                label: Portfolio
                url: https://alexrivera.dev/work?preview=true
                hostname: alexrivera.dev
            featuredExperience:
              - id: role-1
                title: Staff Frontend Engineer
                company: ExampleCo
                dateRangeLabel: Jan 2021 - Mar 2024
                scope: Led the design system and frontend platform.
                highlights:
                  - id: claim-1
                    text: Scaled a component platform used across multiple teams.
                    linkedClaimId: claim-1
                technologies: [React, TypeScript, Storybook]
            featuredClaims:
              - id: claim-1
                text: Scaled a component platform used across multiple teams.
                category: work_impact
                kind: fact
                roleContext:
                  roleId: role-1
                  roleLabel: Staff Frontend Engineer · ExampleCo
                trust:
                  status: supported
                  supportLevel: moderate
                  explanation: This claim is supported by candidate-approved source material visible through the public projection rules.
                  fairnessNote: null
                  sourceLabels: [Candidate confirmation, Uploaded source material]
              - id: claim-2
                text: Bridges design systems and product execution without handoff gaps.
                category: professional_strength
                kind: judgment
                roleContext:
                  roleId: null
                  roleLabel: null
                trust:
                  status: self_reported
                  supportLevel: limited
                  explanation: This reads as candidate-reported strength rather than direct public proof.
                  fairnessNote: Support may be thin because the strongest evidence can be private, historical, or proprietary.
                  sourceLabels: []
            publicState:
              visibility: public
              publicFitEnabled: true
              shareReadiness: ready
        meta:
          apiVersion: v1
          projectionVersion: 3
          generatedAt: '2026-04-20T18:30:00.000Z'
          requestId: partner-read-001
    CandidateFitSuccess:
      value:
        data:
          fitAnalysis:
            publicSlug: alex-rivera
            summary:
              overall: Strong public evidence for lead design systems and component libraries, with thinner public proof on own accessibility program direction.
              evidenceBackedRead: 'Facts: the public profile shows direct evidence of design systems leadership, component platform work, and relevant frontend stack usage.'
              missingEvidenceRead: 'Missing evidence: the public profile does not currently expose direct proof of accessibility program ownership.'
              inferredJudgment: 'Inference: there is meaningful role overlap, but some leadership expectations still rely on adjacent signals rather than explicit proof.'
              recommendation: Treat this as a strong public match for frontend systems work and ask follow-up questions where the public surface stays thin.
            strongestVisibleMatch:
              id: req-design-systems-leadership
              label: Lead design systems and component libraries
              importance: core
              category: technical
              confidence: high
              reasoningType: fact
              matchedKeywords: [design, systems, component]
              evidence:
                - source: claim
                  label: Staff Frontend Engineer · ExampleCo
                  detail: Scaled a component platform used across multiple teams.
                - source: role
                  label: Staff Frontend Engineer · ExampleCo
                  detail: Led the design system and frontend platform across product surfaces.
              explanation: This requirement is directly supported by explicit public role and claim evidence.
            strengths:
              - id: req-design-systems-leadership
                label: Lead design systems and component libraries
                importance: core
                category: technical
                confidence: high
                reasoningType: fact
                matchedKeywords: [design, systems, component]
                evidence:
                  - source: claim
                    label: Staff Frontend Engineer · ExampleCo
                    detail: Scaled a component platform used across multiple teams.
                  - source: role
                    label: Staff Frontend Engineer · ExampleCo
                    detail: Led the design system and frontend platform across product surfaces.
                explanation: This requirement is directly supported by explicit public role and claim evidence.
            lowerConfidence:
              - id: req-mentor-frontend-engineers
                label: Mentor frontend engineers
                importance: core
                category: leadership
                confidence: medium
                reasoningType: inference
                matchedKeywords: [mentor, frontend]
                evidence:
                  - source: summary
                    label: Public summary
                    detail: Frontend platform leader focused on systems and team enablement.
                explanation: The public profile shows adjacent leadership signals, but the match still relies partly on interpretation.
            missingEvidence:
              - id: req-accessibility-program-direction
                label: Own accessibility program direction
                importance: nice
                category: technical
                confidence: low
                reasoningType: missing_evidence
                matchedKeywords: []
                evidence: []
                explanation: The public profile does not currently surface direct evidence for this requirement.
            grounding:
              scope: public_projection_only
              fairnessNote: Missing public evidence does not mean the candidate lacks the underlying experience.
              trustSemantics: claim_level_resolved_labels_only
            job:
              inferredRole: Staff Frontend Engineer
              requirementCount: 6
        meta:
          apiVersion: v1
          fitSchemaVersion: 1
          generatedAt: '2026-04-20T18:31:00.000Z'
          requestId: partner-fit-001
    Unauthorized:
      value:
        error:
          code: UNAUTHORIZED
          message: Missing API key.
          status: 401
          requestId: partner-read-001
          retryable: false
    ReadScopeForbidden:
      value:
        error:
          code: FORBIDDEN
          message: 'API key does not have required scope: public_candidate.read.'
          status: 403
          requestId: partner-read-001
          retryable: false
    FitScopeForbidden:
      value:
        error:
          code: FORBIDDEN
          message: 'API key does not have required scope: public_candidate.fit.'
          status: 403
          requestId: partner-fit-001
          retryable: false
    FitDisabledForbidden:
      value:
        error:
          code: FORBIDDEN
          message: Public fit is disabled for this candidate.
          status: 403
          requestId: partner-fit-001
          retryable: false
    NotFound:
      value:
        error:
          code: NOT_FOUND
          message: Candidate not found.
          status: 404
          requestId: partner-read-001
          retryable: false
    PayloadTooLarge:
      value:
        error:
          code: PAYLOAD_TOO_LARGE
          message: jobDescription must be at most 32000 characters.
          status: 413
          requestId: partner-fit-001
          retryable: false
          details:
            fieldErrors:
              jobDescription:
                - Must be at most 32000 characters
    RateLimited:
      value:
        error:
          code: RATE_LIMITED
          message: Rate limit exceeded.
          status: 429
          requestId: partner-fit-001
          retryable: true
    PublicApiNotConfigured:
      value:
        error:
          code: PUBLIC_API_NOT_CONFIGURED
          message: Public API is not configured.
          status: 503
          requestId: partner-read-001
          retryable: false
