> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fluidehr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate an approved payroll run and generate payslips



## OpenAPI

````yaml /openapi/fluide-payroll.json post /api/v1/payroll/payroll/validate
openapi: 3.0.0
info:
  title: Fluide Payroll API
  description: >-
    Payroll runs, payslip generation, validators, and async payroll processing.
    Integrate after employee records exist in HR. In the API playground, click
    Authorize and provide Bearer JWT, X-Fluide-Api-Key, and X-Fluide-Client-Id
    (fluide-developer). For partner / ISV integrations acting on a merchant,
    also set optional X-Workspace-Id and X-Acting-Company-Id on each request
    (see Multi-tenancy).
  version: '1.0'
  contact: {}
servers:
  - url: https://test.api.fluidehr.com
    description: API
security:
  - bearer: []
    fluideApiKey: []
    fluideClientId: []
tags:
  - name: App
    description: Service root and build metadata. Use for quick connectivity checks.
    x-group: Operations
  - name: Prometheus
    description: >-
      Prometheus scrape endpoint in text exposition format. Configure your
      metrics collector to poll this path on each service.
    x-group: Operations
  - name: Audit
  - name: Payslips
  - name: Payroll ESS
  - name: Payroll tax remittance
  - name: Rules
  - name: Payroll
  - name: Payroll Settings
  - name: Tax filing
  - name: Authorize
    description: >-
      Exchange API key and secret for a machine JWT, read developer metadata,
      rotate secrets, and manage API billing.
