Concurrency and deletes
Concurrent writes
Section titled “Concurrent writes”jtWMS protects data that two requests might change at the same moment. When two writes conflict,
for example two edits of the same webhook subscription, or a storage node created under a parent
that another request is deleting, the first to finish succeeds and the other gets
409 Conflict with errorCode concurrency_conflict:
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10", "title": "Conflict", "status": 409, "detail": "Another request changed this data at the same time. Reload and try again.", "errorCode": "concurrency_conflict", "traceId": "00-6f1c0a9d2b6e4c1f8a3b5d7e9f0a1b2c-4d5e6f7a8b9c0d1e-00", "correlationId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"}Nothing from the conflicting request was saved, and no event was published for it. Conflicts
can occur on warehouse configuration, products, stock, webhook subscriptions and deliveries, and on
changes to a user’s roles or a role’s permissions. Those two team writes use their own detail,
such as “The user’s roles changed while you were editing them. Reload and try again.”, with the
same errorCode.
Retrying a conflict
Section titled “Retrying a conflict”- Read the resource again, and anything your change depends on, such as a parent node.
- Decide whether your change still applies to what you read. Another request may have deleted the record or made your change unnecessary.
- Apply your change to the fresh data and send the request again.
An automated client can retry a few times with a short, randomised delay. Don’t resend the original request body unchanged without re-reading: it would overwrite the other request’s change.
Writes that touch the same part of the data can conflict even when they change different fields. In warehouse configuration, for example, two creates under the same storage parent, or two edges added to the same equipment node at once, can conflict. In inventory, two writes to the same stock position (two adjustments, a receipt adding to a position another request is adjusting, or a hold of stock another request is adjusting, moving or holding) and two receipts creating the same new position or lot at once can conflict, and so can writes to one license plate: stock moved or received into it while it is moved or deleted, two moves of one pallet, or a case put on a pallet that is being moved. Two allocations taking stock from the same position, an allocation and an adjustment, hold or move of that stock, and a pick and a release (or two picks) of one allocation can conflict too. Retrying resolves these.
Other conflicts
Section titled “Other conflicts”Other 409 responses mean the request doesn’t fit the resource’s current state. Retrying the same
request won’t help; read the detail, and change the request or the resource first.
errorCode |
When |
|---|---|
sku_conflict |
Another product already has the SKU, ignoring case. |
barcode_conflict |
Barcodes in the request are already assigned to another product (listed in barcodes). |
serial_conflict |
Serial numbers in the request are already on hand (listed in serials). |
lot_expiry_mismatch |
The lot already exists with another expiry date. |
insufficient_stock / stock_allocated |
An adjustment, move or hold would take a position below zero, or below its allocated quantity; or an allocation asks for more than is available (availableQuantity is included). |
stock_already_held / stock_not_held |
A hold names stock that is already held (release it first), or a release names stock that isn’t held. |
product_blocked / location_blocked / facility_blocked |
Stock can’t be put into a product, location or facility that isn’t active, nor allocated of a product or in a facility that isn’t. |
pick_exceeds_allocation |
A pick asks for more than its allocation line has left to pick. |
allocation_released |
The allocation was released: it can’t be picked or released again. |
lpn_code_conflict |
Another license plate in the facility already has the code, ignoring case. |
lpn_not_empty |
The license plate, or one on it, still holds stock (listed in lpnIds). |
lpn_location_mismatch |
The license plate named isn’t at the location named with it. |
product_already_active / product_already_inactive |
The product is already in the state you asked for. |
api_key_limit_reached |
The tenant has as many API keys as its plan allows. |
system_role_immutable |
A system role can’t be deleted, renamed or edited. |
user_inactive |
The user is deactivated. |
USER_ALREADY_EXISTS |
A user with this email already exists. |
| None | Many other conflicts, such as a code or name that is already taken, a storage node that has active children (see Cascade deletes), a zone profile or webhook subscription that is already in the state you asked for, a role that is still assigned to users, or a delivery that can’t be redelivered. Read the detail. |
Deletes are soft
Section titled “Deletes are soft”A DELETE never erases a record. It marks it deleted, and from then on the record disappears from
the API: reads of it return 404, lists leave it out, and nothing can change it. A deleted record
can’t be restored: there is no undelete, in the API or in the app.
Deleting is different from deactivating. An inactive record, such as a deactivated product or
webhook subscription, is still listed and can be read and reactivated with its activate
endpoint. Deactivate a record you may need again; delete one you don’t.
Revoking an API key works the same way: the key disappears from the list and stops working within about a minute, and it can’t be restored.
Removing a team member is the exception: DELETE /api/v1/team/users/{userId} deactivates the user
rather than deleting them. They stay in the team list, can’t sign in, and can be reactivated with
PATCH /api/v1/team/users/{userId} and {"isActive": true}.
Cascade deletes
Section titled “Cascade deletes”Deleting a record usually deletes what belongs to it. Deleting an equipment node, for example, also deletes its configuration, its location and its edges, and deleting an empty pallet deletes the empty cases and totes on it.
A storage node is the exception. By default, deleting a storage node that has active child nodes is
refused with 409 Conflict, and the response lists the children:
DELETE /api/v1/facilities/{facilityId}/storage-nodes/{id}{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10", "title": "Conflict", "status": 409, "detail": "Cannot delete a storage node that has active child nodes.", "childCount": 2, "childIds": [ "5a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "6b2e3d4c-5f6a-4b7c-9d8e-0f1a2b3c4d5e" ], "traceId": "00-6f1c0a9d2b6e4c1f8a3b5d7e9f0a1b2c-4d5e6f7a8b9c0d1e-00", "correlationId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"}To delete the node and everything under it, add ?cascade=true:
DELETE /api/v1/facilities/{facilityId}/storage-nodes/{id}?cascade=trueThe node, all its descendants, and the locations of every position among them are deleted together,
in one change: either all of them are deleted or, if the request fails, none are. The response is
204 No Content, and one jtWMS.WarehouseConfig.Events.V1.StorageNodeCascadeDeleted event lists
every node and location deleted.