Sending planning data as OTM5 file

To make sending planning data a lot easier, we make use of our universal planning inbox. The inbox is a single API which is able to receive planning data in different formats. One of the formats we are able to consume is JSON and specifically in the OTM5 standard. Underneath you will find more information on how to arrange your OTM5 file. Let's get started.

A look at the basic format

We are going to take you through the data structure, step by step. Note that this is about inserting or updating trips, cancelations are also supported but explained later. There are also variations on the 'normal' trip that come afterwards as well. Let's take a look at the objects we will be using for the default trip planning first:

Copy
Copied
{
  "id": "2a8af6c6-e4d5-5dfb-9532-e8b17c3ffd88",
  "name": "2021-32-2-888",
  "externalAttributes": {},
  "actors": [],
  "actions": []
}
id
uuid

Universally Unique Identifier to the trip.

name
string

Display name of this trip as typically seen in the URL in the Simacan domain, usually unique within a single day.

externalAttributes
object

Any attributes related to this trip.

actors
Array of arrays

All actors that are involved in this trip, and typically have access to at least its planning.

actions
Array of arrays

All actions that are involved in this trip.

Trip planning ruleset

We have implemented a few rules, to which an OTM5 planning should adhere, to make it a validated and valuable planning.

Trip level

  • A trip is required to have a name (display name), as shown in the basic structure
  • A trip is required to have actions: at least two stops are necessary.
  • A trip is required to have actors, a shipper and a carrier; this would look like the following:
Copy
Copied
   "actors": [
    {
      "associationType": "attributeRestriction",
      "restriction": {
        "externalAttributes": {
          "carrierId": "<my-carrier>"
        }
      },
      "entityType": "actor",
      "roles": ["carrier"]
    },
    {
      "associationType": "attributeRestriction",
      "restriction": {
        "externalAttributes": {
          "shipperId": "<my-shipper>"
        }
      },
      "entityType": "actor",
      "roles": ["shipper"]
    }
]
  • A trip optionally has a shift and/or tripId within the externalAttributes object. More attributes can be provided and will be saved in the Control Tower as customer data. This data serves no purpose except for filtering trips that have the same values for the same keys.
Copy
Copied
  "externalAttributes": {
    "tripId": "some-unique-id",
    "shift": "Evening shift",
    "some-custom-field": "some-custom-value"
  }

Stop level

A stop is a member of the actions array

  • A stop is required to have a location, startTime and an endTime.
  • A stop can have multiple load and unload actions; these actions have a consignment.
  • A stop optionally has a name, remark and sequenceNr.
  • A stop optionally has a stopId within an externalAttribute, this identifier can be used for updating this stop. If none is present the UUID of the stop will be used.
  • A stop optionally has time constraints, wich in code looks like:
Copy
Copied
  {
    "id": "48c484f7-54ad-4209-b251-44abef26f6b0",
    "value": {
        "startTime": "2021-03-12T10:00:00Z",
        "endTime": "2021-03-12T11:00:00Z",
        "type": "timeWindowsConstraint"
      }
    }
  }

this replaced the older, but still supported version:

Copy
Copied
  {
    "id": "48c484f7-54ad-4209-b251-44abef26f6b0",
    "value": {
      "and": [
        {
          "startDateTime": "2021-03-12T10:00:00Z",
          "type": "startDateTimeConstraint"
        },
        {
          "endDateTime": "2021-03-12T11:00:00Z",
          "type": "endDateTimeConstraint"
        }
      ],
      "type": "andConstraint"
    }
  }

Consignment level

  • A load/unload action has an inline consignment, a startTime and an endTime
  • A consignment has a type (free text), this is mapped to what is known in the SCT as the goodsFlowType.
  • A consignment optionally has externalAttributes such as consignmentId, orderId, userId, trackTraceCode. Other attributes are ignored.
  • A consignment optionally has goods. A good has a quantity and optionally a productType, barCode en remark
  • A complete consignment will more or less have the following structure in code (depending on what fields you fill of course):
Copy
Copied
{
  "id": "a556da93-46f3-4df5-8269-263bb337fde4",
  "externalAttributes": {
    "orderId": "<order id>",
    "userId": "<user id>",
    "trackTraceCode": "<t&t code>"
  },
  "type": "Frozen goods",
  "goods": [
    {
      "entity": {
        "id": "b74d1b1a-905a-42dd-91be-1fe77f443467",
        "remark": "Be careful not to melt it!",
        "barCode": "123456789",
        "quantity": 3,
        "productType": "Ice cream",
        "type": "items"
      },
      "associationType": "inline"
    }
  ]
}

