This page documents the API for Intempus Admin. There are two key parts to this documentation. The first part documents the connection flow required to prepare credentials. The second part documents the data exchange, which can be performed using those credentials.

Instances

There are two separate instances of the Intempus system, a production system and a testing system. The Intempus production system is running on https://intempus.dk/, the testing system is running on http://api-test.intempus.dk/ and https://api-test.intempus.dk/. The rest of this document will show prodution URLs, the testing URLs will be identical except for the hostname. There are some notable differences between production and testing:

  • Intempus will keep backups of the production system. No guarantees about data retention are given for the testing system, data may be deleted from the testing system without warning.
  • The testing system may run a newer software version which has features, that are not yet available on the production system.
  • SSL is mandatory on the production system. On the testing system SSL is optional (such that in case you need to inspect network traffic for debugging purposes, you can transfer it unencrypted.)
  • If you do use SSL over IPv4 we require you to enable SNI support. Any recent SSL client can be expected to support SNI.

Connect flow

When a user on your site indicate that they want to connect to Intempus Admin, you do the following:

  1. Generate a random 65 character alphanumerical nonce.
  2. Compute a SHA384 hash of the generated nonce.
  3. Send the user to the following URL: https://intempus.dk/api/admin-connect?site=your domain name&hash=computed SHA384 hash
Once the user has confirmed, we will redirect them back to your site on the following URL: https://your domain name/intempus/connect?pk=some alphanumeric string&hash=The SHA384 hash&token=Another alphanumeric string

