Motiontag API

This is a brief tutorial on how to set up your backend/scripts to work with the APIs MOTIONTAG has to offer.

1. Prerequisites

In order to create an account in any of our products you need to contact the MOTIONTAG staff. They will provide you with a dedicated domain, which also serves as the base URL for the APIs. When signed in you can see your custom tenant key below.

API base URL
Only accessible when signed in. You need a custom domain setup by MOTIONTAG (e.g. my-tenant-key.motion-tag.de) and an account to sign in. Contact MOTIONTAG Support to request a custom domain setup and an account. If you already have both, visit your custom domain and sign in to see a customized version of this documentation.

Furthermore, you'll be able to sign in into our dashboard to access information about your setup and your users. When logged in with the right privileges you'll be able to download or upload certain data manually as well.

Part of your base URL contains your tenant key, which you need to create correct iss claims. When signed in you can see your custom tenant key below.

Your tenant key
Only accessible when signed in. You need a custom domain setup by MOTIONTAG (e.g. my-tenant-key.motion-tag.de) and an account to sign in. Contact MOTIONTAG Support to request a custom domain setup and an account. If you already have both, visit your custom domain and sign in to see a customized version of this documentation.

2. Authentication

Every request must be authenticated with a JWT. The JWT must be signed with the shared secret which is available below when signed in and encoded with HS256.

Shared secret
Only accessible when signed in. You need a custom domain setup by MOTIONTAG (e.g. my-tenant-key.motion-tag.de) and an account to sign in. Contact MOTIONTAG Support to request a custom domain setup and an account. If you already have both, visit your custom domain and sign in to see a customized version of this documentation.
Attention
Don't publish the shared secret to your apps! You should create the JWTs for your apps on your backend to minimize the chance of a data breach.

Depending on the endpoint, the JWT must include specific claims such as sub and exp, which are documented in each endpoint's parameter list. Depending on the intended use, a JWT can either be scoped to a single end user, or to all users. Here an example of the claims for a single user.

{
  "iss": "tenant-key",
  "sub": "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb",
  "exp": 1709020393
}

Unless noted otherwise, the JSON Web Token must be sent as HTTP authorization header with Bearer, like this:

{ "Authorization": "Bearer abc123.def456.gehz7890" }

Code example

Here's a small Ruby and Python code example on how to create the JWT for a single user. Which includes the iss as tenant key, the sub as User ID (UUID) and exp as an expiration timestamp (seconds since epoch) as well as assuming the shared secret is available in the environment variable SHARED_SECRET.

In Ruby using the jwt gem

require 'jwt'

claims = {
  iss: 'tenant-key',
  sub: 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb',
  exp: Time.now.to_i + 60,
}
JWT.encode(claims, ENV['SHARED_SECRET'], 'HS256')
# => abc123.def456.gehz7890

In Python using PyJWT package

import os
import time
import jwt

claims = {
  "iss": "tenant-key",
  "sub": "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb",
  "exp": int(time.time()) + 60
}
jwt.encode(claims, os.getenv("SHARED_SECRET"), algorithm="HS256")
# => abc123.def456.gehz7890

3. User management

3.1 Create user

In order to create a user on our backend the SDK (on the phone) must be configured with a user token at runtime. This enables the SDK to send us events we need to create tracks and stays.

Tokens can be generated on your backend, or manually with the form below (only accessible when signed in). They are signed JWTs. Users are identified by distinct UUIDs – the creation and management of the user UUIDs is up to you. MOTIONTAG creates a user entry in its database when data from the SDK for a new user UUID arrives for the first time. This user UUID is used to identify data transferred back to you.

To generate the JWTs on your backend, encode and sign a payload like the example below with the shared secret, which can be found above in Authentication section.

Claims for SDK token

Claim Type Description Required
iss String Your tenant key
sub String User UUID
exp Integer Expiration time in seconds since epoch

When using the HS256 algorithm the following example claim payload

{
  "iss": "tenant-key",
  "sub": "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb",
  "exp": 1709020393
}

can be encoded as a valid user token (JWT). A code example on how to do this can be found here. This token can be used to initialize the MOTIONTAG SDK within your iOS or Android app. Now you have to find a way to deliver this token to your own apps. We need this JWT to create an authorization header ourselves in order to send events (e.g. location data) from inside our SDK to our backend.

