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:
{
"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:
"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/ortripId
within theexternalAttributes
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.
"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 anendTime
. - A stop can have multiple
load
andunload
actions; these actions have aconsignment
. - A stop optionally has a
name
,remark
andsequenceNr
. - A stop optionally has a
stopId
within anexternalAttribute
, 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:
{
"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:
{
"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 inlineconsignment
, astartTime
and anendTime
- 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 asconsignmentId
,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):
{
"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:
"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
.
"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:
{
"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
:
"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:
"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':
{
"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:
"actors": [
{
"associationType": "attributeRestriction",
"restriction": {
"externalAttributes": {
"carrierId": "<my-carrier>"
}
},
"entityType": "actor",
"roles": ["carrier"]
}
]
And a consignment can look like this:
{
"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:
"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:
"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):
"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:
{
"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.
{
"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.