openapi: 3.1.0
info:
  title: jobpost.tech API
  version: 1.0.0
  description: |
    Read-only REST API for canonical Schema.org JobPosting data per site.

    **Demo sites**

    | Slug | Naam | Template |
    |------|------|----------|
    | `reinier-looij` | Reinier Looij Groenbeheer | Hovenier |
    | `gemeente-goeree-overflakkee` | Gemeente Goeree-Overflakkee | Gemeente |

    Try the examples below or open [Scalar docs](/docs) for an interactive reference.
  contact:
    name: jobpost.tech
    url: https://jobpost.tech
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: http://localhost:3014
    description: Local development
  - url: https://jobpost.tech
    description: Production

tags:
  - name: Templates
    description: Werken-bij template packages available for sites.
  - name: Sites
    description: Organization profile and werken-bij settings.
  - name: Jobs
    description: Live JobPosting records for a site.

paths:
  /api/v1/templates:
    get:
      tags: [Templates]
      summary: List templates
      operationId: listTemplates
      description: Returns all werken-bij templates registered in the instance (e.g. Hovenier, Gemeente).
      responses:
        '200':
          description: Template catalog
          content:
            application/json:
              schema:
                type: object
                required: [templates]
                properties:
                  templates:
                    type: array
                    items:
                      $ref: '#/components/schemas/TemplateCard'
              example:
                templates:
                  - id: hovenier
                    label: Hovenier
                    description: Groene sector vacaturepagina met hero, kaartgrid en gestructureerde vacaturesecties.
                    previewTenant: reinier-looij
                    previewImage: /storage/tenants/reinier-looij/hero.jpg
                    supportsPagination: false
                    supportsFilters: false
                  - id: gemeente
                    label: Gemeente
                    description: Overheidsvacaturepagina met intro, filters, paginering en lijstweergave.
                    previewTenant: gemeente-goeree-overflakkee
                    previewImage: /storage/tenants/gemeente-goeree-overflakkee/hero.jpg
                    supportsPagination: true
                    supportsFilters: true

  /api/v1/sites/{site}:
    get:
      tags: [Sites]
      summary: Get site
      operationId: getTenant
      description: Organization metadata used for hiringOrganization defaults and werken-bij branding.
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
      responses:
        '200':
          description: Tenant profile
          content:
            application/json:
              schema:
                type: object
                required: [tenant]
                properties:
                  tenant:
                    $ref: '#/components/schemas/Tenant'
              examples:
                reinierLooij:
                  summary: Reinier Looij Groenbeheer
                  value:
                    tenant:
                      id: 1
                      slug: reinier-looij
                      name: Reinier Looij Groenbeheer
                      website: https://reinierlooij.nl
                      logoUrl: /storage/tenants/reinier-looij/logo.png
                      phone: '0187 727 000'
                      email: info@reinierlooij.nl
                      streetAddress: Sommelsdijkse haven 3
                      postalCode: 3245MA
                      addressLocality: Sommelsdijk
                      addressRegion: Zuid-Holland
                      addressCountry: NL
                      themeId: hovenier
                      enablePagination: false
                      enableFilters: false
                      vacaturesUrl: http://localhost:3014/site/reinier-looij/vacatures
                gemeenteGo:
                  summary: Gemeente Goeree-Overflakkee
                  value:
                    tenant:
                      id: 2
                      slug: gemeente-goeree-overflakkee
                      name: Gemeente Goeree-Overflakkee
                      website: https://www.goeree-overflakkee.nl
                      logoUrl: /storage/tenants/gemeente-goeree-overflakkee/logo.svg
                      phone: '14 0187'
                      email: gemeente@goeree-overflakkee.nl
                      themeId: gemeente
                      enablePagination: true
                      enableFilters: true
                      vacaturesUrl: http://localhost:3014/site/gemeente-goeree-overflakkee/vacatures
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/sites/{site}/jobs:
    get:
      tags: [Jobs]
      summary: List live jobs
      operationId: listJobs
      description: |
        Returns live vacancies for a site. When the site has filters enabled, query parameters apply
        server-side before pagination.
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - name: page
          in: query
          description: Page number (1-based). Only used when site pagination is enabled.
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: per_page
          in: query
          description: Results per page (max 24). Only used when site pagination is enabled.
          schema:
            type: integer
            minimum: 1
            maximum: 24
            default: 6
          example: 6
        - name: q
          in: query
          description: Search in title and description (filters must be enabled).
          schema:
            type: string
          example: hovenier
        - name: location
          in: query
          schema:
            type: string
          example: Middelharnis
        - name: employment
          in: query
          schema:
            type: string
            enum: [FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, PER_DIEM, OTHER]
          example: FULL_TIME
        - name: education
          in: query
          schema:
            type: string
          example: Hbo
        - name: salary_min
          in: query
          description: Minimum monthly salary (EUR). Job range must overlap.
          schema:
            type: number
          example: 3000
        - name: salary_max
          in: query
          description: Maximum monthly salary (EUR). Job range must overlap.
          schema:
            type: number
          example: 5000
      responses:
        '200':
          description: Job list with pagination and filter metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobListResponse'
              examples:
                reinierLooij:
                  summary: Reinier Looij — first page
                  value:
                    tenant:
                      slug: reinier-looij
                      name: Reinier Looij Groenbeheer
                    jobs:
                      - identifier: verkoop-medewerker
                        slug: verkoop-medewerker
                        title: Verkoop medewerker
                        status: live
                        employmentType: PART_TIME
                        url: http://localhost:3014/site/reinier-looij/vacatures/verkoop-medewerker
                    meta:
                      total: 12
                      page: 1
                      perPage: 12
                      totalPages: 1
                      filtersEnabled: false
                      paginationEnabled: false
                gemeenteGoFiltered:
                  summary: Gemeente GO — salary filter
                  value:
                    tenant:
                      slug: gemeente-goeree-overflakkee
                      name: Gemeente Goeree-Overflakkee
                    jobs: []
                    meta:
                      total: 0
                      page: 1
                      perPage: 6
                      totalPages: 1
                      salaryMin: '3500'
                      salaryMax: '5000'
                      hasActiveFilters: true
                      filtersEnabled: true
                      paginationEnabled: true
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/sites/{site}/jobs/{jobSlug}:
    get:
      tags: [Jobs]
      summary: Get job
      operationId: getJob
      description: Single live vacancy with parsed content sections, facts, and Schema.org JSON-LD.
      parameters:
        - $ref: '#/components/parameters/TenantSlug'
        - $ref: '#/components/parameters/JobSlug'
      responses:
        '200':
          description: Job detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobDetailResponse'
              examples:
                reinierLooij:
                  summary: Reinier Looij — verkoop-medewerker
                  value:
                    tenant:
                      slug: reinier-looij
                      name: Reinier Looij Groenbeheer
                    job:
                      identifier: verkoop-medewerker
                      slug: verkoop-medewerker
                      title: Verkoop medewerker
                      status: live
                      url: http://localhost:3014/site/reinier-looij/vacatures/verkoop-medewerker
                      jsonLd:
                        '@context': https://schema.org
                        '@type': JobPosting
                        title: Verkoop medewerker
                gemeenteGo:
                  summary: Gemeente GO — projectondersteuner-energie
                  value:
                    tenant:
                      slug: gemeente-goeree-overflakkee
                      name: Gemeente Goeree-Overflakkee
                    job:
                      identifier: projectondersteuner-energie
                      slug: projectondersteuner-energie
                      title: Projectondersteuner Energie
                      status: live
                      baseSalary:
                        currency: EUR
                        minValue: 2875
                        maxValue: 4556
                        unitText: MONTH
        '404':
          description: Tenant or job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                tenantNotFound:
                  value:
                    error: tenant_not_found
                jobNotFound:
                  value:
                    error: job_not_found

