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.
Your account can make 1,000 requests per 15 minutes. The limit is shared across the whole account, not per endpoint and not per key, so every integration, script and key you have draws from the same budget. Every response tells you where you stand:
| Header | Meaning |
|---|---|
Sortly-Rate-Limit-Max | Requests allowed in the window. |
Sortly-Rate-Limit-Remaining | Requests you have left. |
Sortly-Rate-Limit-Reset | When the window resets. |
Go over the limit and you'll get 429 Too Many Requests. Watch Sortly-Rate-Limit-Remaining and back off before you hit zero. For bulk syncs, request 100 records per page rather than making many small calls.
Because the budget is account-wide, a job that runs flat out can starve everything else you have connected. If you run more than one integration, pace the heavy ones and lean on Webhooks for anything that needs to be current.
The API uses standard HTTP status codes. Errors return a JSON body with a message you can log or show:
{ "message": "Validation failed: Name can't be blank" }| Status | What it means |
|---|---|
400 Bad Request | A parameter is missing or malformed. |
401 Unauthorized | Missing, wrong or expired secret key. |
404 Not Found | That ID doesn't exist in your account. |
409 Conflict | Something else changed the record first. Re-read it and retry. |
422 Unprocessable Entity | Understood, but failed validation. The message says which field. |
429 Too Many Requests | You hit the rate limit. See Rate Limits. |
500 Server Error | Something broke on our side. Contact dev-support@sortly.com. |
Webhooks push changes to you as they happen, so you don't have to poll for them. Sortly can notify your endpoint whenever an item or folder changes, whenever a purchase order or a job is raised or moves status, and whenever an item runs low on stock.
Set them up at app.sortly.com/integrations/webhooks. You give Sortly an HTTPS URL to deliver to, and choose which events you want.
Items and folders
These four are the choices on the setup page. Each covers items and folders both, and two of them deliver more than one kind of change — body.verb is what separates those.
| Setting | Fires when | body.verb |
|---|---|---|
| Created | An item or folder is added. | create |
| Edited | Fields change, such as name, price or a custom field value. | edit |
| Edited | An item's quantity is updated in place. | quantity_change |
| Moved | An item or folder moves to a different parent folder. | move |
| Moved | Part of an item's quantity moves to another folder, leaving the rest behind. | quantity_move |
| Deleted | An item or folder is removed. | delete |
Read body.node_type to tell an item from a folder — it reads item or folder. The two quantity verbs are item-only, since a folder holds no quantity of its own.
If you are syncing stock levels, the verb you want is quantity_change, and it arrives under Edited rather than under a setting of its own. Renaming an item and changing its quantity reach you through that one subscription, so branch on body.verb and not on the setting you ticked — otherwise a rename looks like a stock movement.
Some verbs have no setting on the page today and arrive only on accounts that already had them stored: restore, quantity_merge, count, order, order_voided and order_received. Ignore a verb you don't recognise rather than failing on it; the list can grow.
Purchase orders
| Event | Fires when |
|---|---|
| Created | An order is raised, including one made by duplicating an existing order. |
| Status changed | The order reaches a different status. Every transition fires, including the partially_received and received ones that receiving sets rather than a status call. |
Jobs
| Event | Fires when |
|---|---|
| Created | A job is created. |
| Status changed | The job reaches a different status: starting it, completing it, or reopening a completed one. |
Low stock
| Event | Fires when |
|---|---|
| Low stock | A quantity alert on an item fires because the item ran low. |
Only the alerts that watch for a low level deliver this: less_than_or_equal_to_min_quantity, quantity_less_than and quantity_less_than_equal_to. Overstock alerts (quantity_greater_than, quantity_greater_than_equal_to) and DateReminder alerts do not fire a webhook.
What a delivery carries
Each event names its own resource in the top-level type, and its body is shaped for that resource. There is no shared envelope to unpack and no resource_type to switch on.
| Event | Top-level type | body holds |
|---|---|---|
| Item or folder, any change | sortly.company.transaction.created | The audit record for the change, including node_type and verb |
| Purchase order created | sortly.company.purchase_order.created | purchase_order |
| Purchase order status changed | sortly.company.purchase_order.status_changed | purchase_order and previous_status |
| Job created | sortly.company.job.created | job |
| Job status changed | sortly.company.job.status_changed | job and previous_status |
| Low stock | sortly.company.alert.low_stock | alert and item |
Item and folder changes all share one type, because on our side they are all one audit record — branch on body.verb and body.node_type for those. Every other event carries a type of its own, so branch on type alone and read the body directly.
body.purchase_order is the same shape GET /api/v1/purchase_orders/{purchase_order_id} returns, without line_items; its id is the one that endpoint takes, so read the order if you need its lines. previous_status arrives only on sortly.company.purchase_order.status_changed.
Abbreviated — the order carries every field the REST read does apart from line_items:
{
"type": "sortly.company.purchase_order.status_changed",
"body": {
"previous_status": "approved",
"purchase_order": {
"id": 4471,
"purchase_order_number": "PO-2026-0042",
"status": "ordered",
"currency_code": "USD",
"total": "26.75",
"updated_by": "Jane Smith",
"updated_at": "2026-09-04T09:10:11Z",
"version": 3
}
}
}body.job is the same shape GET /api/v1/jobs/{job_id} returns, without custom_field_values; its id is the one that endpoint takes, so read the job if you need them. previous_status arrives only on sortly.company.job.status_changed, and holds the status the job left.
body on a low stock delivery is shaped for the alert rather than for a record you can read back, so it carries the item inline:
{
"type": "sortly.company.alert.low_stock",
"body": {
"alert": {
"type": "out_of_stock",
"condition": "at or below min level",
"threshold_value": null
},
"item": {
"id": 4471,
"name": "Copper Pipe 15mm",
"quantity": "0.0",
"min_quantity": "10.0",
"parent_id": 33
}
}
}alert.type reads low_stock, or out_of_stock once the quantity reaches zero — both come through this one event, so handle them together unless you want to treat running out differently. alert.condition says which comparison fired: at or below min level, less than, or less than or equal. alert.threshold_value is the number the alert was configured with, and is null for less_than_or_equal_to_min_quantity, which compares against the item's own minimum level instead.
Quantities arrive as decimal strings rather than numbers, so compare them as decimals, not floats. item.min_quantity is null on an item that has no minimum level set, and item.id is the one GET /api/v1/items/{item_id} takes.
When to use webhooks instead of polling. If you need changes reflected in another system promptly, webhooks are the right tool and they cost you nothing against your rate limit. Polling GET /items/recent with updated_since is still useful as a safety net: run it on a slower schedule to catch anything a failed delivery missed. See Sync Inventory.
Purchase orders, jobs and low stock no longer need polling at all — subscribe to the events above instead of reading GET /purchase_orders, GET /jobs or GET /alerts on a schedule.
Check the setup page above for the exact payload your account receives before you write a handler against it.
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.
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.