Attention
Don't publish the shared secret to your apps! You should create the JWTs for your apps on your backend to minimize the chance of a data breach.

Generate valid tokens

In this section you can generate one or multiple valid JWTs to test with your app. Additionally you can compare those generated JWTs with the ones you created and check if you did everything correctly.

Generate JWTs from user UUIDs
Only accessible when signed in. You need a custom domain setup by MOTIONTAG (e.g. my-tenant-key.motion-tag.de) and an account to sign in. Contact MOTIONTAG Support to request a custom domain setup and an account. If you already have both, visit your custom domain and sign in to see a customized version of this documentation.

3.2 Delete user

The following endpoint enqueues a job, which deletes related user data in the background, the complete process can take several hours. The delete user endpoint does not require any request parameters, instead it needs a user specific JWT with the following claims.

Endpoint

DELETE
/json_api/user

Response body

{
  "data": {
    "type": "Delete user",
    "id": "84342a1f-2963-4d78-a8ae-4651b81cb891"
  }
}

On success, you receive a JSON response with a type and id property, id contains the user id. The response HTTP status is 202 accepted.

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
iss String Your tenant key
sub String User UUID
exp Integer Expiration time in seconds since epoch

4. Data management

There are two ways you can retrieve tracks and stays into your backend from MOTIONTAG. Both ways allow you to get multiple items of multiple users with only one authorization token (JWT).

The Synchronization API is like an endless stream of tracks and stays which are chronologically ordered and grouped into pages of reasonable size. New entries are appended continuously. This endpoint is deterministic, which means you can jump back and forth between pages any time and will always retrieve the same data.

The Dump Interface can be accessed programmatically, but allows you only to download data. This is basically the same endpoint you can access when downloading dumps from the dashboard. In order to download dumps they need to be created manually first. Luckily dumps can be scheduled by MOTIONTAG before execution, what allows e.g. for weekly dump creations for the next six months.

4.1 Synchronization API

This is an endpoint for full synchronization of the complete storyline of all users. Meant to be implemented by customers that run their own backend service and want to maintain a full copy of all their user's storyline on their backend service.

An initial request without the special page parameter will return the first available page, and include a link to the next page. When the end is reached, an empty page without a next link is returned. In this case the consumer should pause for a short time and then retry to load the same page again.

In case you are wondering, possible values for modes and purpose depend on the server configuration and can be looked up in the admin dashboard here.

The geometry attribute is optional. It depends on your tenant configuration whether your are able to receive it from the Synchronization API.

Advantages
  • Retrieve tracks and stays for multiple users and days at once
  • You only need one JWT to authenticate
  • It is deterministic, so every time you send the same request you get the same response
  • This also means you can go back and forth in time
  • You handle data edits directly on your side, between your app and your backend
Disadvantages
  • You can't retrieve any edits on tracks and stays
  • There is only tracks and stays data available via this API

Endpoint

GET
/sync/storyline

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
sub String Fixed value for this endpoint: sync
exp Integer Expiration time in seconds since epoch, recommended to set to near future

Query parameters

Name Type Description
page[after] String Paging position, used by the server to determine next page

Initial request example

Do not include page parameter on the initial request.

GET
/sync/storyline
{
  "data": [
    {
      "type" : "Track",
      "id" : "8861e1e2-abd0-41cc-9b3d-f8c2d2613cf4",
      "attributes" : {
        "user_id" : "d28907c7-0846-461f-950e-9d40f2ca418c",
        "created_at" : "2018-01-01T16:00:00Z",
        "started_at" : "2018-01-01T14:00:00Z",
        "started_at_timezone" : "Europe/Zurich",
        "finished_at" : "2018-01-01T15:00:00Z",
        "finished_at_timezone" : "Europe/Zurich",
        "geometry" : {
           "type" : "LineString",
           "coordinates" : [
              [
                 1,
                 1.5
              ],
              [
                 1.5,
                 2
              ]
           ]
        },
        "track_mode" : "bicycle",
        "track_length_in_meters" : 5000
      }
    },
    {
       "type" : "Stay",
       "id" : "c862b09b-2786-4be3-b0d6-90997783a343",
       "attributes" : {
         "user_id" : "437b0a94-d495-4e4e-96fb-fe1783b5c95e",
         "created_at" : "2018-01-01T16:00:00Z",
         "started_at" : "2018-01-01T14:00:00Z",
         "started_at_timezone" : "Europe/Zurich",
         "finished_at" : "2018-01-01T15:00:00Z",
         "finished_at_timezone" : "Europe/Zurich",
         "geometry" : {
            "type" : "Point",
            "coordinates" : [
               1,
               1.5
            ]
         },
         "stay_purpose" : "study"
       }
    }
  ],
  "links": {
    "next" : "https://api.motion-tag.de/sync/storyline?page[after]=abc123"
  }
}