components:
  parameters:
    TenantSlug:
      name: site
      in: path
      required: true
      description: Site slug (URL segment under `/site/{site}/vacatures`).
      schema:
        type: string
      examples:
        reinierLooij:
          summary: Reinier Looij Groenbeheer
          value: reinier-looij
        gemeenteGo:
          summary: Gemeente Goeree-Overflakkee
          value: gemeente-goeree-overflakkee

    JobSlug:
      name: jobSlug
      in: path
      required: true
      description: Vacancy slug (URL segment under `/vacatures/{jobSlug}`).
      schema:
        type: string
      examples:
        reinierVerkoop:
          summary: Reinier Looij — Verkoop medewerker
          value: verkoop-medewerker
        gemeenteProject:
          summary: Gemeente GO — Projectondersteuner Energie
          value: projectondersteuner-energie

  responses:
    NotFound:
      description: Site not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: tenant_not_found

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum: [tenant_not_found, job_not_found]

    TemplateCard:
      type: object
      required: [id, label, description, previewTenant, supportsPagination, supportsFilters]
      properties:
        id:
          type: string
          example: hovenier
        label:
          type: string
        description:
          type: string
        previewTenant:
          type: string
          example: reinier-looij
        previewImage:
          type: string
          nullable: true
        supportsPagination:
          type: boolean
        supportsFilters:
          type: boolean

    Tenant:
      type: object
      required: [id, slug, name, themeId, enablePagination, enableFilters, vacaturesUrl]
      properties:
        id:
          type: integer
        slug:
          type: string
        name:
          type: string
        website:
          type: string
          nullable: true
          format: uri
        logoUrl:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
          format: email
        streetAddress:
          type: string
          nullable: true
        postalCode:
          type: string
          nullable: true
        addressLocality:
          type: string
          nullable: true
        addressRegion:
          type: string
          nullable: true
        addressCountry:
          type: string
          default: NL
        themeId:
          type: string
          example: gemeente
        enablePagination:
          type: boolean
        enableFilters:
          type: boolean
        vacaturesUrl:
          type: string
          format: uri

    MonetaryAmount:
      type: object
      properties:
        currency:
          type: string
          example: EUR
        value:
          type: number
          nullable: true
        minValue:
          type: number
          nullable: true
        maxValue:
          type: number
          nullable: true
        unitText:
          type: string
          enum: [HOUR, DAY, WEEK, MONTH, YEAR]
          example: MONTH

    Job:
      type: object
      required: [identifier, slug, title, description, status, url, hiringOrganization, jobLocation, sections, facts]
      properties:
        identifier:
          type: string
        slug:
          type: string
        title:
          type: string
        description:
          type: string
          description: HTML job description
        status:
          type: string
          enum: [draft, live, closed]
        datePosted:
          type: string
          format: date
          nullable: true
        validThrough:
          type: string
          format: date
          nullable: true
        employmentType:
          type: string
          nullable: true
        employmentLabel:
          type: string
          nullable: true
        directApply:
          type: boolean
        applyUrl:
          type: string
          nullable: true
        sourceUrl:
          type: string
          nullable: true
        workHours:
          type: string
          nullable: true
        educationLevel:
          type: string
          nullable: true
        applicationDeadline:
          type: string
          format: date
          nullable: true
        heroImageUrl:
          type: string
          nullable: true
        url:
          type: string
          format: uri
        hiringOrganization:
          type: object
          properties:
            name:
              type: string
            sameAs:
              type: string
              nullable: true
            logo:
              type: string
              nullable: true
        jobLocation:
          type: object
          properties:
            streetAddress:
              type: string
              nullable: true
            postalCode:
              type: string
              nullable: true
            addressLocality:
              type: string
              nullable: true
            addressRegion:
              type: string
              nullable: true
            addressCountry:
              type: string
        baseSalary:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/MonetaryAmount'
        sections:
          type: object
          properties:
            description:
              type: string
              nullable: true
            qualifications:
              type: string
              nullable: true
            skills:
              type: string
              nullable: true
            benefits:
              type: string
              nullable: true
        facts:
          type: array
          items:
            type: object
            properties:
              label:
                type: string
              value:
                type: string
        jsonLd:
          type: object
          description: Schema.org JobPosting (detail endpoint only)
          additionalProperties: true

    JobListMeta:
      type: object
      required: [total, page, perPage, totalPages, hasActiveFilters, filtersEnabled, paginationEnabled, filterOptions]
      properties:
        total:
          type: integer
        page:
          type: integer
        perPage:
          type: integer
        totalPages:
          type: integer
        search:
          type: string
        education:
          type: string
        employment:
          type: string
        location:
          type: string
        salaryMin:
          type: string
        salaryMax:
          type: string
        hasActiveFilters:
          type: boolean
        filtersEnabled:
          type: boolean
        paginationEnabled:
          type: boolean
        filterOptions:
          type: object
          properties:
            educationLevels:
              type: array
              items:
                type: string
            employmentTypes:
              type: array
              items:
                type: string
            localities:
              type: array
              items:
                type: string
            salaryRange:
              nullable: true
              type: object
              properties:
                min:
                  type: integer
                max:
                  type: integer

    JobListResponse:
      type: object
      required: [tenant, jobs, meta]
      properties:
        tenant:
          $ref: '#/components/schemas/Tenant'
        jobs:
          type: array
          items:
            $ref: '#/components/schemas/Job'
        meta:
          $ref: '#/components/schemas/JobListMeta'

    JobDetailResponse:
      type: object
      required: [tenant, job]
      properties:
        tenant:
          $ref: '#/components/schemas/Tenant'
        job:
          $ref: '#/components/schemas/Job'