Your credentials consist of these three values:

  • The pk value issued by us
  • Your chosen nonce
  • The token issued by us
  • The hash of the original nonce is included in the final URL for your convenience. It is not part of the credentials.

    The following code shows how the nonce, hash, and URL can be generated using python code.

    #!/usr/bin/python
    from hashlib import sha384
    from os import urandom
    
    nonce = ''.join([c for c in urandom(99).encode('base64') if c.isalnum()][:65])
    sha384_hash = sha384(nonce).hexdigest()
    base_url = 'https://intempus.dk/api/admin-connect'
    my_domain = 'example.com'
    url = '%s?site=%s&hash=%s' % (base_url, my_domain, sha384_hash)
    
    print 'Nonce:', nonce
    print 'Hash:', sha384_hash
    print 'URL:', url
    

    Data exchange

    The data exchange happens on the URL https://intempus.dk/api/admin-data-exchange?pk=the pk we issued. Each time this URL is accessed, each party will send a message consisting of JSON structured data. The request must conform to this JSON schema.

    A minimal data exchange looks as follows. Client sends following as POST parameter with the name data:

    {
        "nonce": "the nonce you chose",
        "token": "the token we issued"
    }

    Server replies with:

    {
        "condition_success": true,
        "failed_conditions": {},
        "guaranteed_timestamp": a number,
        "namespace": "a UUID",
        "responses": []
    }

    Notice that since the POST request has to be URL encoded the JSON request will end up looking like this on the wire: data=%7B%22nonce%22%3A+%22the+nonce+you+chose%22%2C+%22token%22%3A+%22the+token+we+issued%22%7D

    A data exchange through this API consists of the following phases, all of which are optional:

    1. Verify a set of preconditions
    2. Delete some existing objects from the database
    3. Update some existing objects in the database
    4. Create some new objects in the database
    5. Perform queries on the database
    All of these phases are executed within a single SQL transaction on the server, which makes it possible to implement a wide range of atomic updates.

    All preconditions must be satisfied in order for updates to happen in the database. If any precondition fail, we will skip from verification of preconditions to performing queries. None of the database updates will be performed in that case.

    The response from the server will list the outcome of precondition checks as well as the responses to the queries. This will be the case both if all preconditions succeed as well as if some of them fail.

    Querying the database

    Each query is given as a set of key-value pairs. There are two mandatory parameters and seven optional parameters. The mandatory parameters are:

    • class: The name of the class you want to retrieve objects from.
    • type: One of the following five constants:
      1. count: Return a count of rows matching the query.
      2. pk: Return a list of primary keys from rows matching the query.
      3. data: Return the objects matching the query as a dictionary.
      4. data-list: Return the objects matching the query as a list.
      5. send-usernames: Only valid if class is Employee. Return objects matching the query as a list. If the request has no failing conditions, the following aditional processing will happen: For each matching employee with a valid email address and no assigned username a username and a password will be generated and send to the employee email address. The language of the email is chosen based on the Accept-Language header in the HTTP request. Each returned object will be annotated with a send_username_status attribute specifying the result of the operation. This feature is only available to accounts which have been approved access. Contact our support for details.

    The optional parameters are:

    • minpk: Restrict range of pk values returned by the query.
    • maxpk: Restrict range of pk values returned by the query.
    • mintime: Restrict range of timestamp values returned by the query.
    • maxtime: Restrict range of timestamp values returned by the query.
    • id: Restrict returned objects to those with a pk in the set specified as a list of integers.
    • uuuid: Restrict returned objects to those with a uuid in the set specified as a list of strings.
    • creation_id: Restrict returned objects to those with a creation_id in the set specified as a list of strings. Use of this parameter will annotate the results returned by data and data_list with the creation_id.

    JSON data with a list of queries could look like this:

    {
        "nonce": "the nonce you chose",
        "token": "the token we issued",
    
        "queries": [
            {"class": "Customer", "type": "data"},
            {"class": "Case", "type": "pk"},
            {"class": "Employee",
             "type": "data-list",
             "creation_id": [
                 "Or7bG9Y6uXbjOug6KdjIfaHkUm58I9RD",
                 "Ry1ix68IqmH5TktdE9R1dwivWs8w91Y9"]
            },
            {"class": "WorkReport", "type": "count"}
        ]
    }

    The reply from the server could look like this:

    {
        "condition_success": true,
        "failed_conditions": {},
        "guaranteed_timestamp": 222,
        "namespace": "e758e41f-b7bc-56f6-ba84-e7b44e06d2b9",
        "responses": [
            {
                "1": {
                     "city": "",
                     "contact": "D. Hallandsen",
                     "country": "",
                     "customer_group_id": null,
                     "email": "",
                     "logical_timestamp": 167,
                     "name": "Forsikringsselskabet af 1873",
                     "notes": "",
                     "number": "",
                     "phone": "",
                     "street_address": "Strandvejen 42",
                     "zip_code": ""
                }
            },
            [1, 2, 3, 4],
            [
                {
                    "city": "",
                    "country": "",
                    "cpr_number": "",
                    "creation_id": "Or7bG9Y6uXbjOug6KdjIfaHkUm58I9RD",
                    "email": "",
                    "id": 592,
                    "logical_timestamp": 168,
                    "may_CUD_customers_and_cases": false,
                    "may_add_work_reports_to_all_cases": false,
                    "name": "Kim Andersen",
                    "number": "",
                    "phone": "",
                    "street_address": "",
                    "uuid": "81620b21-e88e-5b70-bcb0-92549e05b037",
                    "wipe_native_clients_logged_in_before": 0,
                    "zip_code": ""
                }
            ],
            22
        ]
    }

    This example reply would tell the client that there is one customer in the database. That customer has primary key "1", and the customer data is returned. Additionally it tell the client that there are four cases with primary keys "1", "2", "3", and "4". Finally it tells the client, that there are 22 work reports in the database.

    Timestamps

    Every time an object is updated that update is assigned a timestamp. These are assigned from a sequence of integers. There is no guarantee that all integers will be used. There will be cases where some numbers are skipped. There is also no guarantee, that updates will become visible in order of sequence number. One transaction may commit an update with sequence number 444, making it visible, while another ongoing transaction will later commit an update with sequence number 333. It is also not guaranteed that sequence numbers will be unique. When multiple objects are updated within a transaction it is legal for the server to assign separate sequence numbers to these or reuse the same sequence number.

    It is guaranteed that when one transaction is committed and another transaction is started after the first was committed, then all sequence numbers assigned by the second transaction will be higher than all sequence numbers assigned by the first transaction.

    Additionally the server provides a guaranteed timestamp in the reply. This guarantees that no update with a sequence number lower than the guaranteed value will become visible at a later time.

    In the above example the guaranteed timestamp was 222, which means if any customer was created or updated with a timestamp less than 222, they would have been visible to that API call. Thus to get customers updated after the above API call, the following query can be used to get updates to customers made after the above example.

    {
        "nonce": "the nonce you chose",
        "token": "the token we issued",
    
        "queries": [
            {"class": "Customer", "type": "count"},
            {"class": "Customer", "type": "data", "mintime": 222}
        ]
    }

    The reply from the server could look like this:

    {
        "condition_success": true,
        "failed_conditions": {},
        "guaranteed_timestamp": 300,
        "namespace": "e758e41f-b7bc-56f6-ba84-e7b44e06d2b9",
        "responses": [
            2,
            {
                "1": {
                     "city": "",
                     "contact": "D. Hallandsen",
                     "country": "",
                     "customer_group_id": null,
                     "email": "",
                     "logical_timestamp": 250,
                     "name": "Forsikringsselskabet af 1873",
                     "notes": "",
                     "number": "",
                     "phone": "",
                     "street_address": "Strandvejen 42",
                     "zip_code": ""
                },
                "2": {
                    "city": "",
                    "contact": "Sofie Petersen",
                    "country": "",
                    "customer_group_id": null,
                    "email": "",
                    "logical_timestamp": 350,
                    "name": "Sofie Petersen",
                    "notes": "",
                    "number": "",
                    "phone": "",
                    "street_address": "N\u00f8rregade 7",
                    "zip_code": ""
                },
            }
        ]
    }

    Classes

    A list of classes accessible through the API can be retrieved by using query_license.

    {
        "nonce": "the nonce you chose",
        "token": "the token we issued",
    
        "query_license": true
    }

    The reply will provide five lists of classes named according to the operations they can be used for.

    {
        "condition_success": true,
        "failed_conditions": {},
        "guaranteed_timestamp": 310,
        "namespace": "e758e41f-b7bc-56f6-ba84-e7b44e06d2b9",
        "responses": [],
        "license": {
            "condition": [ "Customer", "Employee" ],
            "create": [ "Employee" ],
            "delete": [ "Employee" ],
            "query": [ "Customer", "Employee" ],
            "update": [ "Employee" ]
        }
    }

    Currently the following classes are available under the standard Intempus license.

    • Case
    • CaseGroup
    • Contract
    • Customer
    • CustomerGroup
    • Employee
    • EmployeeCaseRules
    • Product
    • WorkCategory
    • WorkModel
    • WorkType
    • WorkReport

    Creating objects

    The JSON data for object creation could look like this:

    {
        "nonce": "the nonce you chose",
        "token": "the token we issued",
    
        "update": {
            "Case": {
                "4": { "conditions": { "active": true } }
            }
        },
    
        "create": {
            "WorkReport": [
                 {
                      "amount": 7.5,
                      "case_id": 4,
                      "employee_id": 4,
                      "start_date": "2014-05-12",
                      "end_date": "2014-05-12",
                      "work_type_id": 1,
                      "creation_id": "8tktmPSafvMsDPBgcWJM"
                 }
            ]
        },
    
        "queries": [
            {"class": "WorkReport", "type": "data", "mintime": 350}
        ]
    }

    This example will first verify that case number 4 is active. If case number 4 is active, a work report will be created. If case number 4 had been inactive, this would be reported as a failed condition, and no work report will be created.

    After creating the object a query is performed listing all work reports created after time stamp 350.

    On any class that has a uuid field, this can be filled in at creation time by passing creation_id field in the create structure. The assigned UUID will be a version 5 name based UUID computed as follows.

    • Namespace is chosen by the server and listed as namespace in all replies from the server. For a given set of credentials, the namespace remains constant at all times. In the above example the namespace is e758e41f-b7bc-56f6-ba84-e7b44e06d2b9
    • The name to be evaluated in the namespace is constructed by concatenating the class name and creation_id separated by a colon. In the above example, the name to be evaluated would be WorkReport:8tktmPSafvMsDPBgcWJM and the final UUID of the work report would be eb6fd268-a9e0-5c20-bfc3-c709eee5b385

    The following three ranges of ASCII characters are guaranteed to be valid in creation_id: 43 - 57 (+ , - . / and digits), 65 - 90 (upper case letters), and 97 - 122 (lower case letters). Any other characters may be rejected by the server or result in a different UUID from the one resulting from the above calculations.

    The server could reply with:

    {
        "condition_success": true,
        "failed_conditions": {},
        "guaranteed_timestamp": 350,
        "namespace": "e758e41f-b7bc-56f6-ba84-e7b44e06d2b9",
        "responses": [
            {
                "48": {
                    "amount": 7.50000000000000000000,
                    "approved": false,
                    "case_id": 4,
                    "contract_id": 4,
                    "creation_datetime": "2014-05-12 16:28:34.441547",
                    "employee_id": 4,
                    "end_date": "2014-05-12",
                    "end_time": null,
                    "logical_timestamp": 398,
                    "product_id": null,
                    "remarks": "",
                    "start_date": "2014-05-12",
                    "start_time": null,
                    "uuid": "eb6fd268-a9e0-5c20-bfc3-c709eee5b385",
                    "work_type_id": 1
                }
            }
        ]
    }

    Since condition_success is true, the client will know that case 4 was indeed active. The server will automatically add conditions to check for existence of all referenced objects. Had case 4 not existed or had work type 1 not existed, it would have reported this as a failed condition, even if not explicitly listed as a condition.

    Since the reply contains a work report with the UUID computed from the creation_id chosen by the client, the client will know, that this was in fact the work report, which it just created.

    Updating objects

    If the client want to update the object created in the previous example to set it as approved, it could use the following JSON data:

    {
        "nonce": "the nonce you chose",
        "token": "the token we issued",
    
        "update": {
            "WorkReport": {
                "48": {
                    "conditions": { "logical_timestamp": 398 },
                    "update": { "approved": true }
                }
            }
        }
    }

    It is recommended that updates are conditioned on the timestamp to prevent lost updates in case multiple updates are performed in parallel.

    Deleting objects

    If instead of updating the object in the previous example, the client had wanted to delete it, the following JSON could be used:

    {
        "nonce": "the nonce you chose",
        "token": "the token we issued",
    
        "update": {
            "WorkReport": {
                "48": {
                    "conditions": { "logical_timestamp": 398 },
                    "delete": true
                }
            }
        }
    }

    Should both be executed in parallel by two different clients, only one can succeed. If for example the update happened first, the second request would not delete the now approved work report, because of the condition. The server would reply as follows:

    {
        "condition_success": false,
        "failed_conditions": {
            "WorkReport": [
                48
            ]
        },
        "guaranteed_timestamp": 350,
        "namespace": "e758e41f-b7bc-56f6-ba84-e7b44e06d2b9",
        "responses": []
    }