Merge vs Replace

Fontes Portal API exposes two endpoints for submitting salary rows. They differ in how they handle existing data for the same (survey, participant) pair. Choosing the right one for your workflow avoids accidental data loss.


Overview

Merge (POST /v1/salary-rows) Replace (POST /v1/salary-rows/replace)
Semantics Upsert by employee code Delete then insert
Safe to resend? Yes — identical re-sends are no-ops No — a partial re-send removes rows you didn't include
Deletes rows? Never Yes, for every affected (survey, participant) pair — unless the payload is empty (rows: []), then skipped
Default choice? Yes No — use deliberately
Idempotency Required header on all POST calls Required header on all POST calls

Merge

POST /v1/salary-rows upserts each row by the employee code — the same natural key the Excel upload uses. When your data spans multiple legal entities, the composite key (code, groupLegalNames) is used instead. If a row with that key already exists for the participant, the fields you send are updated in place and fields you omit are left untouched (a partial merge). If no matching row exists, a new one is inserted. Rows already on the server that are not in your payload are never removed.

Rows that fail validation are not dropped — they are stored too (flagged invalid) and additionally listed in validationIssues, exactly as replace and the Excel upload behave. Nothing you submit is silently discarded.

code is required on every row — it is the key the merge matches on.

Worked example — two successive calls with merge

Call 1: Submit rows for Alice and Bob.

{
  "customerRegNo": "12345678",
  "rows": [
    { "code": "E-1001", "monthlyGrossSalary": "2000" },
    { "code": "E-1002", "monthlyGrossSalary": "2500" }
  ]
}

After call 1: both Alice (E-1001) and Bob (E-1002) have a row.

Call 2: Submit an updated salary for Alice only (Bob's row unchanged).

{
  "customerRegNo": "12345678",
  "rows": [
    { "code": "E-1001", "monthlyGrossSalary": "2100" }
  ]
}

After call 2: Alice's salary is updated to 2100. Bob's row is untouched, and any fields on Alice's row that you didn't send are left as they were.


Replace

POST /v1/salary-rows/replace first deletes every existing salary row for each eligible (survey, participant) pair represented in the payload, then inserts the rows you supplied. The deletion scope is per participant within the survey — if Alice has rows in two surveys, and your payload only covers one survey, only the rows in that survey are deleted.

This mirrors the portal's Excel upload exactly: all submitted rows are stored, including any that fail validation (they are persisted flagged as invalid and additionally listed in validationIssues). Importing a given dataset via the API therefore leaves the participant in the same state as uploading the equivalent Excel file.

Worked example — two successive calls with replace

Call 1: Submit rows for Alice and Bob (same payload as above).

After call 1: both Alice and Bob have a row.

Call 2: Submit an updated salary for Alice only.

After call 2: Alice's salary is updated to 2100. Bob's row is deleted.

This is the key behavioural difference. If call 2 was intended as "update Alice only", replace was the wrong choice — it removed Bob's row as a side effect.


Replace safety guard

To stop an empty request from wiping a participant's data, replace skips the destructive delete only when the payload contains no rows at all (rows: []). The call returns 200 OK with each affected (survey, participant) pair reported as "skipped": true / "skippedReason": "no_rows", and nothing is deleted.

Any non-empty payload proceeds: existing rows are deleted and the submitted rows are stored. Note that rows failing validation are still stored (flagged invalid) rather than dropped — they are also reported in validationIssues so you can fix and resend them. This matches the Excel upload, where invalid rows are saved and surfaced in the portal for correction.


Clearing rows explicitly

There is no way to bulk-clear a participant's rows by sending an empty payload — rows: [] is a safe no-op (skipped, nothing deleted). To deliberately reset all rows for a customer, contact api-support@fontes.ee; there is no self-service bulk-delete endpoint.


Skipped surveys (locked data)

Both endpoints fan out to every survey the customer participates in (in its division, not yet in distribution). If a survey's data has already been approved or the customer is not filling it, that survey is left untouched and reported in the response as skipped — it is not silently omitted:

{
  "surveyId": 150,
  "surveyName": "Palgauuring 2026",
  "skipped": true,
  "skippedReason": "participant_data_confirmed",
  "acceptedRows": 0,
  "savedRows": 0,
  "deletedRows": 0
}

skippedReason is one of:

Every result block also includes surveyName alongside surveyId so you can show the user exactly which survey a notice refers to.


Which should I use?

Use merge when:

Use replace when:

Replace is destructive. Do not use it as your default. If you accidentally send a partial payload with replace, rows not included will be permanently deleted for every affected participant in that survey.