{
  "openapi": "3.0.1",
  "info": {
    "title": "Fontes.Portal.Api | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://figureapi.win4.mwx.ee/"
    }
  ],
  "paths": {
    "/v1/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Liveness probe",
        "description": "Returns 200 when the API is reachable. No authentication required.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/job-family-catalog": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "Return the active job-family catalog",
        "description": "Returns the catalog version flagged for API export, including products, families, and levels, with localized names.",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobFamilyCatalogResponse"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error\n- `catalog_not_configured` — Catalog not configured",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Unauthorized",
            "content": {
              "application/problem+json": { }
            }
          },
          "429": {
            "description": "`rate_limit_exceeded` — Rate limit exceeded",
            "content": {
              "application/problem+json": { }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "csharp",
            "label": "C# (HttpClient)",
            "source": "var res = await client.GetAsync(\"/v1/job-family-catalog?lang=en\");\nvar body = await res.Content.ReadFromJsonAsync<JobFamilyCatalogResponse>();"
          },
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -H 'Authorization: Bearer <token>' 'https://api.fontes.ee/v1/job-family-catalog?lang=en'"
          }
        ]
      }
    },
    "/v1/salary-rows": {
      "post": {
        "tags": [
          "Salary rows"
        ],
        "summary": "Merge salary rows for a customer",
        "description": "Upserts the supplied rows by employee code (or the (code, groupLegalNames) composite for multi-entity data) into every survey the customer is currently eligible for. Only the fields you send are updated; untouched rows and untouched fields on the server are preserved. `code` is required on every row.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SalaryRowImportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalaryRowImportResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request\n- `validation_failed` — Validation failed\n- `idempotency_key_required` — Idempotency-Key required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found\n- `customer_not_found` — Customer not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity\n- `idempotency_key_reused` — Idempotency-Key reused with different body\n- `no_eligible_survey` — No eligible survey",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Unauthorized",
            "content": {
              "application/problem+json": { }
            }
          },
          "429": {
            "description": "`rate_limit_exceeded` — Rate limit exceeded",
            "content": {
              "application/problem+json": { }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "csharp",
            "label": "C# (HttpClient)",
            "source": "client.DefaultRequestHeaders.Authorization = new(\"Bearer\", token);\nclient.DefaultRequestHeaders.Add(\"Idempotency-Key\", Guid.NewGuid().ToString());\nvar body = new { customerRegNo = \"12345678\", rows = new[] { new { code = \"E-1001\", monthlyGrossSalary = \"2500\" } } };\nvar res = await client.PostAsJsonAsync(\"/v1/salary-rows\", body);"
          },
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST https://api.fontes.ee/v1/salary-rows \\\n  -H 'Authorization: Bearer <token>' \\\n  -H 'Idempotency-Key: <new-guid>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"customerRegNo\":\"12345678\",\"rows\":[{\"code\":\"E-1001\",\"monthlyGrossSalary\":\"2500\"}]}'"
          }
        ]
      }
    },
    "/v1/salary-rows/replace": {
      "post": {
        "tags": [
          "Salary rows"
        ],
        "summary": "Replace all salary rows for a customer",
        "description": "DESTRUCTIVE: Hard-deletes the existing salary_row set for every eligible (survey, participant) pair, then stores every supplied row fresh — identical to the portal's Excel upload, including storing invalid rows flagged as such (they are also listed in rejectedRows). Use when 'this list is the complete truth'. Safety guard: an empty rows array is skipped (the pair is returned as skipped and nothing is deleted); any non-empty payload proceeds. `code` is required on every row.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SalaryRowImportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalaryRowImportResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request\n- `validation_failed` — Validation failed\n- `idempotency_key_required` — Idempotency-Key required",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found\n- `customer_not_found` — Customer not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity\n- `idempotency_key_reused` — Idempotency-Key reused with different body\n- `no_eligible_survey` — No eligible survey",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Unauthorized",
            "content": {
              "application/problem+json": { }
            }
          },
          "429": {
            "description": "`rate_limit_exceeded` — Rate limit exceeded",
            "content": {
              "application/problem+json": { }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "csharp",
            "label": "C# (HttpClient)",
            "source": "client.DefaultRequestHeaders.Authorization = new(\"Bearer\", token);\nclient.DefaultRequestHeaders.Add(\"Idempotency-Key\", Guid.NewGuid().ToString());\nvar body = new { customerRegNo = \"12345678\", rows = new[] { new { code = \"E-1001\", monthlyGrossSalary = \"2500\" } } };\nvar res = await client.PostAsJsonAsync(\"/v1/salary-rows/replace\", body);"
          },
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST https://api.fontes.ee/v1/salary-rows/replace \\\n  -H 'Authorization: Bearer <token>' \\\n  -H 'Idempotency-Key: <new-guid>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"customerRegNo\":\"12345678\",\"rows\":[{\"code\":\"E-1001\",\"monthlyGrossSalary\":\"2500\"}]}'"
          }
        ]
      }
    },
    "/v1/auth/token": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Exchange an API key for a short-lived JWT",
        "description": "Submit your `X-Api-Key`. On success, returns a 15-minute bearer token. Use this token in `Authorization: Bearer <token>` on all other endpoints.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthTokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized\n- `unauthorized` — Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limit_exceeded` — Rate limit exceeded",
            "content": {
              "application/problem+json": { }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "csharp",
            "label": "C# (HttpClient)",
            "source": "using var client = new HttpClient { BaseAddress = new Uri(\"https://api.fontes.ee\") };\nclient.DefaultRequestHeaders.Add(\"X-Api-Key\", apiKey);\nvar res = await client.PostAsync(\"/v1/auth/token\", null);\nvar body = await res.Content.ReadFromJsonAsync<AuthTokenResponse>();"
          },
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST https://api.fontes.ee/v1/auth/token -H 'X-Api-Key: <your-api-key>'"
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "AuthTokenResponse": {
        "required": [
          "accessToken",
          "tokenType",
          "expiresIn"
        ],
        "type": "object",
        "properties": {
          "accessToken": {
            "type": "string"
          },
          "tokenType": {
            "type": "string"
          },
          "expiresIn": {
            "type": "integer",
            "format": "int32"
          }
        }
      },
      "JfFamilyDto": {
        "required": [
          "name",
          "description",
          "levels"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "levels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JfLevelDto"
            }
          }
        }
      },
      "JfLevelDto": {
        "required": [
          "level",
          "name",
          "description",
          "pointGroup"
        ],
        "type": "object",
        "properties": {
          "level": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "pointGroup": {
            "$ref": "#/components/schemas/JfPointGroupDto"
          }
        }
      },
      "JfPointGroupDto": {
        "required": [
          "minValue",
          "midValue",
          "maxValue"
        ],
        "type": "object",
        "properties": {
          "minValue": {
            "type": "integer",
            "format": "int16"
          },
          "midValue": {
            "type": "integer",
            "format": "int16"
          },
          "maxValue": {
            "type": "integer",
            "format": "int16"
          }
        },
        "nullable": true
      },
      "JfProductDto": {
        "required": [
          "productId",
          "productName",
          "productAbbr",
          "families"
        ],
        "type": "object",
        "properties": {
          "productId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "productName": {
            "type": "string",
            "nullable": true
          },
          "productAbbr": {
            "type": "string",
            "nullable": true
          },
          "families": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JfFamilyDto"
            }
          }
        }
      },
      "JobFamilyCatalogResponse": {
        "required": [
          "versionId",
          "versionName",
          "language",
          "products"
        ],
        "type": "object",
        "properties": {
          "versionId": {
            "type": "integer",
            "format": "int32"
          },
          "versionName": {
            "type": "string",
            "nullable": true
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JfProductDto"
            }
          }
        }
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "RowMessageDto": {
        "required": [
          "field",
          "code",
          "message"
        ],
        "type": "object",
        "properties": {
          "field": {
            "type": "string",
            "nullable": true
          },
          "code": {
            "type": "string",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "SalaryRowDto": {
        "required": [
          "employeeExtId",
          "groupLegalNames",
          "department",
          "subdepartment",
          "unitName",
          "county",
          "title",
          "genderValue",
          "code",
          "workLoad",
          "jobFamily",
          "jobFamilyLevel",
          "jobFamilyLevelBias",
          "internationalDuties",
          "valuePoints",
          "internalPoints",
          "hourSalary",
          "monthlyGrossSalary",
          "fixedAnnualBonuses",
          "nonfixedPreannualBonuses",
          "otherMonetaryPayments",
          "preannualMonthlyGrossSalary",
          "preannualGrossTotal",
          "personJobMatchesLastYear",
          "monthlyCarCompensation",
          "employeeBirthyear",
          "employeeRecruited",
          "fixedBonuses",
          "offTimeExtra",
          "salesBonus",
          "longTermBonus",
          "variableShortTermIncentives",
          "shortTermSalesIncentives",
          "salesBonusTarget",
          "salesBonusMax",
          "annualBonusTarget",
          "annualBonusMax",
          "workArr",
          "itLanguage",
          "internalRangeMin",
          "internalRangeMax",
          "performance"
        ],
        "type": "object",
        "properties": {
          "employeeExtId": {
            "type": "string",
            "nullable": true
          },
          "groupLegalNames": {
            "type": "string",
            "nullable": true
          },
          "department": {
            "type": "string",
            "nullable": true
          },
          "subdepartment": {
            "type": "string",
            "nullable": true
          },
          "unitName": {
            "type": "string",
            "nullable": true
          },
          "county": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "genderValue": {
            "type": "string",
            "nullable": true
          },
          "code": {
            "type": "string",
            "nullable": true
          },
          "workLoad": {
            "type": "string",
            "nullable": true
          },
          "jobFamily": {
            "type": "string",
            "nullable": true
          },
          "jobFamilyLevel": {
            "type": "string",
            "nullable": true
          },
          "jobFamilyLevelBias": {
            "type": "string",
            "nullable": true
          },
          "internationalDuties": {
            "type": "string",
            "nullable": true
          },
          "valuePoints": {
            "type": "integer",
            "format": "int16",
            "nullable": true
          },
          "internalPoints": {
            "type": "string",
            "nullable": true
          },
          "hourSalary": {
            "type": "string",
            "nullable": true
          },
          "monthlyGrossSalary": {
            "type": "string",
            "nullable": true
          },
          "fixedAnnualBonuses": {
            "type": "string",
            "nullable": true
          },
          "nonfixedPreannualBonuses": {
            "type": "string",
            "nullable": true
          },
          "otherMonetaryPayments": {
            "type": "string",
            "nullable": true
          },
          "preannualMonthlyGrossSalary": {
            "type": "string",
            "nullable": true
          },
          "preannualGrossTotal": {
            "type": "string",
            "nullable": true
          },
          "personJobMatchesLastYear": {
            "type": "string",
            "nullable": true
          },
          "monthlyCarCompensation": {
            "type": "string",
            "nullable": true
          },
          "employeeBirthyear": {
            "type": "string",
            "nullable": true
          },
          "employeeRecruited": {
            "type": "string",
            "nullable": true
          },
          "fixedBonuses": {
            "type": "string",
            "nullable": true
          },
          "offTimeExtra": {
            "type": "string",
            "nullable": true
          },
          "salesBonus": {
            "type": "string",
            "nullable": true
          },
          "longTermBonus": {
            "type": "string",
            "nullable": true
          },
          "variableShortTermIncentives": {
            "type": "string",
            "nullable": true
          },
          "shortTermSalesIncentives": {
            "type": "string",
            "nullable": true
          },
          "salesBonusTarget": {
            "type": "string",
            "nullable": true
          },
          "salesBonusMax": {
            "type": "string",
            "nullable": true
          },
          "annualBonusTarget": {
            "type": "string",
            "nullable": true
          },
          "annualBonusMax": {
            "type": "string",
            "nullable": true
          },
          "workArr": {
            "type": "string",
            "nullable": true
          },
          "itLanguage": {
            "type": "string",
            "nullable": true
          },
          "internalRangeMin": {
            "type": "string",
            "nullable": true
          },
          "internalRangeMax": {
            "type": "string",
            "nullable": true
          },
          "performance": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "SalaryRowImportRequest": {
        "required": [
          "customerRegNo",
          "rows"
        ],
        "type": "object",
        "properties": {
          "customerRegNo": {
            "type": "string",
            "nullable": true
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SalaryRowDto"
            }
          }
        }
      },
      "SalaryRowImportResponse": {
        "required": [
          "customerRegNo",
          "division",
          "mode",
          "results"
        ],
        "type": "object",
        "properties": {
          "customerRegNo": {
            "type": "string",
            "nullable": true
          },
          "division": {
            "type": "string",
            "nullable": true
          },
          "mode": {
            "type": "string",
            "nullable": true
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SurveyResultDto"
            }
          }
        }
      },
      "SurveyResultDto": {
        "required": [
          "surveyId",
          "surveyName",
          "jobFamilyVersionName",
          "participantId",
          "acceptedRows",
          "savedRows",
          "deletedRows",
          "validationIssues",
          "warnings",
          "skipped",
          "skippedReason"
        ],
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "integer",
            "format": "int32"
          },
          "surveyName": {
            "type": "string",
            "nullable": true
          },
          "jobFamilyVersionName": {
            "type": "string",
            "nullable": true
          },
          "participantId": {
            "type": "integer",
            "format": "int32"
          },
          "acceptedRows": {
            "type": "integer",
            "format": "int32"
          },
          "savedRows": {
            "type": "integer",
            "format": "int32"
          },
          "deletedRows": {
            "type": "integer",
            "format": "int32"
          },
          "validationIssues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ValidationIssueDto"
            }
          },
          "warnings": {
            "type": "array",
            "items": { }
          },
          "skipped": {
            "type": "boolean"
          },
          "skippedReason": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ValidationIssueDto": {
        "required": [
          "rowIndex",
          "code",
          "employeeRef",
          "errors",
          "warnings"
        ],
        "type": "object",
        "properties": {
          "rowIndex": {
            "type": "integer",
            "format": "int32"
          },
          "code": {
            "type": "string",
            "nullable": true
          },
          "employeeRef": {
            "type": "string",
            "nullable": true
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RowMessageDto"
            }
          },
          "warnings": {
            "type": "array",
            "items": { }
          }
        }
      }
    },
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "description": "JWT issued by POST /v1/auth/token",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      },
      "ApiKeyAuth": {
        "type": "apiKey",
        "description": "API key issued by Fontes Backend admin. Submit to /v1/auth/token in exchange for a JWT.",
        "name": "X-Api-Key",
        "in": "header"
      }
    }
  },
  "security": [
    {
      "BearerAuth": [ ]
    }
  ],
  "tags": [
    {
      "name": "System"
    },
    {
      "name": "Catalog"
    },
    {
      "name": "Salary rows"
    },
    {
      "name": "Auth"
    }
  ]
}