メインコンテンツまでスキップ

Create and update operations locations

There are two ways to get an Operations Location into the system:

ApproachEndpointUse it when
REST resourcePOST /api/v2/operationslocationYou are creating a brand new location and want to define its Group and Time Windows inline.
Bulk taskPOST /api/v2/microservices/bulk/task with type: add_update_operations_locationYou are synchronising locations from an external master system, need create-or-update (upsert) semantics, or need to link allowed vehicle types.

Approach 1: create with the REST resource

The Operations Location API supports creating an Operations Location along with its related Time Windows and Operations Location Group in a single API request, rather than creating them sequentially and linking them by IDs.

This significantly simplifies the setup process for new locations.

How it works

Instead of providing the group ID or creating time_windows separately, you can embed the full definition of the group and the list of time windows directly in the POST request body for the location.

Example Request

Below is an example of a single request that creates:

  1. A new Operations Location ("Location name").
  2. A new Operations Location Group (code "E9119").
  3. A Time Window ("Thursday Day 1") for the location.
Create a location with its group and time windows
POST /api/v2/operationslocation

{
"address": "Location address",
"code": "QHCode",
"name": "Location name",
"postal_code": "520710",
"point": {
"coordinates": [
101.917644661626,
12.15892431398393
],
"type": "Point"
},
"group": {
"code": "E9119",
"project": "/api/v2/project/888"
},
"project": "/api/v2/project/888",
"time_windows": [
{
"time_window": {
"name": "Thursday Day 1",
"open_time_ts": "1900-01-01T08:30:00+00:00",
"close_time_ts": "1900-01-01T13:00:00+00:00",
"recurrences": "DTSTART:20240905T184432Z\nRRULE:FREQ=DAILY;BYDAY=TH",
"strict": true,
"public_holiday": false,
"project": "/api/v2/project/888"
}
}
]
}

By using this structure, the server handles the creation and linking of all entities in a single transaction.

Approach 2: upsert with the add_update_operations_location bulk task

The add_update_operations_location bulk task writes the location, its time windows, its allowed vehicle types and its default category in a single transaction, and it is idempotent: the same request can be replayed and it will update the existing location instead of creating a duplicate.

This is the approach to use when a TMS/OMS/ERP owns the location master and pushes changes to SWAT on a schedule.

Step 1: Retrieve the available vehicle types

Vehicle types are linked by their numeric IDs, so first resolve the IDs for the project. Use the generic data endpoint and restrict the payload with only_fields.

GET /api/v2/microservices/get?model=vehicletype&project_id=888&only_fields=id,name
Response
{
"meta": {
"has_more": false,
"next": null,
"next_offset": null
},
"objects": [
{ "id": 41, "name": "small_van" },
{ "id": 42, "name": "light_truck" },
{ "id": 43, "name": "heavy_truck" }
]
}
ヒント

Restrictions are a three-way link between a location, a vehicle type and a simulation, so you also need the target simulation IDs. Retrieve them the same way with model=simulation&project_id=<project_id>&only_fields=id,name,start_time. Linking a vehicle type to a simulation template makes the restriction apply to every simulation later created from that template.

Step 2: Retrieve the available categories

Skip this step if the location does not need a category. Categories are referenced by their numeric IDs too, so resolve them the same way. Filtering out invalidated categories avoids picking one the task will reject.

GET /api/v2/microservices/get?model=operationslocationcategory&project_id=888&only_fields=id,name,external_id&not__is_invalidated=true
Response
{
"meta": {
"has_more": false,
"next": null,
"next_offset": null
},
"objects": [
{ "id": 77, "name": "Chilled dock", "external_id": "CAT-CHILLED" },
{ "id": 78, "name": "Residential", "external_id": "CAT-RES" },
{ "id": 79, "name": "Secure compound", "external_id": "CAT-SEC" }
]
}

Pass the chosen ID as operations_location.default_category_id in the next step — the example uses 77 (Chilled dock).