The consumer is expected to store the received entries and immediately afterwards request the next page given in links.next. The value of page[after] has no meaning beyond being used by the server to determine the next page.

Subsequent request example

In this example, no data is returned. In the case of no data, the consumer should pause for a short time and then request the same URL again.

GET
/sync/storyline?page[after]=abc123
{
  "data": []
}

4.2 Dump interface

Data dumps available on the admin dashboard interface under "Dumps" can be downloaded from this endpoint by using their filename. The filename is chosen automatically on creation and consists of the dump type, its user and date criteria, and the format. Note that the API currently only provides the ability to download dumps, not to create or delete them.

Advantages
  • Retrieve tracks and stays for multiple users and days at once
  • There is also other data, e.g. user statistics or trips and journeys available
  • It is even possible to get a custom dump design
  • You only need one JWT to authenticate
  • You can retrieve edits on tracks and stays
Disadvantages
  • It is not deterministic, so results might change over time regarding tracks and stays
  • Dumps can't be created programmatically and need to be scheduled ahead of time

Endpoint

GET
/dumps/{filename}

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
sub String Fixed value for this endpoint: dumps
exp Integer Expiration time in seconds since epoch, recommended to set to near future

Path parameters

Name Type Description
filename String Filename of the dump

Query parameters

As an alternative authentication method you can use the JWT as URL query parameter instead of the HTTP request header.

Name Type Description Required
jwt String The generated JWT

Example request

GET
/dumps/ExampleDump.SomeProject.2020-01-01--2020-01-31.csv.gz

Example with alternative authorization

GET
/dumps/ExampleDump.SomeProject.2020-01-01--2020-01-31.csv.gz?jwt=abc123.def456.gehz7890

5. Interactive API

If you plan to integrate our SDK and directly connect your app with our backend you are in the correct place here. The Interactive API allows the user to directly fetch (GET) and edit (PATCH) tracks and stays. This means your backend is only needed to create the JWTs for your users in order to directly identify the user on our backend side. You still need to maintain the user UUID and the JWT, but every interaction is completely decoupled from your backend. In order to download tracks or stays with their respective edits to your backend you would need to use the Dump Interface.

Advantages
  • Your users can directly edit tracks and stays
  • Your app talks directly to our backend and handles track and stay data accordingly
  • Your backend doesn't need to save any tracks and stays
  • Your backend only has to deal with user management, but avoids all the internet traffic and data persistence around tracks and stays
Disadvantages
  • You need for each user a separate JWT to authenticate and fetch data, which you anyways have to create in order to create a user for the SDK
  • There is only tracks and stays data available via this API

5.1 Storyline GET

This endpoint allows to retrieve a part of the storyline of a single user. It is meant for interactive use, such as displaying a list of tracks and stays on a map in a mobile app.

Endpoint

GET
/storyline/{date}

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
sub String UUID of a user
aud String Fixed value for this endpoint: read
exp Integer Expiration time in seconds since epoch

Path parameters

Name Type Description Required
date Date Single date, e.g. "2020-01-01"

Query parameters

Name Type Description Required
fields[Track] String Comma separated list of fields to include in Track
fields[Stay] String Comma separated list of fields to include in Stay

The fields parameters allow you to limit the list of fields being returned to a sparse fieldset. In particular the geometry field of Track can become quite large and it makes sense to exclude it when not needed.

Tracks and stays are filtered to have started on the given date, in the local timezone they occurred in.

Response body

