Payroll Runs
Payroll runs represent individual payroll processing cycles. Each run covers a specific period and contains payroll lines for each employee. Payroll runs are a subtype of Documents with document_type set to PRRUN.
Endpoints
List Payroll Runs
GET /api/v2/payroll-runs
Retrieves a paginated list of payroll runs.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | ID of the client account |
| from_date | date | No | Filter runs with period_start on or after this date |
| to_date | date | No | Filter runs with period_end on or before this date |
Response
{
"data": [
{
"id": 1,
"created_by_id": 7,
"created_at": "2024-03-15T10:00:00Z",
"client_account_id": 7,
"document_date": "2024-03-31",
"document_number": "3",
"period_start": "2024-03-01",
"period_end": "2024-03-31",
"uploaded_file_id": 456,
"is_draft": false,
"document_type": "PRRUN",
"run_kind": "STANDARD"
}
],
"meta": {
"page": 1,
"pages": 1,
"per_page": 100,
"records": 1
}
}
Create Payroll Run
POST /api/v2/payroll-runs
Creates a new payroll run. The run is created as a draft (is_draft: true).
When a STANDARD run is created, the system automatically seeds it with employees from active employment contracts that have is_payroll_default set to true. For each default contract, a base earning line (fixed salary or hourly pay; none for a COMMISSION contract) is created, and any configured default payroll lines and outstanding salary advances are also added. Seeding happens once: the seeded lines are ordinary lines that can be edited or deleted, and a recalculation never recreates them. After seeding, derived amounts (tax withholding, employer costs, etc.) are automatically recalculated.
A VACATION_PAYOUT run (feriepengekjøring) is instead seeded from accrued balances (fixed salary, holiday pay, and holiday-pay deduction) for the relevant earning year rather than from the default-line favourites. The earning year (opptjeningsaar) is not a request field — it is derived automatically as the year before period_end (falling back to document_date if period_end is not set), and either one is required on a VACATION_PAYOUT run.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer | Yes | The client account ID |
| document_date | date | Yes | Payment date of the run. It decides which month’s a-melding reports the run |
| document_number | string | No | Optional reference. A run is named by its period, not by a number |
| period_start | date | Yes | Start date of the payroll period |
| period_end | date | Yes | End date of the payroll period, on or after period_start |
| run_kind | string | No | Kind of run: STANDARD (default), VACATION_PAYOUT or HALF_TAX |
Example Request
{
"client_account_id": 7,
"document_date": "2024-03-31",
"document_number": "3",
"period_start": "2024-03-01",
"period_end": "2024-03-31"
}
Response
Returns the created payroll run with status 201 Created. The run’s currency is the client account’s accounting currency.
Error Responses
| Status Code | Error Code | Description |
|---|---|---|
| 400 | payroll.payroll_run_missing_dates |
period_start, period_end or document_date is missing, or period_start is after period_end |
| 400 | common.validation_error |
Unknown run_kind |
| 403 | payroll.client_account_access_denied |
No access to the client account, or client_account_id is missing |
Update Payroll Run
POST /api/v2/payroll-runs/{id}
PUT /api/v2/payroll-runs/{id}
Updates an existing payroll run. Both POST and PUT methods are accepted. Only the fields below can be changed; run_kind and client_account_id are fixed when the run is created and are ignored here.
A posted run (is_draft: false) keeps its period and payment date: changing period_start, period_end or document_date on it answers 400 with payroll.payroll_run_not_draft, because the payment date decides which month’s a-melding reported the run. Reverse the run first to change them.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| document_date | date | No | Payment date of the run |
| document_number | string | No | Optional reference |
| period_start | date | No | Start date of the payroll period |
| period_end | date | No | End date of the payroll period |
Response
Returns the updated payroll run.
Delete Payroll Run
DELETE /api/v2/payroll-runs/{id}
Deletes a payroll run. Only draft runs can be deleted (is_draft must be true).
Response
Returns 204 No Content on success. Returns 400 Bad Request if the payroll run is not a draft.
Attributes
| Attribute | Type | Description |
|---|---|---|
| id | integer | Unique identifier (read-only) |
| created_by_id | integer | ID of the creating user (read-only) |
| created_at | datetime | Creation timestamp (read-only) |
| client_account_id | integer | ID of the client account |
| document_date | date | Payment date of the run |
| document_number | string | Optional reference |
| period_start | date | Start date of the payroll period |
| period_end | date | End date of the payroll period |
| uploaded_file_id | integer | Associated voucher/file ID (read-only) |
| is_draft | boolean | Whether this is a draft run (read-only) |
| document_type | string | Always PRRUN (read-only) |
| run_kind | string | Kind of run: STANDARD (ordinary monthly run), VACATION_PAYOUT (feriepengekjøring) or HALF_TAX (December half-tax month: withholding follows the half-tax rules, while variable pay such as overtime and bonuses is withheld in full). A December run is only halved when it is created as HALF_TAX |
Relationships
The following related objects can be included using the with parameter:
payroll_lines- Employee payroll lines for this run
Payroll lines support nested relations using dot notation (e.g. with=payroll_lines.item_type):
payroll_lines.item_type- The payroll item type definitionpayroll_lines.business_partner- The employeepayroll_lines.contract- The employment contractpayroll_lines.tax_card- The employee’s tax cardpayroll_lines.garnishment- The linked garnishment
Payroll Line Attributes
| Attribute | Type | Description |
|---|---|---|
| id | integer | Unique identifier (read-only) |
| created_by_id | integer | ID of the creating user (read-only) |
| created_at | datetime | Creation timestamp (read-only) |
| document_id | integer | Parent payroll run ID (read-only) |
| business_partner_id | integer | Employee ID |
| item_type_id | integer | Payroll item type ID |
| contract_id | integer | Employment contract ID |
| tax_card_id | integer | Employee tax card ID |
| garnishment_id | integer | Linked garnishment ID (read-only) |
| description | string | Line description |
| quantity | decimal | Quantity (e.g. hours worked) |
| rate | decimal | Rate (e.g. hourly rate) |
| amount | decimal | Calculated amount |
| currency_code | string | Currency code |
| sort_order | integer | Display order |
| relation_type | string | Type of related object (read-only) |
| relation_id | integer | ID of related object (read-only) |
| is_company_cost | boolean | true for a cost of the company as a whole rather than of one employee, such as the pension premium from the provider’s invoice: an employer cost with no employment contract. Such lines appear on no payslip (read-only) |
Error Responses
| Status Code | Error Code | Description |
|---|---|---|
| 400 | payroll.payroll_run_missing_dates |
period_start, period_end or document_date is missing (create) |
| 400 | common.validation_error |
Unknown run_kind, or the client account doesn’t exist (create) |
| 400 | common.serialization_error |
Malformed request body |
| 400 | payroll.payroll_run_not_draft |
The run is posted: it can’t be deleted, and its period and payment date can’t change |
| 403 | payroll.client_account_access_denied |
No access to the client account (list, create) |
| 403 | payroll.payroll_run_access_denied |
No access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found |
Payroll run not found |
Payroll Run Items
Payroll run items are the individual salary components (earnings, deductions, employer costs) within a payroll run. When items are added, updated, or deleted, the system automatically recalculates derived amounts (tax withholding, employer costs, etc.) for all employees in the run.
List Items
GET /api/v2/payroll/runs/{document_id}/items
Retrieves a paginated list of payroll line items for a specific run. The item_type relation is included by default.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | No | Page number (default: 1) |
| per_page | integer | No | Items per page (default: 1000) |
| with | string | No | Include related resources (supported: item_type, business_partner, contract, tax_card, garnishment) |
Response
{
"data": [
{
"id": 501,
"created_by_id": 7,
"created_at": "2024-03-15T10:30:00Z",
"document_id": 1,
"business_partner_id": 56,
"item_type_id": 10,
"contract_id": 3,
"tax_card_id": 12,
"garnishment_id": null,
"description": "Fastlønn",
"quantity": "1.000000",
"rate": "45000.000000",
"amount": "45000.000000",
"currency_code": "NOK",
"sort_order": 1,
"relation_type": null,
"relation_id": null,
"is_company_cost": false,
"item_type": {
"id": 10,
"code": "FIXED_SALARY",
"name_nob": "Fastlønn",
"category": "EARNING"
}
}
],
"meta": {
"page": 1,
"pages": 1,
"per_page": 1000,
"records": 1
}
}
Add Item
POST /api/v2/payroll/runs/{document_id}/items
Adds a new payroll line item to a draft run. The item type must be active for the run’s period. Lines the payroll engine calculates (tax withholding, employer tax and the like) can’t be added by hand; the employer pension lines PENSION_EMPLOYER and AFP_PREMIUM are the exception. An earning or deduction needs an employee with an active employment contract in the client account. If this is the first item for a given employee, the employee joins the run: their base earning from the active contract (unless the new item is itself a base earning), default payroll lines and outstanding salary advances are seeded once. After adding, the system recalculates all derived amounts for the run.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| business_partner_id | integer | Yes | Employee ID |
| item_type_id | integer | Yes | Payroll item type ID |
| contract_id | integer | No | Ignored; set by the server on lines seeded from the employment contract |
| tax_card_id | integer | No | Ignored; set by the server |
| description | string | No | Line description |
| quantity | decimal | No | Quantity (e.g. hours worked) |
| rate | decimal | No | Rate (e.g. hourly rate) |
| amount | decimal | No | Amount (auto-calculated if quantity and rate are set) |
| currency_code | string | No | Currency code (default: account currency) |
| sort_order | integer | No | Display order |
Example Request
{
"business_partner_id": 56,
"item_type_id": 10,
"contract_id": 3,
"description": "Fastlønn",
"quantity": "1",
"rate": "45000",
"amount": "45000"
}
Response
Returns the created item with status 201 Created.
Error Responses
| Status Code | Description |
|---|---|
| 400 | payroll.payroll_run_not_draft: the run is posted |
| 400 | payroll.payroll_line_auto_calculated: the item type is calculated by the payroll engine |
| 400 | payroll.employee_missing_contract: an earning or deduction for an employee with no active employment contract |
| 400 | Unknown item type or one inactive for the run’s period, or a business_partner_id that does not belong to the client account |
| 403 | payroll.payroll_run_access_denied: no access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found: payroll run not found |
Update Item
POST /api/v2/payroll/runs/{document_id}/items/{item_id}
PUT /api/v2/payroll/runs/{document_id}/items/{item_id}
Updates an existing payroll line item. Both POST and PUT methods are accepted. After updating, the system recalculates all derived amounts for the run.
Lines the payroll engine calculates can’t be edited, except a manually added PENSION_EMPLOYER or AFP_PREMIUM line. A new item_type_id or business_partner_id is checked as on Add Item, and moving a line to an employee who is new to the run seeds that employee.
Request Body
Same fields as Add Item. Only provided fields are updated.
Response
Returns the updated item.
Error Responses
| Status Code | Description |
|---|---|
| 400 | payroll.payroll_run_not_draft: the run is posted |
| 400 | payroll.payroll_line_auto_calculated: the line is calculated by the payroll engine |
| 400 | payroll.employee_missing_contract: the line was moved to an employee with no active employment contract |
| 400 | The line is not on this payroll run, or a changed item type or employee is not valid |
| 403 | payroll.payroll_run_access_denied: no access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found: payroll run not found |
Delete Item
DELETE /api/v2/payroll/runs/{document_id}/items/{item_id}
Removes a payroll line item from a draft run. Lines the payroll engine calculates can’t be deleted, except a manually added PENSION_EMPLOYER or AFP_PREMIUM line. After deletion, the system recalculates derived amounts. A deleted item stays deleted, including a base earning seeded from the contract, and the other items keep their ids. Deleting an employee’s last item takes them off the run; adding an item for them again seeds them anew.
Response
Returns 204 No Content on success.
Error Responses
| Status Code | Description |
|---|---|
| 400 | payroll.payroll_run_not_draft: the run is posted |
| 400 | payroll.payroll_line_auto_calculated: the line is calculated by the payroll engine |
| 400 | The line is not on this payroll run |
| 403 | payroll.payroll_run_access_denied: no access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found: payroll run not found |
Posting and Reversal
Once a payroll run’s items are finalized, it can be posted to the general ledger. Posting creates journal entries with employee dimensions and generates a PDF voucher. If the earning period and payment date fall in different calendar months, periodization (accrual and reversal) entries are automatically generated.
Preview Posting
GET /api/v2/payroll/runs/{document_id}/posting-preview
Returns a preview of the journal entry that would be created when posting, without persisting anything. Useful for reviewing before committing. The response follows the same shape as GET /api/v2/journal-entries/{id} with lines and dimensions included, plus an account_name field on each line.
Example Response
{
"id": null,
"client_account_id": 7,
"memo": "Lønnskjøring mars 2024",
"lines": [
{
"line_id": 1,
"posting_date": "2024-03-31",
"account_code": "5000",
"account_name": "Lønn til ansatte",
"currency_code": "NOK",
"debit": "45000.000000",
"credit": "0.000000",
"description": "",
"dimensions": [
{
"relation_type": "BusinessPartner",
"relation_id": 56
}
]
},
{
"line_id": 2,
"posting_date": "2024-03-31",
"account_code": "2610",
"account_name": "Skattetrekk",
"currency_code": "NOK",
"debit": "0.000000",
"credit": "15300.000000",
"description": "",
"dimensions": []
}
]
}
Post Payroll Run
POST /api/v2/payroll/runs/{document_id}/post
Creates journal entries from the payroll document, generates a PDF voucher, and marks the run as finalized (is_draft becomes false). Also triggers automatic payslip email delivery and A-melding submission for the month of the payment date (document_date): the a-melding follows the cash basis, so a September run paid on 5 October is reported in October. The a-melding is queued right away once that month has started; for a later month, the monthly submission in the first days of the following month sends it. The run needs a payment date to be posted, and a posted run’s period and payment date can’t be changed.
Response
{
"document_id": 1,
"journal_entry_id": 42
}
Error Responses
| Status Code | Description |
|---|---|
| 400 | Document has already been posted, or has no items to post |
| 403 | payroll.payroll_run_access_denied: no access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found: payroll run not found |
Reverse Payroll Run
POST /api/v2/payroll/runs/{document_id}/reverse
Reverses a posted payroll document’s journal entry and returns the run to draft status (is_draft becomes true), allowing items to be edited again.
Response
{
"document_id": 1,
"reversal_journal_entry_id": 43
}
Error Responses
| Status Code | Description |
|---|---|
| 400 | Cannot reverse a draft document, or no journal entry found |
| 403 | payroll.payroll_run_access_denied: no access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found: payroll run not found |
Payslips
Generate and distribute payslips for employees in a payroll run.
List Payslips
GET /api/v2/payroll/runs/{document_id}/payslips
Returns payslip summaries for all employees in the payroll run.
Response
{
"payslips": [
{
"employee_id": 56,
"employee_name": "Ola Nordmann",
"total_gross": "45000.000000",
"total_deductions": "15300.000000",
"net_pay": "29700.000000",
"total_employer_costs": "6345.000000"
}
]
}
Download Payslip PDF
GET /api/v2/payroll/runs/{document_id}/payslips/{bp_id}/pdf
Downloads a single employee’s payslip as a PDF file.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| document_id | integer | Payroll run document ID |
| bp_id | integer | Employee business partner ID |
Response
Returns a PDF file with Content-Type: application/pdf and Content-Disposition: attachment headers. The filename follows the pattern lonnsslipp_{period_from}_{period_to}_{bp_id}.pdf.
Error Responses
| Status Code | Description |
|---|---|
| 403 | payroll.payroll_run_access_denied: no access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found: payroll run not found; common.not_found: no payslip found for this employee |
Send Payslips
POST /api/v2/payroll/runs/{document_id}/payslips/send
Queues payslip emails for all employees in the payroll run. Emails are sent asynchronously. Employees without an email address on file are skipped.
Response
{
"message": "Payslip emails queued for delivery"
}
Returns status 202 Accepted.
Error Responses
| Status Code | Description |
|---|---|
| 400 | Cannot send payslips for a draft document |
| 403 | payroll.payroll_run_access_denied: no access to the payroll run’s client account |
| 404 | payroll.payroll_run_not_found: payroll run not found |