ヒント

If your system already knows the category by its own identifier, filter on it directly with &external_id=CAT-CHILLED instead of matching on name.

Step 3: Create or update the location

Post the task to POST /api/v2/microservices/bulk/task. The example below creates (or updates) location EXT-001, assigns it the Chilled dock category (77), allows small_van (41) and light_truck (42) in simulations 118936 and 120419, and attaches two time windows.

Create or update an operations location
POST /api/v2/microservices/bulk/task

{
"project_id": 888,
"type": "add_update_operations_location",
"execution_mode": "sync",
"payload": {
"operations_location": {
"external_id": "EXT-001",
"name": "Jurong Cross Dock",
"address": "1 Jurong Port Road",
"postal_code": "619092",
"lat": 1.31682,
"lon": 103.71083,
"group_id": 4501,
"default_category_id": 77,
"is_invalidated": false,
"data": {
"remarks": "Dock 3 only"
}
},
"vehicle_types": {
"41": [118936, 120419],
"42": [118936]
},
"time_windows": [
{
"name": "Weekday morning",
"open_time_ts": "1900-01-01T08:30:00+00:00",
"close_time_ts": "1900-01-01T13:00:00+00:00",
"recurrences": "DTSTART:20240905T184432Z\nRRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
},
{
"name": "Saturday",
"open_time_ts": "1900-01-01T09:00:00+00:00",
"close_time_ts": "1900-01-01T12:00:00+00:00",
"recurrences": "DTSTART:20240905T184432Z\nRRULE:FREQ=DAILY;BYDAY=SA"
}
]
}
}

Request fields

The task envelope carries the access-control context; the payload carries the location itself.

FieldTypeDescription
project_idIntegerRequired. Project the location belongs to. The caller must have allow_manage rights on it. Must be set here — passing project_id inside payload.operations_location is rejected.
simulation_idIntegerNot used by this task. Omit it. If sent it is only validated against the project (the request fails with Simulation does not exist if it does not belong there) and is otherwise ignored — simulation scoping for this task comes from payload.vehicle_types.
typeStringRequired. Must be add_update_operations_location.
execution_modeEnumsync or async. Defaults to async.
payload.operations_locationObjectRequired. The location definition. See below.
payload.vehicle_typesMap<String, List<Int>>Vehicle type ID → list of simulation IDs in which that vehicle type is allowed to serve the location. Keys are the numeric vehicle type IDs as strings; each list must be non-empty and contain no duplicates. See Simulation IDs are mandatory.
payload.time_windowsList<Object>Time windows to link to the location.
payload.propagate_vehicle_types_fromDateTime (ISO8601)Optional. See Propagating to descendant simulations.

operations_location

FieldTypeDescription
external_idStringThe identifier from your system. This is the upsert key — see Upsert behaviour.
nameStringDisplay name. Defaults to "Operations Location".
latFloatRequired. Latitude.
lonFloatRequired. Longitude.
addressStringFree-form address.
postal_codeStringPostal code.
group_idIntegerOperationsLocationGroup to attach the location to. Must belong to the same project and must not be invalidated.
default_category_idIntegerProject-scoped OperationsLocationCategory used as the location's default category. Must belong to the same project and must not be invalidated. See Default category.
is_invalidatedBooleanSoft-deletion flag. Defaults to false.
dataObjectCustom per-location data.
警告

The task deliberately does not accept id, project_id, code, point, h3, calculation_params, search_index, categories_ids, created_at or modified_at. Sending any of them fails validation rather than being silently ignored. Use coordinates (lat/lon) instead of point, and set calculation_params through the Operations Location API.

time_windows

FieldTypeDescription
nameStringFree-form name.
open_time_tsDateTime (ISO8601)Window opening time.
close_time_tsDateTime (ISO8601)Window closing time.
recurrencesStringSerialized recurrence rule (DTSTART/RRULE).

Time windows here also reject id, project_id, strict, public_holiday, public_holidays, created_at and modified_at.