Location level

Within Simacan we deal with two types of locations:

  • managed locations, those that are reused for different trips repeatedly. Managed locations also are visible in Master Data, can have manual geofences, and first/last mile guidance.
  • unmanaged locations, those are only of interst in one planning and thus are not visible in Master Data. These locations do not have manual geofences, and no first or last mile guidance.

Both types of locations can be send using OTM5. Whenever a location is already in our system (managed) you can refer to it based on its ID by using a 'restriction'.

1. A location as restriction, only uses a locationId, will only work if the location is already known within Simacan:

Copy
Copied
"location": {
  "associationType": "attributeRestriction",
  "restriction": {
    "externalAttributes": {
      "locationId": "someId"
    }
  },
  "entityType": "location"
}

Whenever a location is unknown (unmanaged) or needs to be updated (both managed and unmanaged) you can provide a location 'inline':

2a. A location as a full address requires a street, postalCode, city, country, name and optional a houseNumber and houseNumberExtension. An inline location also has a type which can be customer, store, warehouse, or operationalBase. Note that the first stop in a trip always needs to be on a warehouse or operationalBase.

Copy
Copied
   "location": {
       "entity": {
         "id": "a1463169-8e75-4c0c-8d4f-fbeb408f53aa",
         "geoReference": {
           "name": "Simacan",
           "street": "Valutaboulevard",
           "houseNumber": "16",
           "postalCode": "1234AB",
           "city": "Amersfoort",
           "type": "addressGeoReference"
         },
         "type": "customer"
       },
       "associationType": "inline"
   }
  • An inline location optionally has lat/lon-coordinates:
Copy
Copied
{
  "id": "440fffc6-3d2f-4cb2-9b98-93f0f1a87f3a",
  "geoReference": {
    "lat": 52.370216,
    "lon": 4.895168,
    "type": "latLonPointGeoReference"
  },
  "administrativeReference": {
    "name": "Simacan",
    "street": "Valutaboulevard",
    "houseNumber": "16",
    "postalCode": "1234AB",
    "city": "Amersfoort"
  },
  "type": "warehouse"
}
  • A location optionally has contactDetails:
Copy
Copied
    "contactDetails": [
      {
        "value": "Simacan",
        "type": "name"
      },
      {
        "value": "0612345678",
        "type": "phone"
      },
      {
        "value": "sim@can.com",
        "type": "email"
      }
    ]

2b. Lastly, it is possible to provide an 'address-less' location, that only consists of a name and coordinates. These locations are always unmanged:

Copy
Copied
     "location": {
       "entity": {
         "id": "13022ee7-6c11-42fd-866b-5c10256a77a3",
         "name": "Location on some road",
         "geoReference": {
           "lat": 51.9445,
           "lon": 4.369,
           "type": "latLonPointGeoReference"
         },
         "type": "customer"
       },
       "associationType": "inline"
     },

Variations

The above trip information is the most common type of trip in Simacan. However there are variations on this trip possible.

Cancelations

It is also possible to cancel a trip with OTM5. This message contains the trip ID, the actors for that trip, and the status 'cancelled':

Copy
Copied
{
  "id": "b4c9036f-8d97-4aab-ac25-34da94976849",
  "status": "cancelled",
  "actors": [
    {
      "restriction": {
        "externalAttributes": {
          "carrierId": "carrier"
        }
      },
      "entityType": "actor",
      "roles": [ "carrier" ],
      "associationType": "attributeRestriction"
    },
    {
      "restriction": {
        "externalAttributes": {
          "shipperId": "shipper"
        }
      },
      "entityType": "actor",
      "roles": [ "shipper" ],
      "associationType": "attributeRestriction"
    }
  ]
}

Consignor & consignee

A 'regular' trip has one shipper and one carrier. However, there are also trips that have a single carrier, but consignments are delivered from and to various different actors. Such trips are known as groupage trips. These trips differ from regular trips because the shipper actor is not present, but instead a consignee actor (buyer of the goods) and a consignor actor (seller of the goods) are present on each consignment. Note that the consignor and consignee can be the same actor:

The trip actor(s) thus look like this:

Copy
Copied
   "actors": [
    {
      "associationType": "attributeRestriction",
      "restriction": {
        "externalAttributes": {
          "carrierId": "<my-carrier>"
        }
      },
      "entityType": "actor",
      "roles": ["carrier"]
    }
]

And a consignment can look like this:

Copy
Copied
{
  "id": "a556da93-46f3-4df5-8269-263bb337fde4",
  "externalAttributes": {
    "orderId": "<order id>",
    "userId": "<user id>",
    "trackTraceCode": "<t&t code>"
  },
  "actors": [
    {
      "associationType": "inline",
      "entity": {
        "name": "<receiving-party>"
      },
      "roles": [ "consignor", "consignee" ]
    }
  ],
  "type": "Frozen goods",
  "goods": [
    {
      "entity": {
        "id": "b74d1b1a-905a-42dd-91be-1fe77f443467",
        "remark": "Be careful not to melt it!",
        "barCode": "123456789",
        "quantity": 3,
        "productType": "Ice cream",
        "type": "items"
      },
      "associationType": "inline"
    }
  ]
}

It is also possible to split the actor into two:

Copy
Copied
  "actors": [
    {
      "associationType": "inline",
      "entity": {
        "name": "consignor-name"
      },
      "roles": [ "consignor"]
    },
    {
      "associationType": "inline",
      "entity": {
        "name": "consignee-name"
      },
      "roles": [ "consignee"]
    },
  ]

Trip vehicle data

Regardless if you provide a regular trip or a groupage trip, in both cases it is also possible to provide vehicle data. The vehicle data should at least contain either a licensePlate or an external ID (or both). On top of that it is possible, but not required to provide a vehicle type and fuel type:

Copy
Copied
    "vehicle": {
      "entity": {
        "id": "3de444dd-81cf-4e69-b27b-82e92e0dbda3",
        "externalAttributes": {
          "vehicleId": "my-external-id"
        },
        "vehicleType": "truck",
        "fuel": "Euro95",
        "licensePlate": "AA-12-BB"
      },
      "associationType": "inline"
    }

If you provide a vehicle it is also possible to provide driver and/or trailer. You can add a driver by adding an actor to the trip (on top of the shipper and carrier):

Copy
Copied
   "actors": [
    {
      "associationType": "attributeRestriction",
      "restriction": {
        "externalAttributes": {
          "carrierId": "<my-carrier>"
        }
      },
      "entityType": "actor",
      "roles": ["carrier"]
    },
    {
      "associationType": "attributeRestriction",
      "restriction": {
        "externalAttributes": {
          "shipperId": "<my-shipper>"
        }
      },
      "entityType": "actor",
      "roles": ["shipper"]
    },
    {
      "associationType": "attributeRestriction",
      "restriction": {
        "externalAttributes": {
          "driverId": "<name of the driver>"
        }
      },
      "entityType": "actor",
      "roles": ["driver"]
    }
]

Lastly, it is possible to attach a trailer by adding an attachTransportEquipment action on the first stop, before or after loading and unloading:

Copy
Copied
   {
    "entity": {
      "id": "028ad890-38fb-44d8-a0d3-304f0dbe8e31",
      "transportEquipment": {
        "entity": {
          "id": "2fbe0dfe-c15a-4cee-a807-f462318d6f35",
          "licensePlate": "00-AAA-0"
        },
        "associationType": "inline"
      },
      "startTime": "2021-06-28T04:00:00Z",
      "endTime": "2021-06-28T04:01:00Z",
      "actionType": "attachTransportEquipment"
    },
    "associationType": "inline"
   }

Full message example

Combining the ruleset above and taking into account required and optional fields, we can compose the following message. This is an example message, with three stops, all filled with dummy values. You could copy and paste and replace this with your own data.

