Welcome to the Intempus REST API. This document contains how to read, create, update and delete data on Intempus using common server-side development environments. While Intempus' API supports all HTTP clients, we believe most integrations with Intempus will be data synchronization processes that are run as background processes.

Support

All support requests have to be in English.

If you encounter any problems with the API, contact us at api@intempus.dk. We intend to answer within one working day. Please make sure to describe the problem along with the full url, headers and body sent and the response and statuscode received. Example:

Request:

    GET intempus.dk/web/v1/work_category/
    Headers:
        Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
        Content-Type: application/json
    Body: empty

Response:

    Statuscode 401
    Body: Empty

To help us understand what you need, please also include a short description of your application.

If you received an error message with an incident ID (see Error responses below), please include the incident ID as text (not a screenshot).

Webhooks (Beta)

In addition to our REST API, we are developing webhook support so Intempus can push events to your systems in real time. The feature is currently in beta and may change as we refine behavior and delivery but we will notify affected customers before making any breaking changes.

If you need help or have a use case that is not currently supported, contact us at api@intempus.dk with a short description and we will assist.

Getting started with webhooks

Follow these steps to receive webhook requests to your system:

1) Check available event types

First, see which events we support by calling the webhook event type endpoint. Please reach out to us if you need an event that is not supported.

2) Prepare your receiver (authorize senders)

For security reasons, we send webhook requests only to servers allowing webhook requests coming from us. A server can opt-in by creating a /.well-known/webhook-authorized-senders.json file with the following content:

{
    "authorized-senders": [
        "intempus.dk"
    ]
}

For more technical details and explanation of the security benefits you can read the IANA registration and full webhook-authorized-senders.json specification.

3) Create webhook configurations

Use our webhook configuration API endpoints to subscribe to events and to configure the URL we should send events to. Currently the requests we send contain the event metadata and the id of the object affected by the event. For example we will send the following request when the Case object with id 1 gets updated:

    POST https://your-domain-name.test/your-receiving-endpoint/
    Headers:
        Content-Type: application/json
    Body:
        {
            "event": {
                "name": "Case.updated",
                "uuid_v4": "a29ce5c4-de75-4d12-84be-0d5a850ef60a",
                "timestamp": "2025-09-01T12:00:00.000000Z",
                "logical_timestamp": 5436995
            },
            "data": {
                "id": 1
            }
        }

Authentication

OAuth2 – Intempus Connect

Connect an external application to Intempus with OAuth2 authorization and public-key (private_key_jwt) authentication.

  1. Intempus support registers the integrator application and configures scopes.
  2. The customer authorizes the integrator in Intempus.
  3. The integrator exchanges the authorization code for a short-lived token and registers a public key.
  4. The integrator uses private_key_jwt to obtain API access tokens and call Intempus APIs.
  5. Public keys can later be rotated without repeating consent.

Identifiers

ID Created by Purpose
client_id Intempus support, when the integrator is registered Identifies the integrator app during authorize and consent
customer_client_id Intempus, during authorize and consent Identifies this customer's grant to the integrator. Used for api_token and rotate_public_key

customer_client_id is a different value from the integrator client_id.

Token lifetimes

Token Valid for Purpose
Temporary access token 2 minutes (expires_in: 120) Setup only. Returned by token (step 2), scoped to sync_public_key, and used once to register a public key (step 3)
API access token 1 hour (expires_in: 3600) Accessing Intempus resources. Returned by api_token (step 4). Request a new one when it expires; no refresh token is issued

Use the expires_in value in each token response (seconds) rather than hard-coding these durations.

0) Prerequisite: Register Integrator

Contact the Intempus Support team at support@intempus.dk to register your application. Please include the following details in your request:

Result: a client_id issued to the integrator. The redirect URI registered here must be the callback URL used in later steps. Intempus also configures the scopes shown to the customer during authorization.

1) Authorization

The customer logs in to the integrator application. The integrator then sends the customer to Intempus.

PKCE is required (RFC 7636). Before redirecting, generate a random code_verifier (43–128 characters from [A-Za-z0-9-._~]) in your backend and keep it there. It must never be exposed to the browser. Compute:

code_challenge = BASE64URL(SHA256(code_verifier))

per RFC 7636 section 4.2. Send code_challenge with code_challenge_method=S256. You will present the original code_verifier when exchanging the code in step 2. This binds the token exchange to the same party that started authorization, so a code intercepted in transit cannot be redeemed by anyone else.

GET https://intempus.dk/oauth/?response_type=code&state={state}&client_id={client_id}&redirect_uri={redirect_uri}&code_challenge={code_challenge}&code_challenge_method=S256

