openapi: 3.0.3
info:
  title: Rework Public API
  version: 0.4.2
  description: |
    Job board API for rework.by. Vacancy fields map to JobSearch CPT `job`
    (taxonomies `sector` / `jobtype` / `skill`, meta `experience`, `jobsearch_field_location_*`,
    salary, application type, contact phones, package expiry).

    **Base URL:** `/api/v2`  
    **Authorize:** `/oauth/authorize` · **Docs:** `/developers/api/`  
    **Cabinet:** Dashboard → «Настройки API»

    **Locations:** JobSearch tables `jobsearch_countries` / `jobsearch_states` / `jobsearch_cities`.
    API returns **Belarus only** (`country_id=221`, code `BY`). Cities on write must exist in `/areas`.

    **Experience ids:** `fresh`, `1-3-years`, `3-6-years`, `6-years` (HH aliases accepted).  
    **Employment:** taxonomy `jobtype` (endpoint name `employment`).  
    **Salary types:** `type_1`…`type_5`, `negotiable` (по договорённости).  
    **Application types:** `internal`, `external` (+ url), `with_email` (+ email), `none`.

    **Candidates:** only via **negotiations** (applications) with PDN consent — no `/resumes` search.

    ATS: Huntflow `/api/ats/huntflow/v0.1`, e-staff
    `POST /api/ats/estaff/v2.6/RecruitService`.
servers:
  - url: https://rework.by/api/v2
    description: Production
  - url: https://dev.rework.by/api/v2
    description: Development
tags:
  - name: Vacancies
  - name: Negotiations
  - name: Dictionaries
  - name: Employers
  - name: Me
  - name: Webhooks
  - name: OAuth
