openapi: 3.0.3
info:
  title: IQHR HR Lite External Ingestion API
  version: 1.6.0
  license:
    name: Proprietary
  contact:
    name: IQ.HR Solutions
    email: office@iqhr.ro
  description: |
    The HR Lite API receives HR data from a client system (for example Workday) for review by HR in IQHR.
    A successful response means the data was **received and stored for HR review**. It does not mean an
    employee was created or changed in HR Manager, and nothing is sent to REGES automatically. The response
    body of each write is the receipt; `GET` on the same resource URI returns its current intake state.

    **Resources and identifiers**

    | Resource | Identifier in the path | Create | Update |
    |---|---|---|---|
    | New employee | `badge` (Employee ID) | `POST /new-employees/{badge}` | `PUT` (complete replacement) |
    | CIM change | `externalChangeId` | `POST /cim-changes/{externalChangeId}` | `PUT` (complete replacement) |
    | Suspension | `externalSuspensionId` | `POST /suspensions/{externalSuspensionId}` | `PATCH` (partial) |
    | Termination | `externalTerminationId` | `POST /terminations/{externalTerminationId}` | `PATCH` (partial) |

    Identifiers are chosen by the client and must stay stable for the life of the record: letters, digits,
    `.`, `_`, `:` and `-` (badge up to 50, other identifiers up to 100 characters). A UUID is recommended for
    change, suspension and termination identifiers. For CIM changes, suspensions and terminations the employee
    `badge` travels in the request body, never in the path.

    **Retries and idempotency**

    The `Idempotency-Key` header is optional. Without it, repeating an identical `POST` returns `200` with
    outcome `UNCHANGED`, and repeating an identical `PUT`/`PATCH` also returns `UNCHANGED`, so a retry after a
    timeout is safe. A `POST` with different data for an existing identifier returns `409 INTAKE_EXISTS`;
    use `PUT`/`PATCH` to update. When the header is sent, it must be a UUID: the same key with the same request
    replays the original receipt, and the same key with a different request returns `409 IDEMPOTENCY_KEY_REUSED`.

    **Field values**

    New-employee and CIM data are sent as a list of `{ "fieldKey": ..., "value": ... }` entries. Every field in
    the published catalogue (`GET /catalogue`) is accepted, under its `apiFieldKey` or any of its `apiAliases`
    (the client annex names, such as `personalIdentifier` or `residenceLocalityCode`). Values are JSON strings
    or `null`: dates `YYYY-MM-DD`, decimals with a dot and no thousands separator, booleans `"true"`/`"false"`
    (yes/no, da/nu and 1/0 are also accepted and stored as true/false). An unknown field key is rejected.

    Dictionary fields (those with a `dictionaryKey` in the catalogue) must contain a value IQHR can resolve:
    an HR code or name listed by `GET /dictionaries/{key}`, or a source value configured by IQHR as an external
    mapping (for example Workday's `Fixed Term`, `Passport` or leave type `RO_UNPAID_LEAVE`). Any other value is
    rejected with `422 DICTIONARY_VALUE_NOT_FOUND`, naming the field as it was sent. When both a Code and a Name
    are sent for the same selection they must identify the same entry (`DICTIONARY_PAIR_MISMATCH`).

    Missing business fields do not block intake: HR completes them before processing. Requests may contain up
    to 500 field entries and 1 MiB.

    **CIM changes** are accepted only for a badge that already exists in HR Manager; otherwise the API returns
    `404 EMPLOYEE_NOT_FOUND` (tenant setting, enabled for this integration).

    **Authentication** uses OAuth 2.0 client credentials. Request a token from the token URL with the client ID
    and secret provided by IQHR and send it as `Authorization: Bearer <token>`. The tenant and integration are
    bound to the credential; no tenant header is needed.

    **Errors** use `application/problem+json`-style bodies with a stable `code`, a `correlationId` to quote to
    IQHR, and for validation failures an `errors` list (`field`, `code`, `message`). Messages are in English;
    the HR team also sees a Romanian explanation of every rejected request.
    Write limits default to 300 requests per minute per integration; `429` responses include `Retry-After`.
servers:
  - url: https://api.example.com/hr-lite/v1
    description: Placeholder. The base URL for each environment is provided by IQ.HR at onboarding.
security:
  - clientCredentials: []
tags:
  - name: New employees
    description: New hires, rehires and transfers from affiliates.
  - name: CIM changes
    description: Changes to an existing employee's data or employment contract.
  - name: Suspensions
    description: Leaves of absence that suspend the employment contract.
  - name: Terminations
    description: Terminations and transfers to affiliates.
  - name: Catalogue
    description: Accepted fields and dictionary values.
paths:
  /new-employees/{badge}:
    parameters:
      - $ref: '#/components/parameters/Badge'
    post:
      tags: [New employees]
      summary: Create a new-employee intake
      description: Creates the intake for the badge. Repeating the identical request returns 200 UNCHANGED.
      operationId: createNewEmployee
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NewEmployeeRequest' }
            examples:
              workday:
                $ref: '#/components/examples/NewEmployeeWorkday'
      responses:
        '201': { $ref: '#/components/responses/Created' }
        '200': { $ref: '#/components/responses/Unchanged' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/TooLarge' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    put:
      tags: [New employees]
      summary: Replace the new-employee data
      description: >-
        Replaces the complete client snapshot of an existing intake: send every field you want to keep.
        Omitted fields are removed from the client snapshot; null clears a value. Values HR has already edited
        are protected and any disagreement is shown to HR as a conflict. Returns 404 if the intake does not exist.
      operationId: replaceNewEmployee
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NewEmployeeRequest' }
      responses:
        '200': { $ref: '#/components/responses/Replaced' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    get:
      tags: [New employees]
      summary: Read the new-employee intake
      operationId: getNewEmployee
      responses:
        '200': { $ref: '#/components/responses/Current' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Gone' }
  /new-employees/{badge}/withdrawals:
    parameters:
      - $ref: '#/components/parameters/Badge'
    post:
      tags: [New employees]
      summary: Withdraw the new-employee intake
      description: Removes the intake from HR review (for example a cancelled hire). It does not change HR Manager data.
      operationId: withdrawNewEmployee
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/Empty'
      responses:
        '200': { $ref: '#/components/responses/Withdrawn' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /cim-changes/{externalChangeId}:
    parameters:
      - $ref: '#/components/parameters/ExternalChangeId'
    post:
      tags: [CIM changes]
      summary: Create a CIM change
      description: >-
        Proposes changes for an existing employee, effective on effectiveDate. Every catalogue field can be
        changed. The badge must exist in HR Manager (otherwise 404 EMPLOYEE_NOT_FOUND).
      operationId: createCimChange
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CimChangeRequest' }
            examples:
              workday:
                $ref: '#/components/examples/CimChangeWorkday'
      responses:
        '201': { $ref: '#/components/responses/Created' }
        '200': { $ref: '#/components/responses/Unchanged' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/EmployeeNotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    put:
      tags: [CIM changes]
      summary: Replace the CIM change
      description: >-
        Replaces the complete change snapshot of an existing CIM change (badge must not change; companyCode may be
        omitted). Omitted change lines are removed. Returns 404 NOT_FOUND if the change does not exist and
        404 EMPLOYEE_NOT_FOUND if the badge is not in HR Manager.
      operationId: replaceCimChange
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CimChangeRequest' }
      responses:
        '200': { $ref: '#/components/responses/Replaced' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    get:
      tags: [CIM changes]
      summary: Read the CIM change
      operationId: getCimChange
      responses:
        '200': { $ref: '#/components/responses/Current' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Gone' }
  /cim-changes/{externalChangeId}/withdrawals:
    parameters:
      - $ref: '#/components/parameters/ExternalChangeId'
    post:
      tags: [CIM changes]
      summary: Withdraw the CIM change
      operationId: withdrawCimChange
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/Empty'
      responses:
        '200': { $ref: '#/components/responses/Withdrawn' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /suspensions/{externalSuspensionId}:
    parameters:
      - $ref: '#/components/parameters/ExternalSuspensionId'
    post:
      tags: [Suspensions]
      summary: Create a suspension
      operationId: createSuspension
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SuspensionCreateRequest' }
            examples:
              workday:
                value: { companyCode: RO01, badge: 'E2347', reasonName: RO_UNPAID_LEAVE, startDate: '2026-12-01', endDate: '2027-01-31' }
      responses:
        '201': { $ref: '#/components/responses/Created' }
        '200': { $ref: '#/components/responses/Unchanged' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    patch:
      tags: [Suspensions]
      summary: Update a suspension
      description: >-
        Changes only the supplied properties; omitted properties keep their value and null clears endDate or
        returnDate. The optional action qualifies the update for HR: MODIFY (a real change), CORRECT (a correction
        of wrong data) or UNCLASSIFIED (HR decides). Without action the update is recorded as UNCLASSIFIED.
        MODIFY and CORRECT must contain at least one date property.
      operationId: updateSuspension
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: action
          in: query
          required: false
          schema: { type: string, enum: [MODIFY, CORRECT, UNCLASSIFIED], default: UNCLASSIFIED }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SuspensionPatchRequest' }
            examples:
              extend:
                value: { endDate: '2027-02-28' }
              returned:
                value: { returnDate: '2027-02-15' }
      responses:
        '200': { $ref: '#/components/responses/Replaced' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    get:
      tags: [Suspensions]
      summary: Read the suspension
      operationId: getSuspension
      responses:
        '200': { $ref: '#/components/responses/Current' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Gone' }
  /suspensions/{externalSuspensionId}/cancellations:
    parameters:
      - $ref: '#/components/parameters/ExternalSuspensionId'
    post:
      tags: [Suspensions]
      summary: Request cancellation of a suspension
      description: Records that the leave was cancelled in the source system. HR decides the follow-up.
      operationId: cancelSuspension
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                changeReason: { type: string, maxLength: 500, nullable: true }
            example: { changeReason: Leave cancelled in Workday }
      responses:
        '200': { $ref: '#/components/responses/Replaced' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
  /suspensions/{externalSuspensionId}/withdrawals:
    parameters:
      - $ref: '#/components/parameters/ExternalSuspensionId'
    post:
      tags: [Suspensions]
      summary: Withdraw the suspension intake
      description: Removes a suspension sent by mistake from HR review. To cancel a real leave use /cancellations.
      operationId: withdrawSuspension
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/Empty'
      responses:
        '200': { $ref: '#/components/responses/Withdrawn' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /terminations/{externalTerminationId}:
    parameters:
      - $ref: '#/components/parameters/ExternalTerminationId'
    post:
      tags: [Terminations]
      summary: Create a termination
      operationId: createTermination
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TerminationCreateRequest' }
            examples:
              workday:
                value: { companyCode: RO01, badge: 'E2347', notEmployedFrom: '2027-03-01', terminationReasonName: Resignation }
      responses:
        '201': { $ref: '#/components/responses/Created' }
        '200': { $ref: '#/components/responses/Unchanged' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    patch:
      tags: [Terminations]
      summary: Update a termination
      description: notEmployedFrom is always required; omitted reasons keep their value and null clears them.
      operationId: updateTermination
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TerminationPatchRequest' }
            example: { notEmployedFrom: '2027-03-02' }
      responses:
        '200': { $ref: '#/components/responses/Replaced' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Gone' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
    get:
      tags: [Terminations]
      summary: Read the termination
      operationId: getTermination
      responses:
        '200': { $ref: '#/components/responses/Current' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Gone' }
  /terminations/{externalTerminationId}/withdrawals:
    parameters:
      - $ref: '#/components/parameters/ExternalTerminationId'
    post:
      tags: [Terminations]
      summary: Withdraw the termination intake
      operationId: withdrawTermination
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        $ref: '#/components/requestBodies/Empty'
      responses:
        '200': { $ref: '#/components/responses/Withdrawn' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /catalogue:
    get:
      tags: [Catalogue]
      summary: Read the published field catalogue
      description: >-
        One entry per accepted field: the name to send (fieldKey) and accepted alternative names (aliases), English and
        Romanian labels, the data section (personal, employment, position, contract, payroll, address, documents,
        bankAccounts, healthInsurance, disability, suspension, termination), the value type, the requests that accept it
        (usedIn) and, for lookup fields, the dictionaryKey to read with GET /dictionaries/{key}. The ETag changes when
        IQHR publishes a new revision.
      operationId: getCatalogue
      responses:
        '200':
          description: Published catalogue.
          headers:
            ETag: { schema: { type: string }, description: 'Catalogue revision, for example "catalogue-3".' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Catalogue' }
              example:
                revision: 3
                publishedAt: '2026-10-06T07:44:53Z'
                fields:
                  - { fieldKey: lastName, label: Last Name, labelRo: Nume, section: personal, sectionLabel: Personal details, type: STRING, usedIn: [NEW_EMPLOYEE, CIM_CHANGE], maxLength: 50, informational: false, aliases: [] }
                  - { fieldKey: cnp, aliases: [personalIdentifier], label: CNP, labelRo: CNP, section: personal, sectionLabel: Personal details, type: STRING, usedIn: [NEW_EMPLOYEE, CIM_CHANGE], informational: false }
                  - { fieldKey: employeeAllocationHistory.companyId, aliases: [companyCode], label: Company, labelRo: Companie, section: position, sectionLabel: Position and organisation, type: STRING, usedIn: [NEW_EMPLOYEE, CIM_CHANGE], dictionaryKey: 'hr-create:company', informational: false }
                  - { fieldKey: addressCityCode, aliases: [residenceLocalityCode], label: City (code), labelRo: Oras (cod), section: address, sectionLabel: Address, type: STRING, usedIn: [NEW_EMPLOYEE, CIM_CHANGE], dictionaryKey: cities, informational: false }
                  - { fieldKey: hasDisability, aliases: [], label: Has disability, labelRo: Are dizabilitate (informativ), section: disability, sectionLabel: Disability, type: BOOLEAN, usedIn: [NEW_EMPLOYEE, CIM_CHANGE], informational: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /dictionaries/{key}:
    get:
      tags: [Catalogue]
      summary: Read the HR values of a dictionary
      description: >-
        HR codes and names accepted for fields using this dictionaryKey (URL-encode the key, for example
        hr-create%3Acompany). Source values configured by IQHR as external mappings are accepted in addition.
      operationId: getDictionary
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string }
          example: 'hr-create:company'
      responses:
        '200':
          description: Dictionary values.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Dictionary' }
              example:
                key: 'hr-create:company'
                catalogueRevision: 3
                values:
                  - { code: RO01, name: EXAMPLE COMPANY S.R.L. }
                  - { code: RO02, name: EXAMPLE SERVICES S.R.L. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationFailed' }
components:
  securitySchemes:
    clientCredentials:
      type: oauth2
      description: OAuth 2.0 client credentials. The token URL, client ID and secret are provided by IQ.HR for each environment (the URL above is a placeholder).
      flows:
        clientCredentials:
          tokenUrl: https://auth.example.com/oauth/token
          scopes: {}
  parameters:
    Badge:
      name: badge
      in: path
      required: true
      description: Employee ID (badge) of the new employee; case-insensitive.
      schema: { type: string, pattern: '^[A-Za-z0-9._:-]{1,50}$' }
      example: 'E2400'
    ExternalChangeId:
      name: externalChangeId
      in: path
      required: true
      description: Stable client identifier of the CIM change (UUID recommended).
      schema: { type: string, pattern: '^[A-Za-z0-9._:-]{1,100}$' }
      example: 0e7c8f1c-2a6b-4f5e-9b9a-2b4b6c1d7e01
    ExternalSuspensionId:
      name: externalSuspensionId
      in: path
      required: true
      description: Stable client identifier of the leave of absence (UUID recommended).
      schema: { type: string, pattern: '^[A-Za-z0-9._:-]{1,100}$' }
      example: 5b2f6e44-8d61-4f0f-9a1e-7b9b0c3d2e11
    ExternalTerminationId:
      name: externalTerminationId
      in: path
      required: true
      description: Stable client identifier of the termination (UUID recommended).
      schema: { type: string, pattern: '^[A-Za-z0-9._:-]{1,100}$' }
      example: a3c4d5e6-1b2c-4d3e-8f90-0a1b2c3d4e5f
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Optional UUID. Replays the original receipt for an identical retry; reuse for a different request returns 409.
      schema: { type: string, format: uuid }
  requestBodies:
    Empty:
      required: true
      content:
        application/json:
          schema: { type: object, additionalProperties: false }
          example: {}
  schemas:
    FieldValue:
      type: object
      additionalProperties: false
      required: [fieldKey, value]
      properties:
        fieldKey:
          type: string
          pattern: '^[A-Za-z][A-Za-z0-9._-]{0,99}$'
          description: apiFieldKey or one of the apiAliases of a catalogue field. Each field at most once per request.
        value:
          type: string
          nullable: true
          maxLength: 4000
          description: Value as a string, or null to clear. Dictionary fields accept HR codes/names or configured external values.
    NewEmployeeRequest:
      type: object
      additionalProperties: false
      required: [fields]
      properties:
        fields:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/FieldValue' }
      description: |
        Client annex field names accepted for new employees (all catalogue fields are accepted):
        companyCode, lastName, firstName, previousLastName, personalIdentifier, nationalityCode, citizenshipCode,
        residenceCountryCode, hireDate, costCenterCode, corCode, internalContractNumber, contractSigningDate,
        contractDurationTypeCode, contractEndDate, grossSalary, workHoursPerDay, workNormCode, residenceLocalityCode,
        residenceCountyCode, residenceDistrict, residenceStreet, residenceStreetNumber, residenceBuilding,
        residenceEntrance, residenceFloor, residenceApartment, identityDocumentTypeCode, identityDocumentNumber,
        identityDocumentIssueDate, identityDocumentExpiryDate, identityDocumentIssuer, hasDisability.
        Code fields also accept the matching Name variant (for example workNormName, contractDurationTypeName).
    CimChangeRequest:
      type: object
      additionalProperties: false
      required: [badge, effectiveDate, changes]
      properties:
        badge: { type: string, minLength: 1, maxLength: 50, description: Badge of an employee existing in HR Manager. }
        companyCode: { type: string, maxLength: 100, description: 'Optional; must be a known company code (for example RO01, RO07).' }
        effectiveDate: { type: string, format: date, description: Date from which the changes apply. }
        changeType: { type: string, maxLength: 100, nullable: true }
        changeReason: { type: string, maxLength: 500, nullable: true }
        comment: { type: string, maxLength: 2000, nullable: true }
        changes:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/FieldValue' }
    SuspensionCreateRequest:
      type: object
      additionalProperties: false
      required: [badge, startDate]
      description: reasonCode or reasonName is required (Workday leave type tokens such as RO_UNPAID_LEAVE are accepted as reasonName).
      properties:
        companyCode: { type: string, maxLength: 100 }
        badge: { type: string, minLength: 1, maxLength: 50 }
        reasonCode: { type: string, maxLength: 100 }
        reasonName: { type: string, maxLength: 255 }
        startDate: { type: string, format: date, description: First day of the suspension. }
        endDate: { type: string, format: date, nullable: true, description: 'Last day of the suspension, when known; on or after startDate.' }
        returnDate: { type: string, format: date, nullable: true, description: Effective return date; after startDate. }
        changeReason: { type: string, maxLength: 500, nullable: true }
        notes: { type: string, maxLength: 2000, nullable: true }
    SuspensionPatchRequest:
      type: object
      additionalProperties: false
      properties:
        reasonCode: { type: string, maxLength: 100, nullable: true }
        reasonName: { type: string, maxLength: 255, nullable: true }
        startDate: { type: string, format: date }
        endDate: { type: string, format: date, nullable: true }
        returnDate: { type: string, format: date, nullable: true }
        changeReason: { type: string, maxLength: 500, nullable: true }
        notes: { type: string, maxLength: 2000, nullable: true }
    TerminationCreateRequest:
      type: object
      additionalProperties: false
      required: [badge, notEmployedFrom]
      properties:
        companyCode: { type: string, maxLength: 100 }
        badge: { type: string, minLength: 1, maxLength: 50 }
        notEmployedFrom:
          type: string
          format: date
          description: First calendar day on which the person is no longer employed (last working day + 1).
        terminationReasonCode: { type: string, maxLength: 100, nullable: true }
        terminationReasonName: { type: string, maxLength: 255, nullable: true }
    TerminationPatchRequest:
      type: object
      additionalProperties: false
      required: [notEmployedFrom]
      properties:
        notEmployedFrom: { type: string, format: date }
        terminationReasonCode: { type: string, maxLength: 100, nullable: true }
        terminationReasonName: { type: string, maxLength: 255, nullable: true }
    Receipt:
      type: object
      required: [intakeId, resourceType, externalReference, outcome, recordState, version, receivedAt, correlationId]
      properties:
        intakeId: { type: string, description: IQHR internal reference. }
        resourceType: { type: string, enum: [NEW_EMPLOYEE, CIM_CHANGE, SUSPENSION, TERMINATION] }
        externalReference: { type: string, description: Identifier from the path. }
        badge: { type: string, nullable: true }
        outcome: { type: string, enum: [CREATED, REPLACED, UNCHANGED, WITHDRAWN] }
        recordState: { type: string, enum: [PENDING, DONE, DISMISSED], description: HR review state. }
        version: { type: integer, description: Client revision number of the intake. }
        receivedAt: { type: string, format: date-time }
        correlationId: { type: string }
      example:
        intakeId: '62'
        resourceType: TERMINATION
        externalReference: a3c4d5e6-1b2c-4d3e-8f90-0a1b2c3d4e5f
        badge: 'E2347'
        outcome: CREATED
        recordState: PENDING
        version: 1
        receivedAt: '2026-10-06T08:59:16.359Z'
        correlationId: 143e0b10-deda-4202-adaa-a8216c81c6fe
    Current:
      type: object
      properties:
        resourceType: { type: string }
        externalReference: { type: string }
        badge: { type: string, nullable: true }
        workflowStatus: { type: string, enum: [PENDING, DONE, DISMISSED] }
        withdrawn: { type: boolean }
        version: { type: integer }
        payload: { type: object, description: The latest data as sent by the client (normalized field keys). }
        validationState: { type: string, description: 'HR review indicator (VALID, WARNING, INVALID, STALE, ...).' }
        validationIssues:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              code: { type: string }
              message: { type: string }
        receivedAt: { type: string, format: date-time }
        lastAction: { type: string, enum: [CREATE, REPLACE, MODIFY, CORRECT, UNCLASSIFIED, UPDATE, CANCEL, WITHDRAW] }
    Problem:
      type: object
      required: [status, code, correlationId]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        code:
          type: string
          description: >-
            UNAUTHORIZED, FORBIDDEN, INTEGRATION_NOT_ACTIVE, VALIDATION_FAILED, NOT_FOUND, EMPLOYEE_NOT_FOUND,
            INTAKE_EXISTS, INTAKE_WITHDRAWN, IDENTITY_CONFLICT, IDEMPOTENCY_KEY_REUSED, INVALID_IDEMPOTENCY_KEY,
            REQUEST_IN_PROGRESS, INVALID_ACTION, ACTION_NOT_ALLOWED, INVALID_QUERY, METHOD_NOT_ALLOWED, GONE,
            PAYLOAD_TOO_LARGE, TOO_MANY_REQUESTS, INTERNAL_ERROR.
        correlationId: { type: string }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string, example: fields.residenceLocalityCode }
              code:
                type: string
                description: >-
                  UNKNOWN_FIELD, FIELD_DISABLED, FIELD_NOT_ALLOWED, DUPLICATE_FIELD, REQUIRED, NULL_NOT_ALLOWED,
                  INVALID_SHAPE, INVALID_IDENTIFIER, INVALID_VALUE_TYPE, VALUE_TOO_LONG, INVALID_FORMAT, INVALID_DATE,
                  INVALID_TIME, INVALID_BOOLEAN, INVALID_DECIMAL, INVALID_INTEGER, OUT_OF_RANGE, SCALE_EXCEEDED,
                  PRECISION_EXCEEDED, INVALID_INTERVAL, INVALID_RETURN_DATE, DATE_FIELD_REQUIRED,
                  DICTIONARY_VALUE_REQUIRED, DICTIONARY_VALUE_NOT_FOUND, DICTIONARY_VALUE_AMBIGUOUS,
                  DICTIONARY_PAIR_MISMATCH, UNKNOWN_CHANGE_TYPE.
              message: { type: string }
      example:
        type: about:blank
        title: Validation failed
        status: 422
        code: VALIDATION_FAILED
        correlationId: 20323c34-82f8-4676-858c-f0fa5cf34b0c
        errors:
          - { field: fields.residenceLocalityCode, code: DICTIONARY_VALUE_NOT_FOUND, message: dictionary value not found }
    Catalogue:
      type: object
      properties:
        revision: { type: integer }
        publishedAt: { type: string, format: date-time }
        fields:
          type: array
          items: { $ref: '#/components/schemas/CatalogueField' }
    CatalogueField:
      type: object
      required: [fieldKey, label, section, sectionLabel, type, usedIn]
      properties:
        fieldKey: { type: string, description: Name to send as fieldKey (suspension/termination fields are body properties). }
        aliases: { type: array, items: { type: string }, description: Other accepted names for the same field. }
        label: { type: string, description: English label. }
        labelRo: { type: string, description: Romanian label as shown to HR. }
        section: { type: string, enum: [personal, employment, position, contract, payroll, address, documents, bankAccounts, healthInsurance, disability, suspension, termination, other] }
        sectionLabel: { type: string }
        type: { type: string, enum: [STRING, DATE, TIME, BOOLEAN, DECIMAL], description: 'DATE YYYY-MM-DD, TIME HH:mm, BOOLEAN true/false, DECIMAL with a dot.' }
        usedIn: { type: array, items: { type: string, enum: [NEW_EMPLOYEE, CIM_CHANGE, SUSPENSION, TERMINATION] } }
        dictionaryKey: { type: string, description: 'Lookup field; read accepted values with GET /dictionaries/{dictionaryKey}.' }
        maxLength: { type: integer }
        informational: { type: boolean, description: Shown to HR without being stored in an HR Manager field. }
    Dictionary:
      type: object
      properties:
        key: { type: string }
        catalogueRevision: { type: integer }
        values:
          type: array
          items:
            type: object
            properties:
              code: { type: string, nullable: true }
              name: { type: string }
  examples:
    NewEmployeeWorkday:
      summary: New hire with the client annex fields
      value:
        fields:
          - { fieldKey: companyCode, value: RO01 }
          - { fieldKey: lastName, value: Popescu }
          - { fieldKey: firstName, value: Ana-Maria }
          - { fieldKey: personalIdentifier, value: '2900101400000' }
          - { fieldKey: nationalityCode, value: RO }
          - { fieldKey: citizenshipCode, value: RO }
          - { fieldKey: residenceCountryCode, value: RO }
          - { fieldKey: hireDate, value: '2026-11-03' }
          - { fieldKey: costCenterCode, value: '800' }
          - { fieldKey: corCode, value: RO - 121011 }
          - { fieldKey: internalContractNumber, value: WD-CTR-000123 }
          - { fieldKey: contractSigningDate, value: '2026-10-20' }
          - { fieldKey: contractDurationTypeCode, value: Fixed Term }
          - { fieldKey: contractEndDate, value: '2027-11-02' }
          - { fieldKey: grossSalary, value: '8500' }
          - { fieldKey: workHoursPerDay, value: '8' }
          - { fieldKey: workNormCode, value: Full time }
          - { fieldKey: residenceLocalityName, value: Brasov }
          - { fieldKey: residenceCountyCode, value: BV }
          - { fieldKey: residenceDistrict, value: null }
          - { fieldKey: residenceStreet, value: Str. Lunga }
          - { fieldKey: residenceStreetNumber, value: '10' }
          - { fieldKey: residenceBuilding, value: A }
          - { fieldKey: residenceEntrance, value: '2' }
          - { fieldKey: residenceFloor, value: '3' }
          - { fieldKey: residenceApartment, value: '12' }
          - { fieldKey: identityDocumentTypeCode, value: National ID Card }
          - { fieldKey: identityDocumentNumber, value: BV123456 }
          - { fieldKey: identityDocumentIssueDate, value: '2024-01-10' }
          - { fieldKey: identityDocumentExpiryDate, value: '2034-01-09' }
          - { fieldKey: identityDocumentIssuer, value: SPCLEP Brasov }
          - { fieldKey: hasDisability, value: 'false' }
    CimChangeWorkday:
      summary: Salary and name change
      value:
        badge: 'E2347'
        companyCode: RO01
        effectiveDate: '2026-11-15'
        changes:
          - { fieldKey: grossSalary, value: '9100' }
          - { fieldKey: lastName, value: Biris-Pop }
          - { fieldKey: previousLastName, value: Biris }
  responses:
    Created:
      description: Created. The intake is stored for HR review.
      headers:
        X-Correlation-ID: { schema: { type: string } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Receipt' }
    Unchanged:
      description: Identical request repeated (outcome UNCHANGED), or an Idempotency-Key replay of a 200 response.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Receipt' }
    Replaced:
      description: Updated (outcome REPLACED) or identical to the current data (outcome UNCHANGED).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Receipt' }
    Withdrawn:
      description: Withdrawn from HR review (outcome WITHDRAWN). Repeating it is harmless.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Receipt' }
    Current:
      description: Current intake state.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Current' }
    BadRequest:
      description: Malformed JSON, invalid Idempotency-Key, unsupported query parameter or action.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    Forbidden:
      description: The credential is not bound to an active HR Lite integration.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    NotFound:
      description: The intake does not exist (create it with POST first).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    EmployeeNotFound:
      description: EMPLOYEE_NOT_FOUND - the badge is not an employee in HR Manager.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
          example: { type: about:blank, title: No current HR Manager employee has this badge, status: 404, code: EMPLOYEE_NOT_FOUND, correlationId: 9f4d6480-914b-41a4-847c-55a59747a495 }
    Conflict:
      description: INTAKE_EXISTS, INTAKE_WITHDRAWN, IDENTITY_CONFLICT, IDEMPOTENCY_KEY_REUSED or REQUEST_IN_PROGRESS (retry later).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    Gone:
      description: The intake was removed by the retention policy.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    TooLarge:
      description: The request body exceeds 1 MiB.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    ValidationFailed:
      description: VALIDATION_FAILED - see errors[] for each rejected field. Nothing was stored.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
    TooManyRequests:
      description: Rate limit exceeded; retry after the Retry-After seconds.
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Problem' }