Upsert behaviour

The task looks for an existing location by project_id + external_id.

  • No match — a new OperationsLocation is created.
  • Match — the existing location is updated in place. name, group_id, address, postal_code, lat/lon, data and is_invalidated are all overwritten with what you send.
注意

is_invalidated defaults to false. If you omit it when updating a location that was previously soft-deleted, the location is reactivated. Send "is_invalidated": true explicitly to keep it invalidated.

Because the whole location object is overwritten, always send the complete desired state — omitted optional fields are cleared, not preserved.

Vehicle types and time windows are synchronised, not merged

Both vehicle_types and time_windows describe the full desired state of the links:

  • Links present in the payload but missing in the database are created.
  • Links present in the database but missing from the payload are deleted.

So sending "vehicle_types": {} removes every vehicle type restriction from the location, and omitting a previously linked time window unlinks it. The response reports exactly which link rows were created and deleted.

Time windows are matched against existing OperationsTimeWindow rows in the same project on the triple (open_time_ts, close_time_ts, recurrences). A matching window is reused; otherwise a new one is created. name is not part of the match, so an existing window is reused even if you give it a different name.

危険

Because both collections are synchronised rather than merged, an update that omits them is destructive. Sending only operations_location to correct a typo in an address will strip every vehicle type restriction and every time window from that location. Always send the complete desired state.

A vehicle type restriction is a three-way link between a location, a vehicle type and a simulation — the simulation is a required part of the record, so there is no project-wide form of the restriction. This means:

  • You cannot pass an empty list: "41": [] fails validation.
  • A link created for simulation 118936 has no effect in any other simulation. To cover a recurring plan, link against the simulation template — the restriction is then copied into every simulation created from it — and use propagate_vehicle_types_from for simulations that already exist.

What happens if you omit vehicle_types or time_windows

Both fields default to empty, so omitting them is the same as sending an empty collection.

OmittedEffect on the locationEffect on optimization
vehicle_typesAll existing vehicle type links are deleted.The location becomes unrestricted — no {"or": [...]} clause is added to node.vehicle_labels, so any vehicle may serve it, subject only to the booking's own label requirements.
time_windowsAll existing time window links are deleted.Bookings referencing the location are no longer time-enriched. A booking that supplies its own min_/max_pickup_time (or dropoff) is unaffected, because location time windows are only applied when the booking leaves those fields empty. A booking with neither ends up with no time constraint at that stop.
注意

Unlike calculation_params, time windows have no group-level fallback — they are resolved per location only. Removing them from a location leaves nothing to inherit.

Default category

An Operations Location Category is a project-scoped label that carries its own calculation_params — service times, entry/exit times and cumulative limitations shared by every location of that kind.

Categories are attached to locations by an assignment record that links a location, a category and an optional simulation. default_category_id is a shortcut for the assignment without a simulation, which is the category the location uses in every simulation:

You sendResult
An idThe default assignment is created, or repointed to the new category if one already exists.
NothingAny existing default assignment is deleted, the same as for vehicle_types and time_windows.

The category is validated inside the write transaction: it must belong to the same project as the location and must not be invalidated, otherwise the entire task fails and nothing is persisted.

The task never touches simulation-scoped assignments. To override the category for one simulation only, use the category assignment API directly.

注意

A category replaces the location's own calculation_params rather than filling gaps in them — see Precedence. Assigning a default category to a location that already carries its own parameters will change how that location is planned.

The default_category_id returned in the response is read back from the database after the assignment is written, so it reflects the stored state rather than what you sent.

Propagating to descendant simulations

When you link vehicle types to a simulation template, set propagate_vehicle_types_from to an ISO8601 timestamp to also apply those links to simulations already created from that template whose start_time is at or after the given moment.

payload fragment
{
"propagate_vehicle_types_from": "2026-09-01T00:00:00+00:00",
"vehicle_types": {
"41": [112987]
}
}