paths:
  /vacancies:
    get:
      tags: [Vacancies]
      summary: Search published vacancies
      parameters:
        - { in: query, name: text, schema: { type: string } }
        - { in: query, name: page, schema: { type: integer, default: 0 } }
        - { in: query, name: per_page, schema: { type: integer, default: 20 } }
        - { in: query, name: employer_id, schema: { type: integer } }
        - { in: query, name: sector_id, schema: { type: integer } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VacancyList' }
    post:
      tags: [Vacancies]
      summary: Create and publish vacancy (consumes package slot)
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VacancyWrite' }
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vacancy' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '402':
          description: No package slots
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /vacancies/{id}:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    get:
      tags: [Vacancies]
      summary: Vacancy card (full body includes application, skills, phones)
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vacancy' }
        '404': { $ref: '#/components/responses/Error' }
    put:
      tags: [Vacancies]
      summary: Update own vacancy
      security: [{ bearerAuth: [] }]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VacancyWrite' }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vacancy' }
    patch:
      tags: [Vacancies]
      summary: Partial update own vacancy
      security: [{ bearerAuth: [] }]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VacancyWrite' }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Vacancy' }
    delete:
      tags: [Vacancies]
      summary: Archive own vacancy
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: OK }
  /vacancies/{job_id}/negotiations:
    get:
      tags: [Negotiations]
      summary: List applications (scope negotiations + PDN consent)
      description: Candidate PII and CV — only through negotiations, not a separate resume API.
      security: [{ bearerAuth: [] }]
      parameters:
        - { in: path, name: job_id, required: true, schema: { type: integer } }
        - { in: query, name: page, schema: { type: integer } }
        - { in: query, name: per_page, schema: { type: integer } }
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Error' }
  /vacancies/{job_id}/negotiations/{id}:
    parameters:
      - { in: path, name: job_id, required: true, schema: { type: integer } }
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [Negotiations]
      summary: Single negotiation
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: OK }
    put:
      tags: [Negotiations]
      summary: Update negotiation status/comment
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: OK }
    patch:
      tags: [Negotiations]
      summary: Patch negotiation
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: OK }
  /employers/{id}:
    get:
      tags: [Employers]
      summary: Public employer profile
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { description: OK }
  /employers/me/vacancies:
    get:
      tags: [Vacancies]
      summary: List vacancies owned by the token employer
      description: Same list shape as `GET /vacancies`, filtered to `jobsearch_field_job_posted_by` from the access token.
      security: [{ bearerAuth: [] }]
      parameters:
        - { in: query, name: page, schema: { type: integer, default: 0 } }
        - { in: query, name: per_page, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VacancyList' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
  /areas:
    get:
      tags: [Dictionaries]
      summary: Cities in Belarus (flat list)
      description: |
        Always filtered to Belarus (`country_id=221`). Use `city_id` from response when creating vacancies.
        Without `page`/`per_page` returns all cities (up to 5000). With pagination returns `{items,found,page,pages,per_page}`.
      parameters:
        - { in: query, name: region_id, schema: { type: integer }, description: Filter by oblast/state id }
        - { in: query, name: page, schema: { type: integer, default: 0 } }
        - { in: query, name: per_page, schema: { type: integer, default: 100, maximum: 5000 } }
      responses:
        '200':
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    properties:
                      items:
                        type: array
                        items: { $ref: '#/components/schemas/AreaItem' }
                      found: { type: integer }
                      page: { type: integer }
                      pages: { type: integer }
                      per_page: { type: integer }
                  - type: object
                    properties:
                      items:
                        type: array
                        items: { $ref: '#/components/schemas/AreaItem' }
  /dictionaries/locations:
    get:
      tags: [Dictionaries]
      summary: Belarus location tree (country → region → city)
      parameters:
        - { in: query, name: depth, schema: { type: integer, enum: [1, 2, 3] }, description: "1=country, 2=+regions, 3=+cities" }
        - { in: query, name: region_id, schema: { type: integer }, description: Return cities for one region }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/LocationCountry' }
  /dictionaries/sectors:
    get:
      tags: [Dictionaries]
      summary: sector taxonomy tree
      responses:
        '200': { description: OK }
  /dictionaries/employment:
    get:
      tags: [Dictionaries]
      summary: jobtype terms (API name employment)
      responses:
        '200': { description: OK }
  /dictionaries/experience:
    get:
      tags: [Dictionaries]
      summary: JobSearch experience ids + HH aliases
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/ExperienceItem' }
  /dictionaries/skills:
    get:
      tags: [Dictionaries]
      summary: skill taxonomy (flat, sorted by name)
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/DictionaryItem' }
  /dictionaries/salary-types:
    get:
      tags: [Dictionaries]
      summary: Salary period types (type_1…type_5, negotiable)
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/SalaryTypeItem' }
  /dictionaries/apply-types:
    get:
      tags: [Dictionaries]
      summary: Vacancy application / response channel types
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/ApplyTypeItem' }
  /me:
    get:
      tags: [Me]
      summary: Current token context
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: OK }
  /me/vacancy_types:
    get:
      tags: [Me]
      summary: Publication packages / slots
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: OK }
  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhooks
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: OK }
    post:
      tags: [Webhooks]
      summary: Create webhook (secret once)
      security: [{ bearerAuth: [] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items:
                    type: string
                    enum: [vacancy.created, vacancy.updated, negotiation.created]
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Webhook' }
  /webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Delete webhook
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { description: OK }
  /oauth/token:
    post:
      tags: [OAuth]
      summary: Issue access token
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type:
                  type: string
                  enum: [authorization_code, refresh_token, client_credentials]
                client_id: { type: string }
                client_secret: { type: string }
                code: { type: string }
                redirect_uri: { type: string }
                refresh_token: { type: string }
                scope:
                  type: string
                  example: vacancies,negotiations,webhook,employer
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TokenResponse' }
        '401': { $ref: '#/components/responses/Error' }
  /oauth/apps:
    post:
      tags: [OAuth]
      summary: Create app (privileged)
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: Created }
components:
  parameters:
    IdPath:
      in: path
      name: id
      required: true
      schema: { type: integer }
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      properties:
        code: { type: string }
        message: { type: string }
        data:
          type: object
          properties:
            status: { type: integer }
    TokenResponse:
      type: object
      properties:
        access_token: { type: string }
        token_type: { type: string, example: bearer }
        expires_in: { type: integer, example: 3600 }
        refresh_token: { type: string }
        scope: { type: string }
    SalaryTypeItem:
      type: object
      properties:
        id:
          type: string
          enum: [type_1, type_2, type_3, type_4, type_5, negotiable]
        name: { type: string }
        index: { type: integer, nullable: true }
    ApplyTypeItem:
      type: object
      properties:
        id:
          type: string
          enum: [internal, external, with_email, none]
        name: { type: string }
    DictionaryItem:
      type: object
      properties:
        id: { type: string }
        slug: { type: string }
        name: { type: string }
        areas:
          type: array
          items: { $ref: '#/components/schemas/DictionaryItem' }
    ExperienceItem:
      type: object
      properties:
        id:
          type: string
          enum: [fresh, 1-3-years, 3-6-years, 6-years]
        name: { type: string }
        aliases:
          type: array
          items: { type: string }
    LocationRef:
      type: object
      properties:
        id: { type: string, nullable: true }
        name: { type: string, nullable: true }
    Location:
      type: object
      properties:
        country: { $ref: '#/components/schemas/LocationRef' }
        region: { $ref: '#/components/schemas/LocationRef' }
        city: { $ref: '#/components/schemas/LocationRef' }
        city_id:
          type: string
          description: City id from GET /areas (write alias for city.id)
        address:
          type: string
          nullable: true
          description: Map pin address (jobsearch_field_location_address), not in /areas dictionary
    AreaItem:
      type: object
      properties:
        id: { type: string, description: city_id from jobsearch_cities }
        name: { type: string }
        type: { type: string, example: city }
        country: { $ref: '#/components/schemas/LocationRef' }
        region: { $ref: '#/components/schemas/LocationRef' }
    LocationCountry:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        code: { type: string, example: BY }
        regions:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              cities:
                type: array
                items:
                  type: object
                  properties:
                    id: { type: string }
                    name: { type: string }
    Salary:
      type: object
      properties:
        from: { type: number, nullable: true }
        to: { type: number, nullable: true }
        currency: { type: string, example: BYN }
        type:
          $ref: '#/components/schemas/SalaryTypeItem'
        gross: { type: boolean, nullable: true }
    Application:
      type: object
      properties:
        type: { $ref: '#/components/schemas/ApplyTypeItem' }
        url:
          type: string
          format: uri
          description: Required when type=external → jobsearch_field_job_apply_url
        email:
          type: string
          format: email
          description: Required when type=with_email → jobsearch_field_job_apply_email
    ContactPhone:
      type: object
      properties:
        phone: { type: string, example: "+375 25 536-17-17" }
        comment: { type: string }
    VacancyWrite:
      type: object
      required: [name]
      properties:
        name: { type: string }
        position: { type: string }
        description: { type: string }
        body: { type: string }
        requirements: { type: string }
        conditions: { type: string }
        billing_type: { type: string, description: "standard or order_{id}" }
        city_id: { type: string, description: city id from GET /areas }
        city: { type: string, description: "Deprecated: use city_id or location.city" }
        area_id: { type: string, description: Same as city_id }
        area:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
        location: { $ref: '#/components/schemas/Location' }
        location_address: { type: string, description: Map address override }
        sector_id: { type: string }
        sector: { type: string }
        employment_id: { type: string }
        employment: { type: string, description: "jobtype id|slug|name" }
        job_type: { type: string }
        experience:
          oneOf:
            - type: string
            - type: object
              properties:
                id: { type: string }
        salary: { $ref: '#/components/schemas/Salary' }
        application: { $ref: '#/components/schemas/Application' }
        apply_type:
          type: string
          enum: [internal, external, with_email, none]
          description: Legacy; prefer application.type
        apply_url: { type: string, format: uri }
        apply_email: { type: string, format: email }
        skill_ids:
          type: array
          items: { type: string }
        skills:
          type: array
          items:
            oneOf:
              - type: string
              - type: object
                properties:
                  id: { type: string }
        contact_phones:
          type: array
          maxItems: 3
          items: { $ref: '#/components/schemas/ContactPhone' }
    VacancyList:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Vacancy' }
        found: { type: integer }
        page: { type: integer }
        pages: { type: integer }
        per_page: { type: integer }
    Vacancy:
      allOf:
        - type: object
          properties:
            id: { type: string }
            name: { type: string }
            alternate_url: { type: string }
            published_at: { type: string, format: date-time }
            expires_at: { type: string, format: date-time, nullable: true }
            archived: { type: boolean }
            salary: { $ref: '#/components/schemas/Salary' }
            location: { $ref: '#/components/schemas/Location' }
            area:
              type: object
              description: City shorthand (backward compat)
              properties:
                id: { type: string, nullable: true }
                name: { type: string, nullable: true }
            employer:
              type: object
              properties:
                id: { type: string }
                name: { type: string }
                alternate_url: { type: string }
            description: { type: string }
            sectors:
              type: array
              items: { $ref: '#/components/schemas/DictionaryItem' }
            employment: { $ref: '#/components/schemas/DictionaryItem' }
            experience: { $ref: '#/components/schemas/ExperienceItem' }
            skills:
              type: array
              items: { $ref: '#/components/schemas/DictionaryItem' }
            application: { $ref: '#/components/schemas/Application' }
            apply_type:
              type: string
              enum: [internal, external, with_email, none]
            contact_phones:
              type: array
              items: { $ref: '#/components/schemas/ContactPhone' }
    Webhook:
      type: object
      properties:
        id: { type: integer }
        url: { type: string }
        events:
          type: array
          items: { type: string }
        secret: { type: string, description: Shown once on create }
