## Skilagrein — Implementation Guide for Collectors and Payroll System Developers

> This developer guide is for:
- **collectors** (innheimtuaðilar) who receive fund payment submissions from payroll systems and maintain the web services used for communication with payroll systems.
- **payroll system developers** who send fund payment submissions to collectors and maintain integrations with skilagrein.is and collector web services.
>

---

## 1. Overview

This guide applies to version 2.0, which is based on a JSON REST API and replaces the legacy XML format (version 1.0). The legacy format will be phased out by collectors following this upgrade.

A **collector** is responsible for:

- Exposing a **well-known discovery endpoint** so payroll systems can discover APIs automatically.
- Implementing an **API** to receive and process submissions.
- Issuing **OAuth 2.0 client credentials** to employers who are approved users of the collector.
- Returning **standardised responses** so payroll systems can handle results consistently.

The two OpenAPI specifications are:

| Spec | Purpose |
| --- | --- |
| `service-discovery.yaml` | Discovery — tells payroll systems where the API lives and how to authenticate |
| `fund-submissions.yaml` | Core API — receiving, validating, and acknowledging fund payment submissions |

---

## 2. Architecture

```
Payroll System                            Collector
     │                                        │
     │  GET /.well-known/skilagrein-config    │
     │ ─────────────────────────────────────► │  (no auth required)
     │ ◄───────────────────────────────────── │
     │  { endpoint, tokenUrl, scope, ... }    │
     │                                        │
     │  POST {tokenUrl}                       │
     │  client_credentials grant             │
     │ ─────────────────────────────────────► │  Auth Server
     │ ◄───────────────────────────────────── │
     │  { access_token, expires_in, ... }     │
     │                                        │
     │  POST {endpoint}/fund-payments         │
     │  Authorization: Bearer <token>         │
     │ ─────────────────────────────────────► │
     │ ◄───────────────────────────────────── │
     │  201 / 400 / 422 + FundPaymentResponse │
```

---

## 3. Discovery Endpoint — Well-Known Configuration

Payroll systems locate the API by fetching a well-known configuration document. A collector must expose this endpoint at:

```
GET /.well-known/skilagrein-configuration
```

This endpoint must be publicly accessible — **no authentication required**.

### 3.1 Required Response Fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `schemaVersion` | string | ✅ | Version of the configuration schema (currently `"1.0"`) |
| `collectorId` | string | ✅ | The collector's SAL number |
| `apiVersions` | array | ✅ | At least one API version entry (see below) |

Each entry in `apiVersions` must include:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `apiVersion` | string | ✅ | API version string, e.g. `"1.0"` |
| `validFrom` | date | ✅ | Date this version became active |
| `validTo` | date/null | — | Expiry date, `null` if still active |
| `endpoint` | URI | ✅ | Base URL of the Fund Payments API |
| `validationEndpoint` | URI | — | URL of the optional validation endpoint for this API version. Omit if not supported |
| `openApiUrl` | URI | ✅ | URL to the collector's published OpenAPI spec |
| `authentication` | object | ✅ | OAuth configuration (see Section 4) |

### 3.2 Example Response

```json
{
  "schemaVersion": "1.0",
  "collectorId": "1234",
  "apiVersions": [
    {
      "apiVersion": "1.0",
      "validFrom": "2026-01-01",
      "validTo": null,
      "endpoint": "https://api.example.is/v1/fund-payments",
      "validationEndpoint": "https://api.example.is/v1/fund-payments/validation",
      "openApiUrl": "https://api.example.is/v1/openapi.json",
      "authentication": {
        "type": "oauth2_client_credentials",
        "tokenUrl": "https://auth.example.is/connect/token",
        "scope": "skilagrein",
        "credentialContact": {
          "description": "Contact us to obtain OAuth client credentials (client ID and secret)",
          "email": "api@example.is",
          "url": "https://developer.example.is/access"
        }
      }
    }
  ]
}
```