A list of storyline items. The list is ordered chronologically.

  • type
    Can be Track or Stay. Track has mode attributes, Stay has purpose attributes.
  • started_at
    Is in the local timezone it occured in and includes the UTC offset.
  • finished_at
    Is in the local timezone it occured in and includes the UTC offset.
  • length
    Is only present for Storyline items of type Track and is in meters.
  • mode_key
    Is the untranslated key of the mode, only present if the type is Track. This attribute can potentially be edited by the user.
  • mode_name
    Is the translated name of the mode, only present if the type is Track. See below for more information about the languages.
  • purpose_key
    Is the untranslated key of the purpose, only present if the type is Stay. This attribute can potentially be edited by the user.
  • purpose_name
    Is the translated name of the purpose, only present if the type is Stay. See below for more information about the languages.
  • detected_mode_key
    Is the untranslated key of the detected mode, only present if the type is Track.
  • detected_mode_name
    Is the translated name of the detected mode, only present if the type is Track. See below for more information about the languages.
  • detected_purpose_key
    Is the untranslated key of the detected purpose, only present if the type is Stay.
  • detected_purpose_name
    Is the translated name of the detected purpose, only present if the type is Stay. See below for more information about the languages.
  • misdetected_completely
    Is a flag if a storyline item was misdetected completely and should be therefore not being displayed once set to true. The default is false.
  • geometry
    geometry of a Track is a GeoJSON feature of type LineString or MultiLineString. geometry of a Stay is a GeoJSON feature of type Point.

The possible values for modes and purpose depend on the server configuration and can be looked up in the admin dashboard here.

The language of translated fields (currently mode_name and purpose_name) is determined by the given Accept-Language header, or the server's default. The available languages depend on the settings of the server.

Example request

GET
/storyline/2020-01-01
{
  "data": [
    {
      "id": "4db59ee6-92ae-4e96-b5ec-eb192ee175cc",
      "type": "Track",
      "attributes": {
        "started_at": "2020-11-01T11:00:00+01:00",
        "finished_at": "2020-11-01T13:00:00-05:00",
        "length": 5000,
        "mode_key": "light_rail",
        "mode_name": "Rapid transit railway",
        "detected_mode_key": "train",
        "detected_mode_name": "Train",
        "misdetected_completely": false,
        "geometry": {
          "type": "LineString",
          "coordinates": [[2.0, 3.0], [3.0, 4.0]]
        }
      }
    },
    {
      "id": "26966953-6a54-4f0f-886c-e2e0074b3005",
      "type": "Stay",
      "attributes": {
        "started_at": "2020-11-01T13:00:00-05:00",
        "finished_at": "2020-11-02T01:00:00-05:00",
        "purpose_key": "home",
        "purpose_name": "At home",
        "detected_purpose_key": "home",
        "detected_purpose_name": "At home",
        "misdetected_completely": true,
        "geometry": {
          "type": "Point",
          "coordinates": [3.0, 4.0]
        }
      }
    }
  ]
}

This example response contains two storyline items, one Track and one Stay.

Example request with limited fields

GET
/storyline/2020-01-01?fields[Track]=started_at,finished_at,mode_name&fields[Stay]=started_at,finished_at,purpose_name
{
  "data": [
    {
      "id": "4db59ee6-92ae-4e96-b5ec-eb192ee175cc",
      "type": "Track",
      "attributes": {
        "started_at": "2020-11-01T11:00:00+01:00",
        "finished_at": "2020-11-01T13:00:00-05:00",
        "mode_name": "Rapid transit railway"
      }
    },
    {
      "id": "26966953-6a54-4f0f-886c-e2e0074b3005",
      "type": "Stay",
      "attributes": {
        "started_at": "2020-11-01T13:00:00-05:00",
        "finished_at": "2020-11-02T01:00:00-05:00",
        "purpose_name": "At home"
      }
    }
  ]
}

5.2 Storyline PATCH

This endpoint allows to update a certain attribute of the storyline of a single user. For a storyline item of type Track you can edit the attribute mode. For a storyline item of type Stay you can edit the attribute purpose. For both types Track and Stay you can edit the attribute misdetected_completely, which indicates that this item should no longer be displayed. Note that you can only edit a single attribute per request. Currently the only supported operation in the meta object of the JSON body is edit.

Endpoint

