Developer Guide
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
{
"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
validationEndpointfield from the correspondingapiVersionsentry.
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_credentialsgrant). Ifbasic (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
POSTrequest to thetokenUrlpublished in the well-known configuration. - Support the
client_credentialsgrant type. - Require a
scopevalue ofskilagrein(or another scope defined and published by the collector). - Return a standard OAuth 2.0 token response including
access_tokenandexpires_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
Authorizationheader with a401 Unauthorizedresponse. - Reject expired or invalid tokens with a
401 Unauthorizedresponse. - Reject valid tokens that lack the required scope with a
403 Forbiddenresponse. - 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_idandclient_secretpair. - Communicating the
tokenUrlandscopeto 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:
"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:
{
"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_inseconds. Payroll systems are expected to request a new token before the current one expires. - The API should return
401 Unauthorizedfor expired tokens. Payroll systems will re-authenticate automatically on receiving a401. - Do not return
200 OKwith 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
skilagreinscope 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
201does not automatically mean the submission was fully accepted — theresponsefield 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
[
{
"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:
{
"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:
{
"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: truemust be present on every matchingpaymentEntry. valuemust be parseable as the declaredtype(e.g. anumberfield must hold a valid number).- When
allowedValuesis set,valuemust be one of those values. - Fields not declared in the fund's
additionalAttributesrules should be ignored or raise awarning.
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:
employeeTransactionRefis used deliberately instead ofnationalIdto avoid transmitting personal data in error messages. The quality of error messages depends on whether the payroll system included anemployeeTransactionRefin 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
employerNationalIdmust be exactly 10 digits, no hyphen.nationalId(employee) must be exactly 10 digits, no hyphen.currencymust be a valid ISO 4217 3-letter code.periodFrommust be before or equal toperiodTo.paymentEntriesmust contain at least 1 entry.paymentEntryGroupsmust contain at least 1 entry.
Business Rules
- Both
periodFromandperiodTomust fall within the same reporting period. - Pay periods spanning a year boundary are generally not permitted — validation must follow the collector's own rules.
amountPayrollPercentageshould match the expected percentage for theentityTypeas defined in the collector'sGET /fund-entitiesresponse.summariestotals must match the sum of the corresponding entries across all groups.entityTypeandentityNocombinations submitted must be in the set that the collector advertises viaGET /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[].namemust match anameadvertised by the fund for the correspondingentityNo+entityType.- Fields marked
required: truemust be present on every matchingpaymentEntry. valuemust be parseable as the declaredtype(string,number,booleanordate).- When
allowedValuesis set,valuemust 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-configurationreturns a validCollectorConfigurationobject - Endpoint is publicly accessible (no authentication required)
-
collectorIdis the collector's identifier (SAL) -
apiVersionscontains at least one entry withvalidFromset -
endpointpoints to a live Fund Payments API -
openApiUrlpoints to the published OpenAPI spec -
authenticationblock is complete and correct (see below) -
validationEndpointis included if the validation endpoint is implemented, otherwise omitted
Authentication
- OAuth 2.0 authorization server is running and reachable at the published
tokenUrl -
client_credentialsgrant type is supported - The
skilagreinscope (or an equivalent) is required and enforced - Token response includes
access_token,token_type: "Bearer", andexpires_in - Fund Payments API validates Bearer tokens on every request
-
401is returned for missing, expired, or invalid tokens -
403is 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-paymentsaccepts 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-entitiesreturns the funds and entity types supported by the collector - Responses use
FundPaymentResponseschema with correctresponseenum values -
issuesarray is populated forACCEPTED_WITH_COMMENTSandREJECTEDresponses -
transactionIdfrom submission is echoed back in every response -
employeeTransactionRefis referenced in per-employee issues (notnationalId) -
summariesare cross-validated against entry totals - Period validation is enforced (same reporting period, no year boundary crossing)
-
POST /fund-payments/validationimplemented (ifvalidationEndpointis advertised) -
entityTypeRules[].additionalAttributesadvertises fund-specific additional fields with validation rules (name,type, and where applicablerequired,allowedValues) -
paymentEntry.additionalAttributesare validated against the fund's rules (type,required,allowedValues)