> **Note:** If the validation endpoint is not implemented, simply omit the `validationEndpoint` field from the corresponding `apiVersions` entry.
> 

---

## 4. Authentication

The technical specification offers three authentication methods: `[oauth, basic, none]`. It is up to the fund collector to decide what they use.

> It is strongly recommended that all calls to the Fund Payments API be authenticated using **OAuth 2.0 Client Credentials** (`client_credentials` grant). If `basic (user+pass)` then it should be put in the header.
>

This section describes what a collector must implement and what the flow looks like from a payroll system's perspective, given that the collector uses **OAuth 2.0 Client Credentials** (`client_credentials` grant) 


### 4.1 What the Collector Must Implement

The collector acts as both the **resource server** and the authority issuing **client credentials** to employers (payroll systems). A collector may run its own authorization server or delegate to an identity provider, but the external interface must conform to the standard `client_credentials` grant.

**The authorization server must:**

- Accept a `POST` request to the `tokenUrl` published in the well-known configuration.
- Support the `client_credentials` grant type.
- Require a `scope` value of `skilagrein` (or another scope defined and published by the collector).
- Return a standard OAuth 2.0 token response including `access_token` and `expires_in`.
- Issue tokens as **Bearer tokens** (typically JWT, but the format is up to the collector) that the Fund Payments API can validate on every request.

**The Fund Payments API must:**

- Reject requests that are missing an `Authorization` header with a `401 Unauthorized` response.
- Reject expired or invalid tokens with a `401 Unauthorized` response.
- Reject valid tokens that lack the required scope with a `403 Forbidden` response.
- Accept requests with a valid Bearer token.

### 4.2 Credential Issuance

Payroll systems read the `credentialContact` in the well-known document and contact the collector to request API access. The process should cover:

- Verifying the identity of the requesting operator.
- Issuing a `client_id` and `client_secret` pair.
- Communicating the `tokenUrl` and `scope` to be used.

The `credentialContact` object in the well-known configuration should always include at minimum a `description` field. Including `email` and/or `url` is also recommended:

```json
"credentialContact": {
  "description": "Contact us to obtain OAuth client credentials (client ID and secret)",
  "email": "api@example.is",
  "url": "https://developer.example.is/access"
}
```

### 4.3 Token Request (Payroll System Perspective)

Payroll systems obtain a token by sending a standard client credentials request to the `tokenUrl`:

```
POST https://auth.example.is/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<client_id>
&client_secret=<client_secret>
&scope=skilagrein
```

The server should respond with:

```json
{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "skilagrein"
}
```

### 4.4 Authenticated API Calls

Payroll systems include the access token as a Bearer token on every API call:

```
POST https://api.example.is/v1/fund-payments
Authorization: Bearer eyJhbGci...
Content-Type: application/json

{ ... }
```

### 4.5 Token Expiry and Renewal

- Tokens expire after `expires_in` seconds. Payroll systems are expected to request a new token before the current one expires.
- The API should return `401 Unauthorized` for expired tokens. Payroll systems will re-authenticate automatically on receiving a `401`.
- Do not return `200 OK` with an error body for authentication failures — the correct HTTP status code should be used instead.

### 4.6 Security Recommendations

- Use HTTPS exclusively — reject plain HTTP connections.
- Use short-lived access tokens (5 minutes is a common default).
- Scope tokens tightly — the `skilagrein` scope should grant only the permissions needed for submission.
- Have a process in place to rotate client credentials quickly if a secret is compromised, and provide operators with a self-service or contact mechanism for rotation.

---

## 5. Fund Payments API

### 5.1 `POST /fund-payments` — Submit

Submit a fund payment declaration (skilagrein) for processing.

```
POST {endpoint}/fund-payments
Authorization: Bearer <token>
Content-Type: application/json
```

### HTTP Status Codes

