Bulk import
Setting up jtWMS usually starts with data you already have: a product list, the people who will use the system. These endpoints take that data in batches, so a script or an integration can load it in a few calls.
The web app’s Import data page reads CSV files and sends them to the same endpoints:
- each row of its locations template is a position. Rows in areas the facility doesn’t have go in one topology import; rows in existing areas are created node by node (see Locations);
- each row of its products template becomes one product (the base unit and up to two packs as
unitsOfMeasure), sent 500 at a time; - each row of its starting stock template becomes one receipt line, sent 100 at a time (see Starting stock);
- each row of its team members template becomes one entry of
invites, sent 100 at a time after a dry run.
Locations
Section titled “Locations”A facility’s locations are its storage tree’s positions: each Position node owns one location,
whose code the facility’s code pattern builds from the codes of the node and its ancestors. Storage
node codes are unique across a facility. Two endpoints create them, both needing
warehouseconfig.topology.manage:
POST /api/v1/facilities/{facilityId}/topology/import?mode=additivecreates whole new areas, with everything under them and each position’slocationAttributes, all or nothing. A code the facility already has is a409listing theconflictingCodes, and nothing is saved. The document is the oneGET …/topology/exportreturns, withformatVersion1.0.POST /api/v1/facilities/{facilityId}/storage-nodescreates one node under aparentId, for adding to an area that exists. A position created this way gets default attributes; set its location’s withPUT /api/v1/facilities/{facilityId}/locations/{locationId}, finding thelocationIdinGET …/storage-nodes/tree.
Starting stock
Section titled “Starting stock”POST /api/v1/facilities/{facilityId}/inventory/receipts receives up to 100 lines as one receipt,
all or nothing. It needs inventory.stock.manage. Lines name products, locations and license
plates by id: find them by SKU in GET /api/v1/products/snapshot, by code in
GET /api/v1/facilities/{facilityId}/locations?search=, and by plate code in
GET …/inventory/lookup?code=. Create a missing plate with POST …/inventory/lpns
(inventory.movements.manage).
A receipt is not repeated for you, so make a batch safe to send again with its reference: before
sending, read GET …/inventory/transactions?type=Receipt&reference=<reference>, and skip the batch
when the ledger already has it. The web app’s references are import: followed by the first 12
hex digits of the SHA-256 of the batch’s lines and the batch’s number.
Products and locations reach the stock service through events, so for a moment after you create
one a receipt can answer 404 with errorCode product_not_found or location_not_found. Wait
and send it again: the web app retries with growing pauses for up to a minute.
Products
Section titled “Products”POST /api/v1/products/bulk creates or updates up to 500 products in one request, matched by SKU
(ignoring case). It needs catalog.products.manage.
- All or nothing. If any product is invalid, nothing is saved and the response lists every
problem, keyed by the product’s position (
Products[3].Sku). - Safe to repeat. A product whose SKU already exists is replaced with what you send; one that
hasn’t changed is reported
unchanged. Sending the same file twice changes nothing the second time, so a script can split a large list into batches of 500 and simply retry a batch that failed. - The response counts the products
created,updatedandunchanged, and lists each SKU with its id and result.
Team members
Section titled “Team members”POST /api/v1/team/invites/bulk invites up to 100 people in one request. Each person is emailed a
link to set up their account. It needs identity.users.manage.
{ "invites": [ { "email": "ana@example.com", "roleIds": ["<role id>"] }, { "email": "ben@example.com", "roleIds": [] } ], "expiresInDays": 7, "replaceExisting": false}Find role ids with GET /api/v1/team/roles. The request is checked as a whole first, and refused
with nothing saved when:
- a row is malformed (
400, keyed by the row, such asInvites[1].Email); - a role id isn’t one of your organisation’s roles (
400witherrorCodeinvite_role_unknown, keyed by the row’sInvites[i].RoleIds); - a role grants a permission you don’t hold yourself (
403witherrorCodepermission_not_held): see Permissions and scopes; - the invites would pass your organisation’s daily limit of 200 (
429witherrorCodeinvite_daily_limit). Single and bulk invites count together, over the last 24 hours. Requests sent at the same moment are each checked against the invites saved before them, so a burst of them can go slightly past the limit.
Otherwise each row gets its own result, in the order you sent them, and the rows that can be invited are, whatever happens to the others:
status |
Meaning |
|---|---|
created |
Invited. The row carries the invite’s inviteId, inviteUrl and expiresAt. |
duplicate_in_request |
The same email appears earlier in the request (ignoring case). |
already_member |
Someone in your organisation already uses this email. |
email_in_use |
The email belongs to a jtWMS account elsewhere, so it can’t be invited. |
already_invited |
The email has an invite waiting. Send "replaceExisting": true to send a new one; the old link then stops working. |
Add ?dryRun=true to see the results without inviting anyone or sending email, for example to
check a list before sending it.