PATCH
/storyline/{id}

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
sub String UUID of a user
aud String Fixed value for this endpoint: read
exp Integer Expiration time in seconds since epoch

Path parameters

Name Type Description Required
id UUID ID of a storyline item, e.g. 26966953-6a54-4f0f-886c-e2e0074b3005

Editable attributes in body

Name Type Description
mode String Mode key of a storyline item of type Track, e.g. "car"
purpose String Purpose key of a storyline item of type Stay, e.g. "work"
misdetected_completely Boolean Whether the storyline item should be displayed, e.g. true

Example request

PATCH
/storyline/26966953-6a54-4f0f-886c-e2e0074b3005
with a JSON body like this, to update the purpose of a stay

{
  "data": {
    "id": "26966953-6a54-4f0f-886c-e2e0074b3005",
    "type": "Stay",
    "attributes": {
      "purpose": "sport"
    }
  },
  "meta": {
    "operation": "edit"
  }
}

Returns 204 No content if the operation was successful.

5.3 Calendar

This endpoint allows you to retrieve one element for each day with a count of storyline items since the activation of the user for the requested year.

GET
/calendar/{year}

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
sub String UUID of a user
aud String Fixed value for this endpoint: read
exp Integer Expiration time in seconds since epoch

Path parameters

Name Type Description Required
year Integer Single year, e.g. "2020"

Response body

A chronologically sorted list of dates the user was active in, with storyline counts.

Example request

GET
/calendar/2020
{
  "data": {
    "id": "7015915d-cb3c-417e-a855-6091bf3f9d14/2020",
    "type": "Calendar",
    "attributes": {
      "days": [
        {
          "date": "2020-12-29",
          "storyline_count": 0
        },
        {
          "date": "2020-12-30",
          "storyline_count": 2
        },
        {
          "date": "2020-12-31",
          "storyline_count": 1
        }
      ]
    }
  }
}

This example response contains always just one entry. It shows all days of the year with a count of storyline items per day since the activation of the user (in this example case the user was activated on '2020-12-29').

5.4 Modes

This endpoint allows you to retrieve all enabled modes of your tenant.

Endpoint

GET
/modes

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
sub String UUID of a user
aud String Fixed value for this endpoint: read
exp Integer Expiration time in seconds since epoch

Response body

An alphabetically sorted list of modes. The id property contains the provided user ID of the JWT token sub claim.

Example request

GET
/modes
{
  "data": {
    "id": "7015915d-cb3c-417e-a855-6091bf3f9d14/2020",
    "type": "Modes",
    "attributes": [
      {
        "id": "Mode::Airplane",
        "key": "airplane",
        "name": "Airplane",
        "color": "#e9b100",
        "darker_color": "#ba8e00",
        "icon": "/icons/modes/airplane.png",
        "icon_white_3x": "/icons/modes/airplane_white_3x.png",
        "icon_svg": "/icons/modes/airplane.svg",
        "carbon_emission_per_kilometer": 196,
        "public_transport": false
      },
      {..},
    ]
  }
}

On success, you receive a JSON response with a type and id property, id contains the user id. The response HTTP status is 200 OK.

5.5 Purposes

This endpoint allows you to retrieve all enabled purposes of your tenant.

Endpoint

GET
/purposes

Claims for authorization header

See the authentication section on how to create an authorization header from the following claims.

Claim Type Description Required
sub String UUID of a user
aud String Fixed value for this endpoint: read
exp Integer Expiration time in seconds since epoch

Response body

An alphabetically sorted list of purpose. The id property contains the provided user ID of the JWT token sub claim.

Example request

GET
/purposes
{
  "data": {
    "id": "7015915d-cb3c-417e-a855-6091bf3f9d14/2020",
    "type": "Purposes",
    "attributes": [
      {
        "id": "home",
        "key": "home",
        "name": "At home",
        "color": "#4f4f4f",
        "darker_color": "#3f3f3f",
        "icon_white_3x": "/icons/purposes/home_white_3x.png",
        "icon_svg": "/icons/purposes/home.svg",
        "icon_marker_png": "/icons/purposes/home_marker.png"
      },
      {..},
    ]
  }
}

On success, you receive a JSON response with a type and id property, id contains the user id. The response HTTP status is 200 OK.