| Status | Meaning |
| --- | --- |
| `201 Created` | Submission received. See `response` field in body for outcome (`ACCEPTED` or `ACCEPTED_WITH_COMMENTS`). |
| `400 Bad Request` | Malformed request — structural or format error before business validation. |
| `422 Unprocessable Entity` | Submission rejected due to validation errors. See `issues` array in response. |

> Note: A `201` does not automatically mean the submission was fully accepted — the `response` field in the body must always be checked.
> 

---

### 5.2 `POST /fund-payments/validation` — Validate (Optional)

Validation endpoint (dry-run): validates the submission without processing it or posting entitlements. Useful for payroll systems to check for errors before an actual submission is made.

This endpoint is **optional**. If implemented, the `validationEndpoint` is included in the well-known configuration.

The request and response structure is identical to `POST /fund-payments`.

---

### 5.3 `DELETE /fund-payments/{transactionId}` — Reverse a Submission

Reverse (cancel) a previously submitted fund payment in full by `transactionId`. This replaces sending a negative (mirroring) submission to undo a whole submission.

```
DELETE {endpoint}/fund-payments/{transactionId}
Authorization: Bearer <token>
```

Funds may impose their own business rules on whether a reversal is allowed (e.g. once a fund payment has been booked, reversal may be rejected). For partial reversal, send a correcting (negative) submission via `POST /fund-payments`.

### HTTP Status Codes

| Status | Meaning |
| --- | --- |
| `200 OK` | Fund payment was reversed. `response` field in body is `REVERSED`. |
| `403 Forbidden` | Caller is not authorized to reverse this fund payment. |
| `404 Not Found` | No fund payment exists for the provided `transactionId`. |
| `409 Conflict` | Fund payment cannot be reversed (already booked or already reversed). See `issues` in body for details. |

---

### 5.4 `GET /fund-entities` — Supported Funds

Returns the list of funds the collector collects for, along with the entity types, default ratios (percentages) and dynamic additional fields (`additionalAttributes`) available for each fund.

```
GET {endpoint}/fund-entities
Authorization: Bearer <token>
```

Payroll systems can use this information to update fund-specific settings.


#### Fund-specific fields (`additionalAttributes`)

Instead of fixed, fund-specific fields for B-department funds (or other special funds), a general, dynamic mechanism is used: each fund declares in `entityTypeRules[].additionalAttributes` which extra fields it accepts and how they are validated. Payroll systems carry the values of those fields on `paymentEntry.additionalAttributes` as `{ name, value }` pairs.

This is intentionally general — it is not B-department-specific. Other associations or funds can later declare their own additional fields (e.g. due to new collective agreements) without changes to the spec.

Each rule in `additionalAttributes` has the following fields:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | ✅ | Attribute name. Must match the `name` used on `paymentEntry.additionalAttributes`. |
| `type` | enum | ✅ | Logical type of the value: `string`, `number`, `boolean` or `date`. The collector parses the string accordingly. |
| `description` | string | — | Human-readable description, suitable for display in a payroll system. |
| `required` | boolean | — | Whether the attribute must be present on every matching `paymentEntry`. Defaults to `false`. |
| `allowedValues` | array | — | Optional closed set of allowed values. When present, `value` must be one of them. |

### Example Response

```json
[
  {
    "entityNo": "1005",
    "entityTypeRules": [
      {
        "entityType": "L",
        "defaultPercentage": 0.12,
        "additionalAttributes": [
          { "name": "salarySymbol", "type": "string", "description":"Launatákn", "allowedValues": ["001", "B", "V", "032"] }
        ]
      },
      {
        "entityType": "F",
        "defaultPercentage": 0.01,
        "additionalAttributes": [
          { "name": "daysAtSea", "type": "number", "description": "Dagar á sjó" }
        ]
      }
    ]
  }
]
```

#### Example: B-department fund

A B-department fund can declare the following fund-specific fields on its entity type — including fields that previously were fixed in the spec:

```json
{
  "entityNo": "1234",
  "entityTypeRules": [
    {
      "entityType": "L",
      "defaultPercentage": 0.155,
      "additionalAttributes": [
        { "name": "salarySymbol",      "type": "string", "required": true,  "allowedValues": ["001", "B", "V", "032"], "description": "Salary symbol" },
        { "name": "employmentRatio",   "type": "number", "required": true,  "description": "Employment ratio" },
        { "name": "salaryTable",       "type": "string", "description": "Salary table" },
        { "name": "salaryCategory",    "type": "string", "description": "Salary category" },
        { "name": "salarySubCategory", "type": "string", "description": "Salary subcategory" },
        { "name": "additionalAmount",  "type": "number", "description": "Tied private savings (bundin séreign)" }
      ]
    }
  ]
}
```

The payroll system sends matching values on `paymentEntry.additionalAttributes`:

```json
{
  "entityType": "L",
  "entityNo": "1234",
  "amount": 50000,
  "amountPayrollPercentage": 0.155,
  "additionalAttributes": [
    { "name": "salarySymbol",      "value": "001" },
    { "name": "employmentRatio",   "value": "1.0" },
    { "name": "salaryTable",       "value": "A" },
    { "name": "salaryCategory",    "value": "CAT1" },
    { "name": "salarySubCategory", "value": "SUB1" },
    { "name": "additionalAmount",  "value": "0" }
  ]
}
```

Validation rules for additional fields (see also section 8):

- A field marked `required: true` must be present on every matching `paymentEntry`.
- `value` must be parseable as the declared `type` (e.g. a `number` field must hold a valid number).
- When `allowedValues` is set, `value` must be one of those values.
- Fields not declared in the fund's `additionalAttributes` rules should be ignored or raise a `warning`.

---

## 6. Request Structure Reference

### 6.1 Top-Level: `FundPaymentSubmission`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `transactionId` | string | ✅ | Unique ID assigned by a payroll system for this submission. Used to correlate responses. |
| `employerNationalId` | string | ✅ | Employer kennitala — exactly 10 digits, no hyphen. |
| `currency` | string | ✅ | ISO 4217 currency code (3 uppercase letters). Default is `ISK`. For ISK, amounts are integers (no decimal places). |
| `paymentEntryGroups` | array | ✅ | One group per employee. At least 1 item. |
| `summaries` | array | ✅ | Totals per fund and entity type. |

### 6.2 `PaymentEntryGroup`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `nationalId` | string | ✅ | Employee kennitala — exactly 10 digits, no hyphen. |
| `employeeTransactionRef` | string | ✅ | Unique reference for this employee within the submission. Returned in issue messages to identify errors without exposing the employee's national ID in responses. |
| `periodFrom` | date | ✅ | Start date of pay period (`YYYY-MM-DD`), belongs to the period being reported. |
| `periodTo` | date | ✅ | End date of pay period (`YYYY-MM-DD`), belongs to the period being reported. |
| `paymentEntries` | array | ✅ | Payment entries. |


### 6.3 `PaymentEntry` 

Represents payment entries for each employee broken down by entity type. Note that the entity type has been split so that the employee and employer contributions are separated into two entries, each with its own `entityType`, whereas the old model combined these into a single line.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `entityType` | string | ✅ | Short identifier for the type of entity (e.g. `L1` for employee pension contribution, `F1` for employee union due, etc.). Use `GET /fund-entities` to retrieve valid entity types. |
| `entityNo` | string | ✅ | Unique fund number (SAL). |
| `amount` | number | ✅ | Amount in the declared currency. |
| `amountPayrollPercentage` | number | ✅ | Amount as a percentage of the payroll base (decimal, e.g. `0.12` = 12%). |
| `date` | date | — | Payment date. Optional. |
| `additionalAttributes` | array | — | Dynamic fund-specific fields as `{ name, value }` pairs (e.g. `salarySymbol`, `employmentRatio`, `daysAtSea`, B-department fields). The fund declares which fields it supports and their validation rules via `entityTypeRules[].additionalAttributes` returned from `GET /fund-entities`. See section 5.4 for a B-department example. |

