openapi: 3.1.0
info:
  title: Aurélia.jobs V2 Public API
  version: 0.3.0
  summary: Surface API externe sûre pour Aurélia.jobs V2.
  license:
    name: Proprietary
    url: https://aurelia.jobs/terms
  description: |
    Cette spécification OpenAPI documente uniquement les endpoints externes
    disponibles et sûrs à exposer : exports d'offres, intégration Jobposting,
    candidature publique, préqualification candidat et portail candidat.

    Les routes admin, agent, workers, OAuth techniques et webhooks providers
    sont volontairement exclues de cette spécification.
servers:
  - url: https://aurelia.jobs
    description: Production
tags:
  - name: Jobs
    description: Exports d'offres publiées.
  - name: Jobposting
    description: Intégration partenaire Jobposting.
  - name: Applications
    description: Candidatures publiques.
  - name: Prequalification
    description: Parcours de préqualification candidat.
  - name: Candidate portal
    description: Portail candidat et demandes RGPD.
  - name: Service
    description: Santé et découverte du service.
  - name: Public careers
    description: Offres publiées des pages carrière publiques.
paths:
  /api/jobs.json:
    get:
      tags: [Jobs]
      summary: Lister les offres publiées en JSON
      description: |
        Retourne les recrutements actifs et publiés du workspace identifié par
        la clé workspace `X-API-Key`.
      operationId: listPublishedJobsJson
      x-aurelia-status: implemented
      security:
        - WorkspaceApiKey: []
      responses:
        "200":
          description: Offres publiées du workspace.
          headers:
            Cache-Control:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublishedJobsResponse"
        "401":
          $ref: "#/components/responses/UnauthorizedJson"
        "404":
          $ref: "#/components/responses/NotFoundJson"
        "500":
          $ref: "#/components/responses/ServerErrorJson"
      x-required-scopes:
        - jobs:read
  /api/jobs.xml:
    get:
      tags: [Jobs, Jobposting]
      summary: Lister les offres publiées en XML
      description: |
        Flux XML historique pour Jobposting/ATS. Cette route utilise le header
        `X-API-Key`, mais avec la clé d'intégration `JOBPOSTING_API_KEY`, pas
        une clé `workspace_api_keys`.
      operationId: listPublishedJobsXml
      x-aurelia-status: implemented
      security:
        - JobpostingApiKey: []
      responses:
        "200":
          description: Flux XML des offres.
          headers:
            Cache-Control:
              schema:
                type: string
            X-Jobs-Count:
              schema:
                type: string
          content:
            application/xml:
              schema:
                type: string
        "401":
          description: Clé absente ou invalide.
          content:
            text/plain:
              schema:
                type: string
                example: Unauthorized
        "500":
          description: Erreur serveur.
          content:
            text/plain:
              schema:
                type: string
                example: Internal Server Error
      x-required-scopes:
        - jobs:feed
  /api/candidatures:
    post:
      tags: [Applications, Jobposting]
      summary: Recevoir une candidature Jobposting
      description: |
        Crée ou réutilise un candidat, attache éventuellement un CV encodé en
        base64, crée une candidature et déclenche les traitements non bloquants
        associés. Cette route est une intégration partenaire spécifique.
      operationId: createJobpostingApplication
      x-aurelia-status: implemented
      security:
        - JobpostingApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JobpostingApplicationRequest"
      responses:
        "200":
          description: Candidature créée ou déjà existante.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobpostingApplicationResponse"
        "400":
          $ref: "#/components/responses/BadRequestJson"
        "401":
          $ref: "#/components/responses/UnauthorizedJson"
        "404":
          $ref: "#/components/responses/NotFoundJson"
        "500":
          $ref: "#/components/responses/ServerErrorJson"
      x-required-scopes:
        - applications:write
  /api/v2/application/submit:
    post:
      tags: [Applications]
      summary: Soumettre une candidature publique V2
      description: |
        Endpoint appelé par le formulaire carrière public. Il crée ou réutilise
        le candidat, crée la candidature, audite le consentement RGPD et accepte
        un CV optionnel.
      operationId: submitPublicApplication
      x-aurelia-status: implemented
      security: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/PublicApplicationForm"
      responses:
        "200":
          description: Candidature créée.
          content:
            application/json:
              schema:
                type: object
                required: [ok, applicationId]
                properties:
                  ok:
                    type: boolean
                    const: true
                  applicationId:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/PublicBadRequest"
        "404":
          $ref: "#/components/responses/PublicNotFound"
        "500":
          $ref: "#/components/responses/PublicServerError"
      x-required-scopes:
        - applications:write
  /api/v2/prequalif/{sessionId}/consent:
    post:
      tags: [Prequalification]
      summary: Enregistrer le consentement de préqualification
      description: |
        `sessionId` correspond actuellement à `candidate_applications.id`.
        La décision `ai` autorise la conversation IA ; `human` demande une
        revue humaine seule et révoque le consentement IA.
      operationId: recordPrequalificationConsent
      x-aurelia-status: implemented
      security: []
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision:
                  type: string
                  enum: [ai, human]
      responses:
        "200":
          description: Consentement enregistré.
          content:
            application/json:
              schema:
                type: object
                required: [ok, aiConsent]
                properties:
                  ok:
                    type: boolean
                  aiConsent:
                    type: boolean
        "400":
          $ref: "#/components/responses/PublicBadRequest"
        "404":
          $ref: "#/components/responses/SessionNotFound"
        "500":
          $ref: "#/components/responses/PublicServerError"
      x-required-scopes:
        - prequalification:write
  /api/v2/prequalif/{sessionId}/stream:
    post:
      tags: [Prequalification]
      summary: Streamer la conversation de préqualification
      description: |
        Lance ou poursuit la conversation IA de préqualification. Le consentement
        IA doit avoir été enregistré au préalable. Un rate limit en mémoire
        limite actuellement chaque session à 12 requêtes par minute.
      operationId: streamPrequalificationConversation
      x-aurelia-status: implemented
      security: []
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PrequalificationStreamRequest"
      responses:
        "200":
          description: Stream de réponse UI/IA.
          headers:
            X-Aurelia-Prequalif-Mode:
              schema:
                type: string
              description: Présent en fallback local.
          content:
            text/event-stream:
              schema:
                type: string
            application/octet-stream:
              schema:
                type: string
        "400":
          $ref: "#/components/responses/PublicBadRequest"
        "403":
          description: Consentement IA manquant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/SessionNotFound"
        "429":
          description: Rate limit atteint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Provider IA non configuré.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
      x-required-scopes:
        - prequalification:write
  /api/v2/prequalif/{sessionId}/save:
    post:
      tags: [Prequalification]
      summary: Sauvegarder l'état de préqualification
      description: Fusionne le JSON reçu dans `candidate_applications.prequalif_session`.
      operationId: savePrequalificationState
      x-aurelia-status: implemented
      security: []
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: État sauvegardé.
          content:
            application/json:
              schema:
                type: object
                required: [ok, persisted]
                properties:
                  ok:
                    type: boolean
                  persisted:
                    type: boolean
        "400":
          $ref: "#/components/responses/PublicBadRequest"
        "404":
          $ref: "#/components/responses/SessionNotFound"
        "500":
          $ref: "#/components/responses/PublicServerError"
      x-required-scopes:
        - prequalification:write
  /api/v2/portal/{token}/export:
    get:
      tags: [Candidate portal]
      summary: Exporter les données candidat
      description: |
        Export JSON RGPD candidat. Le token est temporairement
        `candidate_applications.id`; la cible produit est un token opaque signé.
      operationId: exportCandidateData
      x-aurelia-status: implemented-token-temporary
      security: []
      parameters:
        - $ref: "#/components/parameters/PortalToken"
      responses:
        "200":
          description: Export candidat.
          headers:
            content-disposition:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CandidateExport"
        "404":
          $ref: "#/components/responses/PublicNotFound"
        "500":
          $ref: "#/components/responses/PublicServerError"
      x-required-scopes:
        - candidate-portal:read
  /api/v2/portal/{token}/delete:
    post:
      tags: [Candidate portal]
      summary: Demander la suppression des données candidat
      description: |
        Journalise une demande de suppression RGPD. La suppression effective
        reste traitée par un administrateur workspace dans le délai légal.
      operationId: requestCandidateDeletion
      x-aurelia-status: implemented-token-temporary
      security: []
      parameters:
        - $ref: "#/components/parameters/PortalToken"
      responses:
        "200":
          description: Demande enregistrée.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        "404":
          $ref: "#/components/responses/PublicNotFound"
        "500":
          $ref: "#/components/responses/PublicServerError"
      x-required-scopes:
        - candidate-portal:delete
  /api/health:
    get:
      tags:
        - Service
      summary: Vérifier la santé du service
      description: Sonde publique, sans authentification. Retourne l'état du serveur applicatif et l'horodatage de la réponse. Elle sert au healthcheck du conteneur ; un agent peut s'en servir pour vérifier que l'API répond avant d'enchaîner.
      operationId: getServiceHealth
      x-aurelia-status: implemented
      x-required-scopes:
        - public:read
      security: []
      responses:
        "200":
          description: Le service répond.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceHealth"
        "500":
          $ref: "#/components/responses/ServerErrorJson"
  /api/public/v1/service-catalog:
    get:
      tags:
        - Service
      summary: Lister les ressources lisibles par une machine
      description: "Catalogue de découverte, sans authentification : fichiers machine du domaine (openapi.json, llms.txt, agent.txt, mcp.json, métadonnée RFC 9728), opérations publiques, portées déclarées et coordonnées de l'éditeur. C'est le point d'entrée recommandé pour un agent qui découvre Aurélia.jobs."
      operationId: getServiceCatalog
      x-aurelia-status: implemented
      x-required-scopes:
        - public:read
      security: []
      responses:
        "200":
          description: Catalogue de service.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceCatalog"
        "500":
          $ref: "#/components/responses/ServerErrorJson"
  /api/public/v1/careers/{workspaceSlug}/jobs:
    get:
      tags:
        - Public careers
      summary: Lister les offres d'une page carrière publique
      description: "Retourne, sans authentification, exactement les offres déjà visibles sur la page carrière publique `/careers/{workspaceSlug}` : les recrutements actifs dont la diffusion est publiée. Aucune donnée candidat n'est exposée. Un espace de travail inconnu répond 404."
      operationId: getPublicCareersJobs
      x-aurelia-status: implemented
      x-required-scopes:
        - public:read
      security: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceSlug"
      responses:
        "200":
          description: Offres publiées de l'espace de travail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicCareersJobsResponse"
        "404":
          $ref: "#/components/responses/NotFoundJson"
        "500":
          $ref: "#/components/responses/ServerErrorJson"