Required query parameters:

If the customer is not logged in, they are redirected to the Intempus login page. After login, they see the authorization page and can grant access to the configured scopes.

After the customer clicks Authorize, Intempus redirects to the integrator redirect_uri with:

GET {redirect_uri}?code={code}&state={state}

2) Exchange Code for Temporary Access Token

Validate state, then exchange the authorization code for a temporary access token used only for public key registration. Present the code_verifier generated in step 1. The request body must be application/x-www-form-urlencoded, per RFC 6749 section 4.1.3. A JSON body is rejected with invalid_request. The response is always JSON.

POST https://intempus.dk/api/v1/access_token/token/
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&client_id=<your_client_id>&code=<code>&redirect_uri=<redirect_uri>&code_verifier=<code_verifier>

redirect_uri must match the URI used to obtain the authorization code. code_verifier must be the exact value whose SHA-256 hash was sent as code_challenge in step 1. The exchange is rejected with invalid_grant if it is missing or does not match.

Response:

{
  "access_token": "<temp_access_token>",
  "token_type": "Bearer",
  "expires_in": 120,
  "scope": "sync_public_key"
}

This token is valid for 2 minutes, scoped only to sync_public_key, and is only used to register a public key.

3) Public Key Registration

Register an RSA public key for server-to-server authentication. Each key is referenced by a unique kid, which is later sent in the JWT header so Intempus can select the correct key.

A newly registered public key has no expiry. It remains valid until it is rotated (see Rotate a public key).

The request body must be application/x-www-form-urlencoded. access_token is presented as a form field, per RFC 6750 section 2.2. JSON bodies are rejected.

POST https://intempus.dk/api/v1/access_token/register_public_key/
Content-Type: application/x-www-form-urlencoded

public_key=-----BEGIN PUBLIC KEY-----%0AMIIC...%0A-----END PUBLIC KEY-----&algorithm=RS256&client_id=<your_client_id>&kid=<unique_key_id>&access_token=<temp_access_token>

Response:

{ "customer_client_id": "<customer_client_id>" }

HTTP 200 means registration succeeded. Store customer_client_id together with the private key for later token requests.

4) Generate API Token (using JWT client assertion)

Create a signed JWT (client_assertion) with the registered private key and the kid you registered. Then exchange it for an API access token. The request body is JSON.

POST https://intempus.dk/api/v1/access_token/api_token/
Content-Type: application/json

{
  "customer_client_id": "<customer_client_id>",
  "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
  "client_assertion": "<jwt signed with the registered private key>"
}

Required JWT claims. client_id, iss, and sub must all be the integrator client_id.

Environment aud value
Production https://intempus.dk/api/v1/access_token/api_token/
Staging https://staging-web.intempus.dk/api/v1/access_token/api_token/

Sign the JWT with the registered private key and include the matching kid in the JWT header.

Python
JavaScript

Python



from datetime import timedelta, timezone, datetime
import jwt
import requests
import uuid

private_key = load_private_key()  # Your RSA private key (PEM as str/bytes)
now = datetime.now(timezone.utc)
payload = {
    'client_id': 'your_client_id',
    'iss': 'your_client_id',
    'sub': 'your_client_id',
    'aud': 'https://intempus.dk/api/v1/access_token/api_token/',
    'iat': int(now.timestamp()),
    'exp': int((now + timedelta(minutes=5)).timestamp()),
    'jti': str(uuid.uuid4())
}
headers = { 'kid': 'your_kid' }
token = jwt.encode(payload, private_key, algorithm='RS256', headers=headers)

post_data = {
    'customer_client_id': 'customer_client_id',
    'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    'client_assertion': token,
}
response = requests.post(
    'https://intempus.dk/api/v1/access_token/api_token/',
    json=post_data,
)
access_token = response.json().get('access_token')

JavaScript (Node.js)


const jwt = require('jsonwebtoken');
const fs = require('fs');
const axios = require('axios');
const { randomUUID } = require('crypto');

const privateKey = fs.readFileSync('path/to/private.key');
const now = Math.floor(Date.now() / 1000);
const payload = {
  client_id: 'your_client_id',
  iss: 'your_client_id',
  sub: 'your_client_id',
  aud: 'https://intempus.dk/api/v1/access_token/api_token/',
  iat: now,
  exp: now + 300,
  jti: randomUUID()
};

const token = jwt.sign(payload, privateKey, {
  algorithm: 'RS256',
  header: { kid: 'your_kid' }
});

const data = {
  customer_client_id: 'customer_client_id',
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
  client_assertion: token
};