### 6.4 `Summary`

Contribution sums are aggregated in these fields by entity type and fund number, for cross-checking of totals.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `entityNo` | string | ✅ | Fund number (SAL). |
| `entityType` | string | ✅ | Entity type identifier. |
| `amountSum` | number | ✅ | Sum of all `amount` values across all entries for this `entityNo` + `entityType` combination. |
| `entityName` | string | — | Human-readable fund name. Optional. |

---

## 7. Response Structure Reference

### 7.1 `FundPaymentResponse`

Response in `POST /fund-payments`, `POST /fund-payments/validation` and `DELETE /fund-payments/{transactionId}` calls.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `transactionId` | string | ✅ | Echoes back the `transactionId` from the submission. |
| `responseId` | string | ✅ | The collector's unique reference for this response. |
| `response` | enum | ✅ | Outcome — see values below. |
| `postedAmount` | number | ✅ | Total amount posted. |
| `issues` | array | ✅ | Empty when `ACCEPTED`. Contains warnings or errors otherwise. |

### 7.2 Response Outcomes

| `response` value | HTTP Status | Meaning |
| --- | --- | --- |
| `ACCEPTED` | `201` | Submission fully accepted. `issues` array is empty. |
| `ACCEPTED_WITH_COMMENTS` | `201` | Accepted but with warnings or informational comments. Check `issues`. |
| `REJECTED` | `422` | Submission rejected. `issues` contains the errors. |
| `REVERSED` | `200` | Submission was reversed in full via `DELETE /fund-payments/{transactionId}`. |

### 7.3 `Issue`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `severity` | enum | ✅ | `"warning"` or `"error"` |
| `message` | string | ✅ | Human-readable description of the issue. |
| `employeeTransactionRef` | string | — | Reference to the affected employee entry. Present when the issue is tied to a specific employee line. Should **not** contain the employee's national ID. |
| `entityType` | string | — | Entity type involved in the issue, if applicable. |
| `entityNo` | string | — | Fund number involved in the issue, if applicable. |

> **Design note:** `employeeTransactionRef` is used deliberately instead of `nationalId` to avoid transmitting personal data in error messages. The quality of error messages depends on whether the payroll system included an `employeeTransactionRef` in the submission.
> 

---

## 8. Validation Rules

These guidelines serve as minimum requirements for validation of fund payment submissions. Each collector and fund may add further rules that apply during validation and/or during receipt and posting of submissions.

### Structural

- `employerNationalId` must be exactly 10 digits, no hyphen.
- `nationalId` (employee) must be exactly 10 digits, no hyphen.
- `currency` must be a valid ISO 4217 3-letter code.
- `periodFrom` must be before or equal to `periodTo`.
- `paymentEntries` must contain at least 1 entry.
- `paymentEntryGroups` must contain at least 1 entry.

### Business Rules

- Both `periodFrom` and `periodTo` must fall within the same reporting period.
- Pay periods spanning a year boundary are generally not permitted — validation must follow the collector's own rules.
- `amountPayrollPercentage` should match the expected percentage for the `entityType` as defined in the collector's `GET /fund-entities` response.
- `summaries` totals must match the sum of the corresponding entries across all groups.
- `entityType` and `entityNo` combinations submitted must be in the set that the collector advertises via `GET /fund-entities`.
- Percentage values are given as decimals in the range 0–1 (e.g. 0.5 = 50%).

### Additional fields (`additionalAttributes`)

Fund-specific additional fields (e.g. `salarySymbol`, `employmentRatio`, B-department fields, `daysAtSea`) are validated against the rules the fund advertises in `entityTypeRules[].additionalAttributes` from `GET /fund-entities` — see section 5.4 for the detailed shape and examples.