paths:
  /api/v1/payroll/payroll/validate:
    post:
      tags:
        - Payroll
      summary: Validate an approved payroll run and generate payslips
      operationId: PayrollController_validate
      parameters:
        - name: X-Workspace-Id
          in: header
          required: false
          description: >-
            Partner / ISV only: UUID of the workspace that owns the client
            company. Required together with X-Acting-Company-Id when scoping
            product APIs to a merchant. See /getting-started/multi-tenancy.
          schema:
            type: string
            format: uuid
          example: b03fa178-67bd-4378-a5aa-d169c01ccb6f
        - name: X-Acting-Company-Id
          in: header
          required: false
          description: >-
            Partner / ISV only: UUID of the client company to act on. Must
            belong to the workspace in X-Workspace-Id.
          schema:
            type: string
            format: uuid
          example: ab2df10a-c66c-4bef-b7d6-efda26cca494
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidatePayrollDto'
      responses:
        '200':
          description: Successful response
        '201':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayrollRun'
        '400':
          description: Validation failed or invalid request parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponseDto'
        '401':
          description: Missing or invalid JWT / API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponseDto'
        '403':
          description: Token valid but insufficient permission for this operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponseDto'
        '404':
          description: Resource not found or outside caller scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponseDto'
      security:
        - bearer: []
          fluideApiKey: []
          fluideClientId: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >-
            curl -sS -X POST "$FLUIDE_BASE_URL/api/v1/payroll/payroll/validate"
            \
              -H "Authorization: Bearer $FLUIDE_ACCESS_TOKEN" \
              -H "X-Fluide-Api-Key: $FLUIDE_API_KEY" \
              -H "X-Fluide-Client-Id: fluide-developer" \
              -H "X-Workspace-Id: $FLUIDE_WORKSPACE_ID" \
              -H "X-Acting-Company-Id: $FLUIDE_COMPANY_ID" \
              -H "Content-Type: application/json" \
              -d '{}'
        - lang: node
          label: Node.js
          source: >-
            const baseUrl = process.env.FLUIDE_BASE_URL;


            const response = await
            fetch(`${baseUrl}/api/v1/payroll/payroll/validate`, {
              method: 'POST',
              headers: {
                Authorization: `Bearer ${process.env.FLUIDE_ACCESS_TOKEN}`,
                'X-Fluide-Api-Key': process.env.FLUIDE_API_KEY,
                'X-Fluide-Client-Id': 'fluide-developer',
                'X-Workspace-Id': process.env.FLUIDE_WORKSPACE_ID,
                'X-Acting-Company-Id': process.env.FLUIDE_COMPANY_ID,
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({}),
            });


            if (!response.ok) throw new Error(`HTTP ${response.status}: ${await
            response.text()}`);

            console.log(await response.json());
        - lang: python
          label: Python
          source: |-
            import os
            import requests

            base_url = os.environ["FLUIDE_BASE_URL"]
            headers = {
                    "Authorization": f"Bearer {os.environ['FLUIDE_ACCESS_TOKEN']}",
                    "X-Fluide-Api-Key": os.environ["FLUIDE_API_KEY"],
                    "X-Fluide-Client-Id": "fluide-developer",
                    "X-Workspace-Id": os.environ["FLUIDE_WORKSPACE_ID"],
                    "X-Acting-Company-Id": os.environ["FLUIDE_COMPANY_ID"],
            }

            response = requests.post(
                f"{base_url}/api/v1/payroll/payroll/validate",
                headers=headers,
                json={},
                timeout=30,
            )
            response.raise_for_status()
            print(response.json())
        - lang: java
          label: Java
          source: >-
            import java.net.URI;

            import java.net.http.HttpClient;

            import java.net.http.HttpRequest;

            import java.net.http.HttpResponse;


            String baseUrl = System.getenv("FLUIDE_BASE_URL");

            HttpClient client = HttpClient.newHttpClient();

            HttpRequest.Builder builder = HttpRequest.newBuilder()
                .uri(URI.create(baseUrl + "/api/v1/payroll/payroll/validate"))
                .header("Authorization", "Bearer " + System.getenv("FLUIDE_ACCESS_TOKEN"))
                .header("X-Fluide-Api-Key", System.getenv("FLUIDE_API_KEY"))
                .header("X-Fluide-Client-Id", "fluide-developer")
                .header("X-Workspace-Id", System.getenv("FLUIDE_WORKSPACE_ID"))
                .header("X-Acting-Company-Id", System.getenv("FLUIDE_COMPANY_ID"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString("{}"))
                .build();
            HttpResponse<String> response = client.send(builder.build(),
            HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) throw new RuntimeException("HTTP "
            + response.statusCode() + ": " + response.body());

            System.out.println(response.body());
        - lang: php
          label: PHP
          source: >-
            <?php

            $baseUrl = getenv("FLUIDE_BASE_URL");

            $ch = curl_init($baseUrl . "/api/v1/payroll/payroll/validate");

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_CUSTOMREQUEST => 'POST',
                CURLOPT_HTTPHEADER => [
                    'Authorization: Bearer ' . getenv('FLUIDE_ACCESS_TOKEN'),
                    'X-Fluide-Api-Key: ' . getenv('FLUIDE_API_KEY'),
                    'X-Fluide-Client-Id: fluide-developer',
                    'X-Workspace-Id: ' . getenv('FLUIDE_WORKSPACE_ID'),
                    'X-Acting-Company-Id: ' . getenv('FLUIDE_COMPANY_ID'),
                    'Content-Type: application/json',
                ],
                CURLOPT_POSTFIELDS => "{}",
            ]);

            $response = curl_exec($ch);

            if ($response === false) throw new
            RuntimeException(curl_error($ch));

            $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

            if ($status >= 400) throw new RuntimeException("HTTP $status:
            $response");

            echo $response;
components:
  schemas:
    ValidatePayrollDto:
      type: object
      properties:
        payrollRunId:
          type: string
          format: uuid
          description: Payroll run ID
          example: c0a80123-...
        employeeIds:
          minItems: 1
          description: >-
            Only validate these payroll employee IDs (payslips for others are
            removed). Omit for all lines.
          type: array
          items:
            type: string
            format: uuid
      required:
        - payrollRunId
    PayrollRun:
      type: object
      properties:
        organizationId:
          type: string
        companyId:
          type: string
        title:
          type: string
        periodMonth:
          type: number
        periodYear:
          type: number
        countryCode:
          type: string
        status:
          enum:
            - DRAFT
            - PENDING_APPROVAL
            - REJECTED
            - APPROVED
            - VALIDATED
            - DISBURSING
            - SETTLED
            - CANCELLED
          type: string
        idempotencyKey:
          type: string
          nullable: true
        totalGross:
          type: number
        totalDeductions:
          type: number
        totalNet:
          type: number
        currency:
          type: string
        totalsByCurrency:
          type: object
          description: >-
            Per-currency sums for the run. When the run spans multiple line
            currencies, scalar totals are zero.

            Shape: { [iso4217]: { gross, deductions, net } }.
        processedBy:
          type: string
        processedAt:
          format: date-time
          type: string
        lockedAt:
          format: date-time
          type: string
          nullable: true
        submittedAt:
          format: date-time
          type: string
          nullable: true
        approvedAt:
          format: date-time
          type: string
          nullable: true
        rejectedAt:
          format: date-time
          type: string
          nullable: true
        validatedAt:
          format: date-time
          type: string
          nullable: true
        ruleSnapshotId:
          type: string
          nullable: true
        calculationFingerprint:
          type: string
          nullable: true
        includedEmployeeIds:
          nullable: true
          description: >-
            When set, this run was computed only for these canonical employee
            UUIDs (fluide-hr id = payroll mirror id).
          type: array
          items:
            type: string
        payrollCycleId:
          type: string
          nullable: true
        isOffCycle:
          type: boolean
        periodStart:
          type: string
          nullable: true
        periodEnd:
          type: string
          nullable: true
        payDate:
          type: string
          nullable: true
        runType:
          type: object
        offCycleReason:
          type: string
          nullable: true
        retroEffectiveDate:
          type: string
          nullable: true
          description: >-
            Inclusive effective date for retroactive corrections (RETRO runs
            only).
        retroReason:
          type: string
          nullable: true
        referencePayrollRunId:
          type: string
          nullable: true
          description: Prior run whose lines are subtracted when computing retro deltas.
        settlementSnapshot:
          type: object
          nullable: true
        taxRemittanceDocumentUrl:
          type: string
          nullable: true
        taxRemittanceArtifactId:
          type: string
          nullable: true
        payslips:
          type: array
          items:
            $ref: '#/components/schemas/PaySlip'
        id:
          type: string
        createdAt:
          format: date-time
          type: string
        updatedAt:
          format: date-time
          type: string
        deletedAt:
          format: date-time
          type: string
      required:
        - organizationId
        - companyId
        - title
        - periodMonth
        - periodYear
        - countryCode
        - status
        - idempotencyKey
        - totalGross
        - totalDeductions
        - totalNet
        - currency
        - totalsByCurrency
        - processedBy
        - processedAt
        - lockedAt
        - submittedAt
        - approvedAt
        - rejectedAt
        - validatedAt
        - ruleSnapshotId
        - calculationFingerprint
        - includedEmployeeIds
        - payrollCycleId
        - isOffCycle
        - periodStart
        - periodEnd
        - payDate
        - runType
        - offCycleReason
        - retroEffectiveDate
        - retroReason
        - referencePayrollRunId
        - settlementSnapshot
        - taxRemittanceDocumentUrl
        - taxRemittanceArtifactId
        - payslips
        - id
        - createdAt
        - updatedAt
    ApiErrorResponseDto:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
          example: Validation failed
          description: Human-readable error message (localized when i18n is configured)
        code:
          type: string
          example: VALIDATION_FAILED
          description: Stable machine-readable error code for client handling and support
        errors:
          type: object
          description: Field-level validation errors keyed by property name
          example:
            from:
              - from must be a valid date
        statusCode:
          type: number
          example: 400
        timestamp:
          type: string
          example: '2026-06-03T12:00:00.000Z'
      required:
        - success
        - message
        - code
        - statusCode
        - timestamp
    PaySlip:
      type: object
      properties:
        payrollRunId:
          type: string
        payrollRun:
          $ref: '#/components/schemas/PayrollRun'
        employeeId:
          type: string
        employee:
          $ref: '#/components/schemas/Employee'
        basicSalary:
          type: number
        allowances:
          type: object
        deductions:
          type: object
        grossPay:
          type: number
        netPay:
          type: number
        currency:
          type: string
        payoutCurrency:
          type: string
          nullable: true
          description: >-
            ISO currency used for FluidePay / PSP payout (Alternate A:
            rail-driven).

            Must match {@link currency} for bank/MoMo; set at validation from
            employee + run country.
        employerLegalName:
          type: string
          nullable: true
          description: >-
            Denormalized from FluideAuth company at validation (Auth is SoT;
            payroll DB does not store companies).
        status:
          type: string
          enum:
            - PENDING_PAYMENT
            - DISBURSING
            - PAID
            - FAILED
        disbursementQueuedAt:
          format: date-time
          type: string
          nullable: true
        disbursementReference:
          type: string
          nullable: true
        disbursementError:
          type: string
          nullable: true
        documentPublicUrl:
          type: string
          nullable: true
          description: >-
            Public download URL (from fluide-utils) — denormalized for APIs;
            file remains in utils.
        documentContentHash:
          type: string
          nullable: true
        documentArtifactId:
          type: string
          nullable: true
        documentReadyAt:
          format: date-time
          type: string
          nullable: true
        id:
          type: string
        createdAt:
          format: date-time
          type: string
        updatedAt:
          format: date-time
          type: string
        deletedAt:
          format: date-time
          type: string
      required:
        - payrollRunId
        - payrollRun
        - employeeId
        - employee
        - basicSalary
        - allowances
        - deductions
        - grossPay
        - netPay
        - currency
        - payoutCurrency
        - employerLegalName
        - status
        - disbursementQueuedAt
        - disbursementReference
        - disbursementError
        - documentPublicUrl
        - documentContentHash
        - documentArtifactId
        - documentReadyAt
        - id
        - createdAt
        - updatedAt
    Employee:
      type: object
      properties: {}
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Access token JWT. Use as Authorization: Bearer <token>. In the API
        playground, paste the JWT only.
    fluideApiKey:
      type: apiKey
      in: header
      name: X-Fluide-Api-Key
      description: >-
        Developer API key (fl_dev_...). Required on every API call with a
        machine access token.
      x-default: fl_dev_your_key
    fluideClientId:
      type: apiKey
      in: header
      name: X-Fluide-Client-Id
      description: >-
        First-party client audience. Must match the fluide_client_id claim on
        the JWT. Use fluide-developer for Connect.
      x-default: fluide-developer

````