const response = await axios.post(
  'https://intempus.dk/api/v1/access_token/api_token/',
  data,
);
const accessToken = response.data.access_token;

Response:

{
  "access_token": "<access_token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read write"
}

5) Use the API

Send the access token from step 4 in the Authorization header of every request to the Intempus REST API (https://intempus.dk/web/v1/...):

Authorization: Bearer <access_token>

The token acts on behalf of the customer who gave consent, so it only sees that customer's company data. Each resource requires the matching scope granted during consent:

Filtering, ordering, paging and incremental updates work the same way as described under General notes for the api.

Example: fetch work reports updated since the last sync, using cursor based pagination.

GET https://intempus.dk/web/v1/work_report/?pagination_type=cursor&limit=500&logical_timestamp__gt=5436995
Authorization: Bearer <access_token>
Accept: application/json
cURL
Python
JavaScript

cURL

curl -H "Authorization: Bearer <access_token>" \
  -H "Accept: application/json" \
  "https://intempus.dk/web/v1/work_report/?pagination_type=cursor&limit=500"

Python

import requests

BASE_URL = 'https://intempus.dk'
headers = {
    'Authorization': f'Bearer {access_token}',
    'Accept': 'application/json',
}

work_reports = []
url = f'{BASE_URL}/web/v1/work_report/?pagination_type=cursor&limit=500'
while url:
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    body = response.json()
    work_reports.extend(body['objects'])
    next_page = body['meta'].get('next')
    url = f'{BASE_URL}{next_page}' if next_page else None

JavaScript (Node.js)

const axios = require('axios');

const BASE_URL = 'https://intempus.dk';
const headers = {
  Authorization: `Bearer ${accessToken}`,
  Accept: 'application/json',
};

const workReports = [];
let url = `${BASE_URL}/web/v1/work_report/?pagination_type=cursor&limit=500`;
while (url) {
  const response = await axios.get(url, { headers });
  workReports.push(...response.data.objects);
  const nextPage = response.data.meta.next;
  url = nextPage ? `${BASE_URL}${nextPage}` : null;
}

The access token is valid for 1 hour. When it expires, request a new one from api_token (step 4). Consent and key registration do not need to be repeated.

A request returns 401 Unauthorized when:

6) Rotate a public key

After the initial registration, rotate keys without repeating consent. The request must be signed with the current private key and include the new public key. The request body is JSON.

POST https://intempus.dk/api/v1/access_token/rotate_public_key/
Content-Type: application/json

{
  "customer_client_id": "<customer_client_id>",
  "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
  "client_assertion": "<jwt signed with the current private key>"
}

The JWT uses the same claims as step 4, plus the new key fields. aud is still the api_token URL for the environment.

{
  "client_id": "<your_client_id>",
  "iss": "<your_client_id>",
  "sub": "<your_client_id>",
  "aud": "https://intempus.dk/api/v1/access_token/api_token/",
  "iat": 1230049,
  "exp": 1230049,
  "jti": "unique-rotation-request-id",
  "new_public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
  "kid": "key-2",
  "algorithm": "RS256"
}

Sign with the old private key. Set the JWT header kid to the current registered key id:

import jwt

client_assertion = jwt.encode(
    jwt_payload,
    old_private_key_pem,
    algorithm='RS256',
    headers={'kid': 'key-1'},
)

The new key must meet the same requirements as initial registration (RSA, minimum 4096 bits, RS256, RS384, or RS512).

Response:

{ "success": true }

After rotation:

Recommended sequence: generate a new key pair, call rotate_public_key, start signing api_token requests with the new private key and new kid, then retire the old private key after the 1-hour overlap.

API Key based authentication

⚠ Deprecated: API key based authentication is deprecated and will be removed in a future API version. Please migrate to OAuth2 – Intempus Connect.

You can also authenticate using an API key. To obtain an API key, you need an active Intempus account with a company. If you do not have one, you may create a demo company. To connect, include the following HTTP header in your request:

    Authorization: ApiKey <username>:<api_key>

MCP

Intempus provides a Model Context Protocol (MCP) Server that lets AI agents work with the API on your behalf through a list of MCP Tools, which are:

The MCP Server can be used with any MCP-compatible client, including Cursor, Claude Desktop, GitHub Copilot, and other AI coding assistants.

Configuration

Add an MCP Server entry to your client's configuration file. The exact file path depends on the tool you use (for example ~/.cursor/mcp.json in Cursor or claude_desktop_config.json in Claude Desktop), but the server definition follows the same structure:

{
  "mcpServers": {
    "intempus": {
      "transport": "http",
      "url": "https://mcp.intempus.dk/mcp",
      "headers": {
        "Authorization": "apikey <username>:<api_key>",
        "X-Security-Key": "<security_key>"
      }
    }
  }
}

Authentication

The MCP Server requires two HTTP headers on every request. Both values are available from your Intempus profile under API Key:

Keep both values secret and do not commit them to version control. If a key is compromised, revoke it from your profile and generate a new one.

Intempus model

The following will describe the Intempus model briefly in order to understand and use the api.

The main model in Intempus is the workreport. There are a number of models that are directly attached to a workreport.

To facilitate the identification of the appropriate endpoint, we recommend setting the application language of the administration interface to English This adjustment aligns with the English naming convention of our models and attributes, thereby simplifying the process of locating the correct endpoint.

Case

A case that can be worked on. All cases have a customer attached.

Employee

The employee performing the work. An employee needs an open contract to be able to make a workreport. The contract also describes which workmodel the employee is working under.

WorkModel

A workreport can have a worktype attached, that describe the type of work performed WorkTypes are grouped by workcategories, which are grouped by workmodels.

General notes for the api

Recommendations for integrators

We recommend getting a separate user account for your integration, to avoid the integration interfering with the admin normal usage of the system. Please add an email address to the user account, so we can contact you if we see any issues with the integration.

When integrating to another system a lot of data is expected to be sent back and forth between the two systems. For both systems too much can be detrimental to performance. The following are recommendations to reduce load and response sizes for the benefit of both systems.

Details on the mentioned features can be found below.

Throttling

The api is throttled to 500 requests per minute per user, and 50.000 per day. If any limit is hit the user will receive a 429 status code along with a Retry-After header containing the number of seconds until the user can make another request.

Required fields

Fields marked with an asterisk (*) are required when creating or updating a resource.

Parameter formats

Parameters indicated as URL expect the id of a model formatted as their url. This can always be found in the "resource_uri" field in the response of GET calls to models. Example: "/web/v1/work_type/2983/".

If nothing else is indicated, dates are specified as "YYYY-MM-DD" and times are specified as "HH:MM:SS"

Strings are generally allowed to be 100 characters. Exceptions will be noted on the specific attribute.

Nested resources

When a resource is specified in the type column, that attribute is a nested resource. A nested resource can be manipulated directly through the parent resource. When editing an existing nested resource, the 'resource_uri' attribute must be included, otherwise the api will attempt to create a new instance of the nested resource.

Filters

Available filters are described in the description section of each item. To filter on an attribute, and a query parameter with the filter parameter. The filter parameter is specified as the attribute name and the filter type, separated by a double underscore.

The following types of filters are available:

Examples:

https://intempus.dk/web/v1/work_category/?id__in=204,205
https://intempus.dk/web/v1/case_upload/?case__number=12

Paging

All list endpoints are paged. The maximum page size is 10000, and the default is 1000 per page. The page size can be changed by setting the 'limit' parameter. Please note that using a limit below 50 might result in high response times.

We provide two options for changing to the next page. The default is using offset. But using offset is less performant for large datasets, so we also provide the option of using cursor based pagination. This is done by setting the query parameter 'pagination_type' to 'cursor'.

Offset based pagination

⚠ Deprecated: Offset-based pagination is deprecated and will be removed in a future API version. Please migrate to cursor-based pagination.

Changing to the next page is done by setting the 'offset' parameter. The following example sets the page size to 500 and requests the third page.

https://intempus.dk/web/v1/case/?limit=500&offset=1000

All list endpoints have a 'meta' attribute which contains information about paging. 'limit' and 'offset' shows the current settings, while 'next' and 'previous' contains links to the next/previous page. These are null when no such page exists. The final attribute 'total_count' shows how many objects exist in total for the current filtering. Example of a meta object below