- `paymentEntry.additionalAttributes[].name` must match a `name` advertised by the fund for the corresponding `entityNo` + `entityType`.
- Fields marked `required: true` must be present on every matching `paymentEntry`.
- `value` must be parseable as the declared `type` (`string`, `number`, `boolean` or `date`).
- When `allowedValues` is set, `value` must be one of those values.
- Fields not declared in the fund's rules should be ignored or raise a `warning`.

---

## 9. Error Handling

Use standard HTTP status codes. `200 OK` should not be returned with an error payload.

| Scenario | HTTP Status | `response` field |
| --- | --- | --- |
| Fully accepted | `201` | `ACCEPTED` |
| Accepted with warnings | `201` | `ACCEPTED_WITH_COMMENTS` |
| Validation errors (business rules) | `422` | `REJECTED` |
| Malformed JSON or missing required fields | `400` | `REJECTED` |
| Reversal accepted | `200` | `REVERSED` |
| Reversal — submission not found | `404` | — (body optional) |
| Reversal — already booked or already reversed | `409` | `REJECTED` (detail in `issues`) |
| Missing or invalid Bearer token | `401` | — (no body required) |
| Valid token but insufficient scope | `403` | — (no body required) |
| Internal server error | `500` | — |

For `400` and `422` responses, always return a `FundPaymentResponse` body with the `issues` array populated. For `401` and `403`, a body is optional; if included, keep it minimal to avoid leaking information about why authentication failed. For `5xx`, a body is not required.

---

## 10. Implementation Checklist

This checklist may be used as a support tool during the upgrade. Note that it is not exhaustive and is provided only as a guide.

### Discovery

- [ ]  `GET /.well-known/skilagrein-configuration` returns a valid `CollectorConfiguration` object
- [ ]  Endpoint is publicly accessible (no authentication required)
- [ ]  `collectorId` is the collector's identifier (SAL)
- [ ]  `apiVersions` contains at least one entry with `validFrom` set
- [ ]  `endpoint` points to a live Fund Payments API
- [ ]  `openApiUrl` points to the published OpenAPI spec
- [ ]  `authentication` block is complete and correct (see below)
- [ ]  `validationEndpoint` is included if the validation endpoint is implemented, otherwise omitted

### Authentication

- [ ]  OAuth 2.0 authorization server is running and reachable at the published `tokenUrl`
- [ ]  `client_credentials` grant type is supported
- [ ]  The `skilagrein` scope (or an equivalent) is required and enforced
- [ ]  Token response includes `access_token`, `token_type: "Bearer"`, and `expires_in`
- [ ]  Fund Payments API validates Bearer tokens on every request
- [ ]  `401` is returned for missing, expired, or invalid tokens
- [ ]  `403` is returned for valid tokens lacking the required scope
- [ ]  Credential issuance process is documented in `credentialContact`
- [ ]  HTTPS is enforced on all endpoints

### Fund Payments API

- [ ]  `POST /fund-payments` accepts and processes submissions
- [ ]  `DELETE /fund-payments/{transactionId}` supports reversal and enforces the fund's rules for when reversal is allowed (`200 REVERSED` / `403` / `404` / `409`)
- [ ]  `GET /fund-entities` returns the funds and entity types supported by the collector
- [ ]  Responses use `FundPaymentResponse` schema with correct `response` enum values
- [ ]  `issues` array is populated for `ACCEPTED_WITH_COMMENTS` and `REJECTED` responses
- [ ]  `transactionId` from submission is echoed back in every response
- [ ]  `employeeTransactionRef` is referenced in per-employee issues (not `nationalId`)
- [ ]  `summaries` are cross-validated against entry totals
- [ ]  Period validation is enforced (same reporting period, no year boundary crossing)
- [ ]  `POST /fund-payments/validation` implemented (if `validationEndpoint` is advertised)
- [ ]  `entityTypeRules[].additionalAttributes` advertises fund-specific additional fields with validation rules (`name`, `type`, and where applicable `required`, `allowedValues`)
- [ ]  `paymentEntry.additionalAttributes` are validated against the fund's rules (type, `required`, `allowedValues`)