Here 112987 is a template. Every simulation created from it that starts on or after 1 September 2026 also gets the link. Simulations that started earlier are left untouched, so past plans are not rewritten. Explicit and propagated links are de-duplicated.

Response

In sync mode the result is returned inline:

Response
{
"result": {
"operations_location_id": 90210,
"operations_location": {
"id": 90210,
"project_id": 888,
"external_id": "EXT-001",
"name": "Jurong Cross Dock",
"group_id": 4501,
"default_category_id": 77,
"address": "1 Jurong Port Road",
"postal_code": "619092",
"is_invalidated": false,
"lat": 1.31682,
"lon": 103.71083,
"data": { "remarks": "Dock 3 only" }
},
"operations_locations_created_id": 90210,
"operations_locations_updated_id": null,
"operations_locations_vehicle_types_created_ids": [7001, 7002, 7003],
"operations_locations_vehicle_types_deleted_ids": [],
"operations_locations_time_windows_created_ids": [8001, 8002],
"operations_locations_time_windows_deleted_ids": [],
"time_windows_created_ids": [5501]
}
}

operations_locations_created_id and operations_locations_updated_id are mutually exclusive — exactly one is populated, which tells you whether the call created a new location or updated an existing one.

In async mode the endpoint returns a task identifier instead:

Response
{ "task_id": "52fb6f16-ff16-4db9-933f-607fcb61e1ee" }

Poll for completion and read the same result payload with:

GET /api/v2/microservices/get?model=bulktask&id=52fb6f16-ff16-4db9-933f-607fcb61e1ee
ヒント

add_update_operations_location handles one location per task and has no size limit in sync mode, so sync is usually the simpler choice — you get the created and deleted IDs back immediately. Use async when you are queueing a large number of locations and do not want to block on each one.

All-or-nothing writes

Every write in the task — the location, the time windows, the vehicle type links and the default category assignment — happens inside one database transaction. If validation of the group or category fails, or any write fails, nothing is persisted.

This guarantee covers one task message. When you send several at once, see Sending several locations in one request below.

Sending several locations in one request

A single task carries exactly one location — operations_location is an object, not a list. To cover several locations in one call, post a JSON array of task messages instead of a single object. Each element is a complete message with its own project_id, type and payload.

Batch of two locations
POST /api/v2/microservices/bulk/task

[
{
"project_id": 888,
"type": "add_update_operations_location",
"payload": {
"operations_location": {
"external_id": "EXT-001",
"name": "Jurong Cross Dock",
"lat": 1.31682,
"lon": 103.71083
}
}
},
{
"project_id": 888,
"type": "add_update_operations_location",
"payload": {
"operations_location": {
"external_id": "EXT-002",
"name": "Tuas Depot",
"lat": 1.32115,
"lon": 103.63790
}
}
}
]

The response is the list of submitted messages echoed back in order, each with its result filled in — the same result object described in Response.

The batch form behaves differently from a single message in ways that matter:

Single messageArray of messages
Executionsync or asyncAlways synchronous. execution_mode is ignored and no task_id is returned.
PermissionsChecked for the one projectChecked for every message before any of them run. One project you cannot manage means nothing executes.
FailureNothing is writtenLocations already processed stay written
危険

There is no transaction spanning the array. Each location is committed in its own transaction as the server works through the list in order, so a failure partway through leaves the earlier locations already applied while the later ones are never attempted.

The request fails as a whole with a 500 and an error message, and no per-item results — so the response does not tell you how far it got. You have to re-read the locations to find out.

Because the task is idempotent on project_id + external_id, the safe recovery is simply to resend the whole batch: locations that were already written are updated in place rather than duplicated.

ヒント

No size limit is enforced on the array, but the whole batch runs inline within a single HTTP request, so large batches risk a timeout rather than a validation error. For a bulk synchronisation of many locations, prefer one message per location in async mode — each gets its own task_id and fails independently — and keep the array form for small batches you are willing to resend in full.