{
  "meta": {
  "limit": 100,
  "next": "/web/v1/employee/?limit=100&offset=100",
  "offset": 0,
  "previous": null,
  "total_count": 250
}

Cursor based pagination

To use cursor based pagination, set the query parameter 'pagination_type' to 'cursor'. The cursor based pagination will always be ordered by the id of the objects and return the newest first. The response will then contain a 'meta' attribute with a 'next' attribute containing a URI to the next page. The following example sets the page size to 500 and requests the next page.

https://intempus.dk/web/v1/case/?pagination_type=cursor&limit=500&after=eyJwYWdlIjoxLCJvZmZzZXQiOjB9

All list endpoints have a 'meta' attribute which contains information about paging. For the cursor based pagination it only contains the next URI.

{
  "next": "/web/v1/employee/?pagination_type=cursor&limit=500&after=eyJwYWdlIjoxLCJvZmZzZXQiOjB9"
}

Ordering

Available orderings are described in the description section of each item. If no description is present, 'id' will be the default ordering. To order by an attribute, add a query parameter 'order_by' with the name of the attribute. To perform the inverse ordering, add a minus before the attribute name

Examples:

https://intempus.dk/web/v1/work_type?order_by=-work_category

Incremental updates

For applications synchronizing data from Intempus it is very valuable to only get objects that changed since last update. The 'logical_timestamp' attribute can be used to accomplish this, by filtering by the newest timestamp header received. Notice that deletions will not be shown in the list, and a full sync is needed to capture deleted objects. All responses to GET requests on list resources contains a 'Logical-Timestamp' header. This timestamp should be used for filtering the subsequent request. When filtering this way you are guaranteed to see each update at least once. An example of a logical_timestamp filter:

https://intempus.dk/web/v1/case/?logical_timestamp__gt=5436995

Currently implemented on the following models: workreport, case, customer, employee, product, planned_work_report, employee_department_case_rules, work_type_case_rules, employee_case_rules.

Notice that the logical timestamp is a 64 bit integer. It has no correspondence with a real timestamp, it is simply a counter that is incremented on each update. It has the sole purpose of allowing incremental updates without missing any updates. The Logical-Timestamp header contains the newest timestamp that is guaranteed to be committed to the database. It is therefore important to use this value, and not the logical_timestamp of an individual object.

Safeguarding against duplicate creations from duplicate POST requests

Consider the following scenario, a classic problem with a basic REST API:

The Intempus API implements a mechanism to ensure such duplications are impossible. Each of our database tables includes a uuid column, which can be filled in at creation time by supplying a creation_id field in the body of a POST request. This UUID defines a uniqueness constraint on the database table, and thus can be used as a guarantee that duplication as described above is impossible, provided the creation_id provided in the request's body is the same on duplicate/retry requests).

The assigned UUID will be a version 5 name based UUID computed as follows:

So, as long as the client includes a creation_id parameter in the body of the POST requests, the problematic scenario outlined above will now play out as before, with one crucial difference: instead of a duplicate entry being created in the server database upon the request retry, we return a "200-OK" HTTP response to indicate to the user that we did successfully receive and process the response, but did not create a new item (since the new item would violate the uniqueness of the UUID created in the first request).

Example

A user creates a case object via a POST to /web/v1/case with the following request body:

{
	"name": "my_case",
	"number": "1",
	"creation_id": "6",
	"customer": "/web/v1/customer/1/"
}

Let's suppose the POST successfully creates the case object in the database, but the client does not receive the 201 response, as described in the problem scenario above. The client retries the same request again. This time, the client is met with a "200-OK" response. The body of the response contains the UUID, namespace and creation_id as described above (as well as all other fields which would usually be returned in a 201 response):

{
  ...
  "creation_id": "6",
  ...
  "namespace": "39138a56-85f0-5e96-85d8-2aa537978054",
  ...
  "uuid": "71afbace-20b0-5803-be81-466aa94713cf",
  ...
}

Bulk operations

It is possible to do creations, updates and deletions in bulk by sending a PATCH request to the relevant list endpoint. The body of the request should contain an 'objects' attribute containing a list of the objects to be created or updated. Any entry with a resource_uri attribute will be updated, while any entry without a resource_uri attribute will be created as a new object. The body can also contain a 'deleted_objects' attribute containing a list of the resource_uri of the objects to be deleted.

Example

Send PATCH to /web/v1/work_report/. This will create a new workreport, update an existing workreport and delete an existing workreport.

{
  "objects":
    [
      {
        "creation_id": "1",
        "employee": "/web/v1/employee/1/",
        "start_date": "2024-01-01",
        "end_date": "2024-01-01",
        "worktype": "/web/v1/work_type/1/",
        "start_time": "09:00:00",
        "end_time": "10:00:00",
        "amount": 1,
        "remarks": "Create this object"
      },
      {
        "resource_uri": "/web/v1/work_report/1/",
        "remarks": "Update this field"
      }
    ],
    "deleted_objects":
    [
      "/web/v1/work_report/2/"
    ]
}

Error responses

In addition to the appropriate HTTP status code, we strive to return relevant details about any error that occurs during the handling of requests. For 500 errors, we provide a fallback message when no specific details are provided (subject to translation):

Sorry, this request could not be processed. Please try again later.

It might be followed by an incident id:

Incident ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

If you need to report an error to us, please include the incident ID as text (not a screenshot).