Getting Started

Fontes Portal API lets accountant-software systems submit salary data on behalf of their clients. Three endpoints cover the complete workflow: exchange an API key for a short-lived JWT, fetch the job-family catalog so you can classify rows correctly, and push a batch of salary rows into the Fontes survey platform.

This guide walks you through the complete flow in four steps. For deeper coverage of individual topics, see the Authentication, Merge vs Replace, Idempotency, and Errors guides.


Before you start

You need an API key issued by Fontes support. The key is a long opaque string that looks like fpk_live_.... Store it in your secret manager (Azure Key Vault, AWS Secrets Manager, environment variable) — never in source code.

To request an API key, contact api-support@fontes.ee with the name of your integration and the organisation responsible for it.


Step 1 — Exchange your API key for a JWT

Every API call (except /v1/auth/token itself) requires a Bearer JWT. JWTs expire after 15 minutes, so your client should re-issue one automatically whenever a request returns 401.

using var http = new HttpClient { BaseAddress = new Uri("https://api.fontes.ee") };

// Step 1: get a token
http.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
var tokenResp = await http.PostAsync("/v1/auth/token", null);
tokenResp.EnsureSuccessStatusCode();
var token = (await tokenResp.Content.ReadFromJsonAsync<TokenResponse>())!.AccessToken;
curl -X POST https://api.fontes.ee/v1/auth/token \
  -H "X-Api-Key: fpk_live_..."

The response contains access_token (the JWT) and expires_in (seconds, always 900). There is no refresh token — simply re-call /v1/auth/token when the token expires.


Step 2 — Fetch the job-family catalog

Job-family codes are required on every salary row. Fetch the catalog once per session (or cache it for a few hours — it changes rarely).

// Step 2: get the catalog
http.DefaultRequestHeaders.Authorization = new("Bearer", token);
var catalog = await http.GetFromJsonAsync<JobFamilyCatalogResponse>("/v1/job-family-catalog");
curl https://api.fontes.ee/v1/job-family-catalog \
  -H "Authorization: Bearer <token>"

Each entry has a code (use this in rows), a human-readable name, and a surveyId that identifies which survey it belongs to.


Step 3 — Submit a salary-row batch with merge

Use POST /v1/salary-rows (the merge endpoint) as your default. It upserts rows by employee code, so sending the same data twice is safe. Every row must carry a code — it is the key the merge matches on.

// Step 3: submit rows
http.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());

var body = new
{
    customerRegNo = "12345678",
    rows = new[]
    {
        new { code = "E-1001", jobFamily = "JF001", monthlyGrossSalary = "2500" }
    }
};

var resp = await http.PostAsJsonAsync("/v1/salary-rows", body);
resp.EnsureSuccessStatusCode();
curl -X POST https://api.fontes.ee/v1/salary-rows \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"customerRegNo":"12345678","rows":[{"code":"E-1001","jobFamily":"JF001","monthlyGrossSalary":"2500"}]}'

Important — generate a new Idempotency-Key for each distinct submission. Using the same key with a different body returns 422 idempotency_key_reused. See the Idempotency guide for full details on the retry contract.


Step 4 — When to use replace instead of merge

POST /v1/salary-rows/replace deletes every existing salary row for the affected (survey, participant) pairs and then inserts what you supplied. Use it only when your source system says "this list is now the complete truth" and you want removals to propagate.

Replace is destructive. It permanently removes rows for every eligible (survey, participant) pair before inserting. If you accidentally send a partial payload with replace, rows you didn't include will be deleted. Use merge as your default; switch to replace only deliberately.

See Merge vs Replace for a side-by-side comparison and worked examples.


Next steps