components:
  securitySchemes:
    WorkspaceApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Clé workspace `ak_...` validée via `workspace_api_keys`.
    JobpostingApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Clé d'intégration Jobposting issue de `JOBPOSTING_API_KEY`.
  parameters:
    SessionId:
      name: sessionId
      in: path
      required: true
      description: Identifiant de session, actuellement `candidate_applications.id`.
      schema:
        type: string
        format: uuid
    PortalToken:
      name: token
      in: path
      required: true
      description: Token portail candidat. Temporairement un identifiant de candidature.
      schema:
        type: string
    WorkspaceSlug:
      name: workspaceSlug
      in: path
      required: true
      description: Identifiant d'URL de l'espace de travail, tel qu'il apparaît dans /careers/{workspaceSlug}.
      schema:
        type: string
        minLength: 1
  responses:
    BadRequestJson:
      description: Requête invalide.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    UnauthorizedJson:
      description: Authentification absente ou invalide.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFoundJson:
      description: Ressource introuvable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ServerErrorJson:
      description: Erreur serveur.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    PublicBadRequest:
      description: Requête publique invalide.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicErrorResponse"
    PublicNotFound:
      description: Ressource publique introuvable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicErrorResponse"
    PublicServerError:
      description: Erreur serveur.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicErrorResponse"
    SessionNotFound:
      description: Session de préqualification introuvable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicErrorResponse"
  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Message lisible par un humain.
        code:
          type: string
          description: Code d'erreur stable, sur lequel un agent peut brancher sa logique.
          enum:
            - bad_request
            - unauthorized
            - forbidden
            - not_found
            - method_not_allowed
            - not_acceptable
            - rate_limited
            - internal_error
        message:
          type: string
          description: Identique à `error`.
        hint:
          type: string
          description: Ce qu'il faut corriger pour que l'appel passe.
        documentation_url:
          type: string
          format: uri
        status:
          type: integer
      additionalProperties: true
      required:
        - error
        - code
        - message
    PublicErrorResponse:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
      additionalProperties: true
    PublishedJobsResponse:
      type: object
      required: [workspace, jobs, count, generatedAt]
      properties:
        workspace:
          type: object
          required: [name, slug]
          properties:
            name:
              type: string
            logo:
              type:
                - string
                - "null"
            slug:
              type: string
        jobs:
          type: array
          items:
            $ref: "#/components/schemas/PublishedJob"
        count:
          type: integer
        generatedAt:
          type: string
          format: date-time
    PublishedJob:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        location:
          type: object
          properties:
            display:
              type:
                - string
                - "null"
            city:
              type:
                - string
                - "null"
            postalCode:
              type:
                - string
                - "null"
            region:
              type:
                - string
                - "null"
            country:
              type:
                - string
                - "null"
        contract:
          type: object
          properties:
            type:
              type:
                - string
                - "null"
            durationValue:
              type:
                - integer
                - "null"
            durationUnit:
              type:
                - string
                - "null"
        salary:
          type:
            - object
            - "null"
          properties:
            min:
              type:
                - number
                - "null"
            max:
              type:
                - number
                - "null"
        remoteWorkDays:
          type:
            - integer
            - "null"
        qualifications:
          type: object
          additionalProperties: true
        company:
          type: object
          properties:
            name:
              type:
                - string
                - "null"
            logo:
              type:
                - string
                - "null"
        descriptions:
          type: object
          properties:
            company:
              type:
                - string
                - "null"
            position:
              type:
                - string
                - "null"
            profile:
              type:
                - string
                - "null"
        benefits:
          type: array
          items:
            type: string
        publishedAt:
          type:
            - string
            - "null"
          format: date-time
        applyUrl:
          type: string
          format: uri
    JobpostingApplicationRequest:
      type: object
      required: [id, job, applicant]
      properties:
        id:
          type: string
          description: Identifiant ou hash unique de la candidature côté partenaire.
        job:
          type: object
          required: [jobId, source]
          properties:
            jobId:
              type: string
              format: uuid
              description: Identifiant du recrutement Aurélia.
            source:
              type: string
              description: Code source partenaire.
            broadcastId:
              type: string
        applicant:
          type: object
          required: [lastName, email]
          properties:
            firstName:
              type: string
            lastName:
              type: string
            email:
              type: string
              format: email
            phoneNumber:
              type: string
            resume:
              type: object
              properties:
                file:
                  type: object
                  required: [fileName, contentType, data]
                  properties:
                    fileName:
                      type: string
                    contentType:
                      type: string
                    data:
                      type: string
                      description: Contenu du fichier encodé en base64.
        verified:
          type: boolean
    JobpostingApplicationResponse:
      type: object
      required: [success, message, candidateId, applicationId]
      properties:
        success:
          type: boolean
        message:
          type: string
        candidateId:
          type: string
          format: uuid
        applicationId:
          type: string
          format: uuid
    PublicApplicationForm:
      type: object
      required: [workspaceSlug, jobId, firstName, lastName, email, consent]
      properties:
        workspaceSlug:
          type: string
        jobId:
          type: string
          format: uuid
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        why:
          type: string
          description: Message de motivation.
        consent:
          type: string
          enum: ["1"]
        cv:
          type: string
          format: binary
          description: Fichier optionnel PDF, DOC, DOCX, JPG ou PNG.
    PrequalificationStreamRequest:
      type: object
      required: [messages]
      properties:
        messages:
          type: array
          items:
            type: object
            additionalProperties: true
        workspaceName:
          type: string
        jobTitle:
          type: string
        candidateFirstName:
          type: string
    CandidateExport:
      type: object
      properties:
        exportedAt:
          type: string
          format: date-time
        rgpd:
          type: object
          properties:
            legalBasis:
              type: string
            retentionMonths:
              type: integer
        candidate:
          type: object
          properties:
            firstName:
              type:
                - string
                - "null"
            lastName:
              type:
                - string
                - "null"
            email:
              type:
                - string
                - "null"
            phone:
              type:
                - string
                - "null"
            cvFilePath:
              type:
                - string
                - "null"
            createdAt:
              type:
                - string
                - "null"
              format: date-time
        applications:
          type: array
          items:
            type: object
            additionalProperties: true
        consents:
          type: array
          items:
            type: object
            additionalProperties: true
    ServiceHealth:
      type: object
      required:
        - status
        - ok
        - timestamp
      properties:
        status:
          type: string
          const: ok
        ok:
          type: boolean
        startedAt:
          type: string
          format: date-time
        timestamp:
          type: string
          format: date-time
      additionalProperties: true
    ServiceCatalog:
      type: object
      required:
        - name
        - description
        - documents
        - endpoints
        - scopes
        - publisher
      properties:
        name:
          type: string
        description:
          type: string
        whenToUse:
          type: array
          description: Cas d'usage pour lesquels un agent a intérêt à appeler Aurélia.jobs.
          items:
            type: string
        documents:
          type: array
          description: Fichiers lisibles par une machine publiés sur le domaine.
          items:
            type: object
            required:
              - name
              - url
              - mediaType
            properties:
              name:
                type: string
              url:
                type: string
                format: uri
              mediaType:
                type: string
              description:
                type: string
        endpoints:
          type: array
          items:
            type: object
            required:
              - operationId
              - method
              - path
              - authentication
              - scopes
            properties:
              operationId:
                type: string
              method:
                type: string
              path:
                type: string
              summary:
                type: string
              authentication:
                type: string
              scopes:
                type: array
                items:
                  type: string
        scopes:
          type: array
          items:
            type: object
            required:
              - name
              - description
            properties:
              name:
                type: string
              description:
                type: string
              carriedBy:
                type: string
        publisher:
          type: object
          required:
            - legalName
            - contactEmail
          properties:
            legalName:
              type: string
            brandName:
              type: string
            contactEmail:
              type: string
              format: email
            siret:
              type: string
            vatId:
              type: string
            address:
              type: string
    PublicCareersJobsResponse:
      type: object
      required:
        - workspace
        - jobs
        - count
        - generatedAt
      properties:
        workspace:
          type: object
          required:
            - name
            - slug
          properties:
            name:
              type: string
            slug:
              type: string
            logo:
              type:
                - string
                - "null"
            careersUrl:
              type: string
              format: uri
        count:
          type: integer
        generatedAt:
          type: string
          format: date-time
        jobs:
          type: array
          items:
            type: object
            required:
              - id
              - title
              - url
            properties:
              id:
                type: string
                format: uuid
              title:
                type: string
              url:
                type: string
                format: uri
              location:
                type:
                  - string
                  - "null"
              company:
                type:
                  - string
                  - "null"
                description: Nom de l'entreprise qui recrute, ou null si le recrutement est publié de façon anonyme.
              contractType:
                type:
                  - string
                  - "null"
              experienceLevel:
                type:
                  - string
                  - "null"
              educationLevel:
                type:
                  - string
                  - "null"
              remoteWorkDays:
                type:
                  - integer
                  - "null"
              salaryMin:
                type:
                  - number
                  - "null"
                description: Renseigné seulement si le recruteur affiche la rémunération.
              salaryMax:
                type:
                  - number
                  - "null"
                description: Renseigné seulement si le recruteur affiche la rémunération.
              publishedAt:
                type:
                  - string
                  - "null"
                format: date-time
