The units available for tracking quantity: weight, length, volume and count.
Sortly API
The Sortly API lets you work with your inventory from your own systems: add and update items, keep stock levels in sync, and track what goes out on a job.
Available on the Sortly Enterprise Plan. All requests go to https://api.sortly.co over HTTPS and return JSON. Email dev-support@sortly.com with questions or issues.
New here? Start with Getting Started, then read Items and Folders so the vocabulary in the endpoint reference makes sense. Once you know what you're building, jump to the matching Use Case Guide.
Goal: keep Sortly jobs in step with the work orders in your external system, from the moment one is raised to the moment it closes, so field staff work against accurate inventory and nobody keys the same movement in twice.
A job is a work order. Creating one also creates a folder to hold the items assigned to it. Jobs move through three statuses: not_started, in_progress, completed.
Each subsection below maps to one event in your system. Your system owns the job lifecycle and drives each of these calls; where you need to see the state on the Sortly side, read it with GET /api/v1/jobs on whatever schedule suits you.
Rather than creating jobs by hand, listen for job creation in your external system and create the matching job in Sortly in response. Most systems can do this with a webhook or an outbound event; whatever your side emits when a work order is raised, use it to fire the call below. Field techs and warehouse staff then open the job in Sortly and assign items to it while they work.
Try it: POST /api/v1/jobs
curl -X POST 'https://api.sortly.co/api/v1/jobs' \
-H 'Authorization: Bearer YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{
"name": "WO-10042 Rooftop HVAC install",
"start_date": "2026-08-10",
"end_date": "2026-08-14",
"notes": "Bring spare filters",
"external_job_link": "https://app.servicetitan.com/jobs/10042",
"subfolders": [ { "name": "On Site" } ]
}'external_job_link takes a full URL, not a bare id: give it a properly formed link that opens this job in your own system, such as https://app.servicetitan.com/jobs/10042. Anyone looking at the job in Sortly can then follow it straight back to the source record.
Who the job belongs to. The job is assigned to the owner by default. Job members are not exposed through the API, so you cannot set them on this call. Once the job exists, anyone with an owner or admin role can add members to it through the Sortly app.
Store the mapping. Keep the returned data.id against your system's job id. You need it for every later call, and it saves you searching by name.
Job names are unique within an account, so creating one that already exists fails. Before creating jobs in bulk, list what is already there and skip the names you find:
Try it: GET /api/v1/jobs
curl 'https://api.sortly.co/api/v1/jobs?per_page=100&status=not_started,in_progress' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'The new job comes back in not_started. Keep the id and the version.
When work actually begins in your external system, move the Sortly job to in_progress so anyone looking at it knows the crew is on site and the folder is live.
1. Read the job to get its current version. Status changes are guarded by version, so always read immediately before writing.
Try it: GET /api/v1/jobs/{job_id}
curl 'https://api.sortly.co/api/v1/jobs/678' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'2. Move it to in_progress, passing the version from step 1.
Try it: PUT /api/v1/jobs/{job_id}/status
curl -X PUT 'https://api.sortly.co/api/v1/jobs/678/status' \
-H 'Authorization: Bearer YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{ "status": "in_progress", "version": 1 }'A 409 means someone changed the job between your read and your write. Re-read it and retry with the new version. Resending a status the job already has is safe, so a retried delivery does no harm. Moving a job backwards is rejected with 400.
When usage is recorded in your external system, move what was used off the truck and into the job folder in Sortly, so usage is recorded against the job and replenishment reflects it.
1. Find the job's folder. Read the job back. The response carries folder_id, which is where stock needs to land.
Try it: GET /api/v1/jobs/{job_id}
curl 'https://api.sortly.co/api/v1/jobs/678' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'2. Find the stock on the truck. List the truck folder's contents and match the items your system says were used.
Try it: GET /api/v1/items
curl 'https://api.sortly.co/api/v1/items?folder_id=55&per_page=100' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'3. Move the used quantities onto the job. Both endpoints below perform the same action: they move quantity out of the folder an item currently sits in and onto the job. Neither creates a new item.
Prefer Pull Items into Job. It takes up to 100 items in one request, and those items can sit in different folders, so a whole truck's worth of usage goes over in a single call.
Try it: POST /api/v1/jobs/{job_id}/items
curl -X POST 'https://api.sortly.co/api/v1/jobs/678/items' \
-H 'Authorization: Bearer YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{
"items": [ { "item_id": 12345, "quantity": 4 },
{ "item_id": 12346, "quantity": 1 } ],
"action": { "reason_id": "e36df891-596f-42a2-a561-3d127d9fa080" }
}'action.reason_id is required here. It is the reason written into each item's history.
- Every Sortly account carries the default move reason Added to Job,
e36df891-596f-42a2-a561-3d127d9fa080. Send that unless your account has its own. - Leave
actionout and the call is rejected with400 A reason is required for this action type. That holds even if you sendactionwith onlynotesin it. - Returning items needs no reason.
Pass item_version on an entry to have it rejected if the item changed since you read it, rather than applied over the top.
A partial failure still returns
200. Each item is processed on its own, so one bad row does not reject the rest. Checkdata.errorson every response rather than relying on the status code, and retry only the items listed there.{ "data": { "added_item_ids": [], "errors": [ { "item_id": 14494919, "name": "Copper Pipe", "error": "Invalid quantity for moving item 14494919: 999. Must be between 0 and 4.000000" } ] } }Note that moving part of a quantity splits the item, so an id in
added_item_idscan differ from the one you sent.
The move endpoint reaches the same end state and is available as an alternative, one item per call. Prefer Pull Items into Job either way, including for a single item: it is the call that expresses adding to a job, and it reports per-item failures. Set folder_id to the job's folder_id from step 1.
curl -X POST 'https://api.sortly.co/api/v1/items/12345/move' \
-H 'Authorization: Bearer YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{ "quantity": 4, "folder_id": 4200, "leave_zero_quantity": false }'On both endpoints quantity is how much to move, not the new total. On the move endpoint, set leave_zero_quantity: true if you want a zero-quantity record left behind rather than the item disappearing from that folder once it hits 0.
Stock lands in the job folder itself, never in a subfolder. Even if the job was created with subfolders, both endpoints drop the items at the top level of the job's folder. If you need them filed into a particular subfolder, do a second move on the item afterwards with folder_id set to that subfolder.
With the truck folder now drawn down, low stock alerts on those items fire as normal and drive replenishment. See Track Stock Levels.
Goal: keep Sortly purchase orders in step with the accounting or ordering system you buy through, so both sides agree on what was ordered, what it costs and where it has got to.
A purchase order records what you ordered from a vendor and on what terms, with a list of line items drawn from your inventory. You don't need to send amount, sub_total, total or line_number: Sortly works them out from the lines.
How an order moves:
draft,ready_for_reviewandapprovedcan each move to one another, or on toordered- From
orderedyou can requestvoidedorclosed - From
ordered, receiving stock also moves the order topartially_receivedand thenreceived, through the receive flow rather than a status call - From
partially_receivedorreceived, the only status you can request isclosed voidedandclosedare final
For the transitions themselves, see Manage PO Statuses.
Goal: move a Sortly purchase order to match where it has got to in your own process, whether that decision was made in another system or over email.
Every transition uses the same endpoint and the same two fields: the target status and the version you got from your last read.
- A stale
versionis rejected with409. Read immediately before writing, and retry with the new one. - Sending a status the order already has changes nothing, so a retried delivery is safe.
| From | Can move to |
|---|---|
draft, ready_for_review, approved | each other, or ordered |
ordered | voided or closed |
partially_received, received | closed |
voided, closed | final |
Anything outside that returns 400. received and partially_received cannot be requested here at all: Sortly sets them when a delivery is recorded against the order, either in the app or through the receive endpoint.
Items are the things you track: stock, tools, assets, supplies. Each one has a quantity and can carry a price, photos, tags and custom fields.
Items and folders share these endpoints; type is what tells them apart. Pass "type": "item" here. For the folder side of the same endpoints, see Folders.
Folders are how you organize inventory by location, category, job, or whatever fits how you work. They can be nested, and items live inside them.
Folders use the same endpoints as items, with "type": "folder" and no quantity or price. Nest one inside another by setting parent_id; leave it out for the top level. To list a folder's contents, pass its id as folder_id when listing items.
Purchase orders record what you have ordered from a vendor and on what terms. Each one carries vendor and ship-to details plus a list of line items drawn from your inventory.
How an order moves:
draft,ready_for_reviewandapprovedcan each move to one another, or on toordered- From
orderedyou can requestvoidedorclosed - Receiving stock moves an
orderedorder topartially_receivedand thenreceived, through the receive flow rather than a status call - From
partially_receivedorreceived, the only status you can request isclosed voidedandclosedare final
Use the status endpoint for these transitions rather than the update endpoint.
Aug 5, 2026. Added Purchase Orders: list, create, fetch, update and change status.
Jul 31, 2026. Added Jobs: list, create, fetch, update, change status and delete, plus adding items to a job and returning them.
Jun 17, 2021. Added yard and gallon units of measure.
Jun 5, 2021. Added support for item variants (Item Groups).
Jan 4, 2021. Renamed attribute_value to value on custom field values (attribute_value still works). Added tags to item reads and search.
Sep 1, 2020. Added the search endpoint.
Jun 15, 2020. Added alerts.
Mar 2, 2019. First release.