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.
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.
When a delivery arrives, record what turned up against the order, so Sortly stock goes up and the order moves towards received without anyone keying it in twice.
Receiving runs in the background: you start it, then poll until it finishes. The order must be ordered or partially_received.
1. Read the order for its line ids. You receive against line_items[].id, not against item ids, so fetch the order and match its lines to what arrived.
curl 'https://api.sortly.co/api/v1/purchase_orders/4471' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'Each line carries quantity and received_quantity. The most you can receive now is the difference between the two.
2. Start the receive, with up to 100 lines and what arrived on each. Nothing else is required. A successful call returns 202 straight away and the receive pending; nothing has been applied yet.
curl -X POST 'https://api.sortly.co/api/v1/purchase_orders/4471/receive' \
-H 'Authorization: Bearer YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{
"line_items": [ { "line_item_id": 204, "received_quantity": "12" },
{ "line_item_id": 205, "received_quantity": "3", "folder_id": 87 } ]
}'received_quantity is what arrived now, on top of anything received before, and no two lines in one request may point at the same item. folder_id is where the stock lands; leave it out and it goes to the top level, not to the folder the item is in. The item's quantity only goes up when you name the folder it is in and its price matches the line's unit_rate — otherwise Sortly makes a copy at that price and leaves the original alone.
To keep a line against the receive, add an action. notes is optional and is the only thing it carries; Sortly files the receive under its own transaction reason:
curl -X POST 'https://api.sortly.co/api/v1/purchase_orders/4471/receive' \
-H 'Authorization: Bearer YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{
"line_items": [ { "line_item_id": 204, "received_quantity": "12" } ],
"action": { "notes": "Delivered short, driver noted two boxes missing." }
}'3. Poll until it finishes. Only one receive runs against an order at a time, so there is nothing to carry between the two calls. Starting a second while the first is pending returns 409.
curl 'https://api.sortly.co/api/v1/purchase_orders/4471/receive/status' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'Lines can fail on their own while the call itself succeeds. Stop polling on
status, never onpending_line_item_idsemptying, and read the ids out rather than trusting the HTTP code.{ "data": { "status": "failed", "received_line_item_ids": [ 204 ], "pending_line_item_ids": [], "errors": [ { "line_item_id": 205, "name": "Copper Pipe 3/4 inch", "error": "Received quantity exceeds the remaining quantity on this line" } ], "error": null } }A
failedreceive with ids still inpending_line_item_idsanderrorsempty fell over before it reached those lines:errorcarries the reason and nothing on them was applied. Retry only the lines that did not go through. Theerrortext is written for a person to read, so do not match on it.
Receiving is what moves the order to partially_received and then received, which is why the status endpoint rejects both. A 404 from the status call means nothing has been received against this order yet.
Once stock has arrived, the order is ready to become a bill or an expense on the accounting side.
1. Find the orders that have been received.
Try it: GET /api/v1/purchase_orders
curl 'https://api.sortly.co/api/v1/purchase_orders?status=received,partially_received&per_page=100' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'2. Fetch each one for the detail a bill needs: line_items with their received_quantity, plus sub_total, total and terms.
curl 'https://api.sortly.co/api/v1/purchase_orders/4471' \
-H 'Authorization: Bearer YOUR_SECRET_KEY'Note. Bill for
received_quantity, notquantity, whenever an order is only partially received:quantityis what was ordered, and billing it would charge for stock that has not arrived. Make the handler idempotent on the Sortly order id as well, so a retry does not raise a second bill.
When the order is done with, close it so nobody keeps working against it. Read it for a current version, then move it to closed.
curl -X PUT 'https://api.sortly.co/api/v1/purchase_orders/4471/status' \
-H 'Authorization: Bearer YOUR_SECRET_KEY' \
-H 'Content-Type: application/json' \
-d '{ "status": "closed", "version": 4 }'closed is what Sortly calls a completed order, and it is final. It can be reached from ordered, partially_received or received.
A draft order cannot be closed, and it cannot be voided either: voiding is only available once an order is ordered. A draft you no longer want is deleted rather than given an end status. See Delete a Purchase Order.
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. That returns the folder's direct contents; add include_subfolders=true to get everything nested beneath it as well.
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.
Sep 3, 2026. Added include_subfolders to the items list, recent and search endpoints. It defaults to false, so a folder filter still returns only that folder's direct contents; set it to true to include items nested in child folders as well.
Aug 24, 2026. Removed the item group fields from the Items endpoints. item_group_id and option_value_ids are no longer accepted on create or update, the item group fields are no longer returned on any read, and include=options no longer embeds variant option values.
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.