Copy
Copied
{
  "id": "8aef6638-3828-45b6-9391-3e2882512345",
  "name": "OTM5 Trip Example",
  "externalAttributes": {
    "tripId": "25/04/21|OTM5-EXAMPLE-TRIP"
  },
  "actors": [{
    "associationType": "attributeRestriction",
    "restriction": {
      "externalAttributes": {
        "carrierId": "bbf1e201-c1a3-4205-a496-04a7e6f12345"
      }
    },
    "entityType": "actor",
    "roles": ["carrier"]
  },
  {
    "associationType": "attributeRestriction",
    "restriction": {
    "externalAttributes": {
      "shipperId": "08d6740f-0f4c-484f-8c0b-6c69f0d54321"
      }
  },
    "entityType": "actor",
    "roles": ["shipper"]
  }
],
  "actions": [{
    "entity": {
      "id": "66f041f1-cd50-4c0f-9c1a-dea046909876",
      "name": "S",
      "externalAttributes": {
        "stopId": "25/04/21|OTM5-EXAMPLE-TRIP-start"
      },
      "lifecycle": "planned",
      "location": {
        "associationType": "attributeRestriction",
        "restriction": {
          "externalAttributes": {
            "locationId": "HUB Amsterdam"
          }
        },
        "entityType": "location"
      },
      "startTime": "2021-04-25T17:36:00Z",
      "endTime": "2021-04-25T17:51:00Z",
      "actions": [{
        "entity": {
          "id": "e86bd334-f62c-45fe-8a8b-661cde187654",
          "consignment": {
            "entity": {
              "id": "6d0931ef-f9a1-4267-be7c-758aef154321",
              "type": "Products to load",
              "goods": [{
                "entity": {
                  "id": "3ddcac3b-c349-4e08-bab5-f39925145678",
                  "barCode": "x-0006",
                  "quantity": 12,
                  "productType": "Newspaper",
                  "type": "items"
                },
                "associationType": "inline"
              }, {
                "entity": {
                  "id": "86511ae4-8956-4df3-b3c5-0c0efc355555",
                  "barCode": "x-0012",
                  "quantity": 1,
                  "productType": "Book",
                  "type": "items"
                },
                "associationType": "inline"
              }, {
                "entity": {
                  "id": "7a6f6039-2f01-40a7-a557-6f2dbf022222",
                  "barCode": "x-0013",
                  "quantity": 1,
                  "productType": "Champagne",
                  "type": "items"
                },
                "associationType": "inline"
              }]
            },
            "associationType": "inline"
          },
          "startTime": "2021-04-25T17:36:00Z",
          "endTime": "2021-04-25T17:51:00Z",
          "actionType": "load"
        },
        "associationType": "inline"
      }],
      "constraint": {
        "entity": {
          "id": "be406065-048e-40c6-b212-0d0561188888",
          "value": {
            "startTime": "2021-04-25T17:35:00Z",
            "endTime": "2021-04-25T18:35:00Z",
            "type": "timeWindowsConstraint"
          }   
        },
        "associationType": "inline"
      },
      "actionType": "stop"
    },
    "associationType": "inline"
  }, {
    "entity": {
      "id": "1716d72d-d480-4035-9b19-5a92bfc88888",
      "name": "1",
      "externalAttributes": {
        "stopId": "25/04/21|OTM5-EXAMPLE-TRIP-SOME-STOP"
      },
      "lifecycle": "planned",
      "remark": "Doorbell is broken, please knock on door, place on doormat when not home",
      "location": {
        "entity": {
          "id": "dff6de51-a24c-4f8b-8a12-ea8205577777",
          "name": "Shirley",
          "geoReference": {
            "lat": 52.32686,
            "lon": 4.88846,
            "type": "latLonPointGeoReference"
          },
          "type": "customer",
          "administrativeReference": {
            "name": "Shirley",
            "street": "Nieuw Herlaer 2",
            "postalCode": "1083 BD",
            "city": "Amsterdam",
            "country": "NL"
          },
          "contactDetails": [{
            "value": "Shirley",
            "type": "firstName"
          }, {
            "value": "Shirley de Boer",
            "type": "name"
          }, {
            "value": "+31612345678",
            "type": "phone"
          }, {
            "value": "shirleydeboer@shotmail.com",
            "type": "email"
          }]
        },
        "associationType": "inline"
      },
      "startTime": "2021-04-25T18:01:00Z",
      "endTime": "2021-04-25T18:03:00Z",
      "actions": [{
        "entity": {
          "id": "3f992c74-867d-41f0-a9db-794ffa364444",
          "lifecycle": "planned",
          "consignment": {
            "entity": {
              "id": "a359a0f6-d695-45d6-9fa8-afcfb7233333",
              "externalAttributes": {
                "orderId": "6028716",
                "userId": "599271",
                "trackTraceCode": "602CLV05",
                "orderType": "Subscriptions"
              },
              "type": "OVERIG",
              "goods": [{
                "entity": {
                  "id": "86511ae4-8956-4df3-b3c5-0c0efc355555",
                  "barCode": "x-0012",
                  "quantity": 1,
                  "productType": "Book",
                  "type": "items"
                },
                "associationType": "inline"
              }, {
                "entity": {
                  "id": "3ddcac3b-c349-4e08-bab5-f39925145678",
                  "barCode": "x-0006",
                  "quantity": 1,
                  "productType": "Newspaper",
                  "type": "items"
                },
                "associationType": "inline"
              }]
            },
            "associationType": "inline"
          },
          "startTime": "2021-04-25T18:01:00Z",
          "endTime": "2021-04-25T18:03:00Z",
          "actionType": "unload"
        },
        "associationType": "inline"
      }],
      "constraint": {
        "entity": {
          "id": "6d6e0ab3-9ae0-4041-96bf-047861332222",
          "value": {
            "startTime": "2021-04-25T18:00:00Z",
            "endTime": "2021-04-25T19:00:00Z",
            "type": "timeWindowsConstraint"
          }   
        },
        "associationType": "inline"
      },
      "actionType": "stop"
    },
    "associationType": "inline"
  }, {
    "entity": {
      "id": "7b77e80d-527b-4700-8239-8d077fd99999",
      "name": "2",
      "externalAttributes": {
        "stopId": "25/04/21|OTM5-EXAMPLE-TRIP-SOME-STOP-2"
      },
      "lifecycle": "planned",
      "remark": "Leave at neighbours when not at home.",
      "location": {
        "entity": {
          "id": "ece51c99-0a6c-458d-ab44-91c77549bbbb",
          "name": "Martin",
          "geoReference": {
            "lat": 52.31945,
            "lon": 4.88408,
            "type": "latLonPointGeoReference"
          },
          "type": "customer",
          "administrativeReference": {
            "name": "Martin",
            "street": "Aanloop 3",
            "postalCode": "1183SZ",
            "city": "Amstelveen",
            "country": "NL"
          },
          "contactDetails": [{
            "value": "Martin",
            "type": "firstName"
          }, {
            "value": "Martin Golf",
            "type": "name"
          }, {
            "value": "+31687654321",
            "type": "phone"
          }, {
            "value": "martingolf@igoogle.nl",
            "type": "email"
          }]
        },
        "associationType": "inline"
      },
      "startTime": "2021-04-25T18:04:00Z",
      "endTime": "2021-04-25T18:06:00Z",
      "actions": [{
        "entity": {
          "id": "79597251-0aad-4401-9e34-e6367ef4ffff",
          "lifecycle": "planned",
          "consignment": {
            "entity": {
              "id": "e5f0528c-32b8-4a57-af15-57c297db4444",
              "externalAttributes": {
                "orderId": "6021923",
                "userId": "199283",
                "trackTraceCode": "60ABC04",
                "orderType": "Subscriptions"
              },
              "type": "OVERIG",
              "goods": [{
                "entity": {
                  "id": "7a6f6039-2f01-40a7-a557-6f2dbf022222",
                  "barCode": "x-0013",
                  "quantity": 1,
                  "productType": "Champagne",
                  "type": "items"
                },
                "associationType": "inline"
              }]
            },
            "associationType": "inline"
          },
          "startTime": "2021-04-25T18:04:00Z",
          "endTime": "2021-04-25T18:06:00Z",
          "actionType": "unload"
        },
        "associationType": "inline"
      }],
      "constraint": {
        "entity": {
          "id": "30918d21-763c-4287-bc85-b6f2436f2222",
          "value": {
            "startTime": "2021-04-25T18:00:00Z",
            "endTime": "2021-04-25T19:00:00Z",
            "type": "timeWindowConstraint"
          }
        },
        "associationType": "inline"
      },
      "actionType": "stop"
    },
    "associationType": "inline"
  }]
}

Updating your planning

Now that you have seen the appropriate format, you should also be aware that it is possible to update a planning that you sent to us before. It is important that sent messages always contains a full state, so a full planning. Any IDs we receive, will always be matched to any previous data, wherever possible. A trip id (UUID) used, that remained the same matched to a previous trip ID, will update the known trip. A new trip id (UUID) will secondly be checked on externalAttributes>tripId. If the UUID and the tripId don't match with a previous id, the new trip ID will be considered as a new trip. Stop IDs that remain the same, will be considered as the previous sent stop or changes to the previous sent stop. New stop IDs are considered as new stops. Removed stop IDs will be marked as cancelled stops.