Skip to content

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.

Download OpenAPI description
Languages
Servers
https://api.sortly.co

Getting Started

Make your first Sortly API call in about five minutes.

Guides

The mechanics every integration needs, whatever you are building: how to authenticate, how to page through results, what the rate limit costs you, and what Sortly sends back when something goes wrong.

Core Concepts

The vocabulary the endpoint reference assumes you already have. Read these once and the field names in the reference stop needing explanation.

Build Your Inventory

Goal: get your existing inventory into Sortly from a spreadsheet, database or another system.

Every command below runs as-is once you swap in your own secret key.

Manage Your Inventory

Goal: keep the inventory you have already loaded accurate as it changes: what it looks like, how much of it there is, and where it sits.

Track Stock Levels

Goal: be told when something is running low, instead of watching it yourself.

Sync Inventory

Goal: keep another system (an ERP, a warehouse tool, a dashboard) in step with Sortly.

Sync Jobs

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.

Sync Purchase Orders

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_review and approved can each move to one another, or on to ordered
  • From ordered you can request voided or closed
  • From ordered, receiving stock also moves the order to partially_received and then received, through the receive flow rather than a status call
  • From partially_received or received, the only status you can request is closed
  • voided and closed are final

For the transitions themselves, see Manage PO Statuses.

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 version is rejected with 409. 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.
FromCan move to
draft, ready_for_review, approvedeach other, or ordered
orderedvoided or closed
partially_received, receivedclosed
voided, closedfinal

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

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.

Operations

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.

Operations

Custom Fields

The extra fields defined on your account. Fetch these to get the custom_attribute_id values you need when writing custom field values onto items.

Operations

Units of Measure

The units available for tracking quantity: weight, length, volume and count.

Operations

Alerts

Alerts tell your team when stock runs low, or when a date you're tracking is coming up. Two kinds: Quantity and DateReminder.

Operations

Purchase Orders

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_review and approved can each move to one another, or on to ordered
  • From ordered you can request voided or closed
  • Receiving stock moves an ordered order to partially_received and then received, through the receive flow rather than a status call
  • From partially_received or received, the only status you can request is closed
  • voided and closed are final

Use the status endpoint for these transitions rather than the update endpoint.

Operations

Fetch a Purchase Order

Request

Returns a single purchase order by its ID, including notes, terms, sub_total and the full line_items list that the list endpoint leaves out.

Security
BearerAuth
Path
purchase_order_idnumberrequired

ID of the purchase order

Example: 506
curl -i -X GET \
  https://api.sortly.co/api/v1/purchase_orders/506 \
  -H 'Authorization: Bearer <YOUR_TOKEN_HERE>'

Responses

OK

Headers
Sortly-Rate-Limit-Maxstring
Sortly-Rate-Limit-Remainingstring
Bodyapplication/json
dataobject
Response
application/json
{ "data": { "id": 506, "purchase_order_number": "PO-2026-0042", "status": "draft", "currency_code": "USD", "charges": {}, "sub_total": "138", "total": "142", "line_items": [], "created_by": "Jane Smith", "created_at": "2026-08-03T08:00:00.000000Z", "updated_by": "Jane Smith", "updated_at": "2026-08-03T08:00:00.000000Z", "version": 1, "vendor": {}, "ship_to": {}, "bill_to": null, "notes": "Deliver to the rear entrance.", "terms": "Net 30", "expected_delivery_date": "2026-08-20T00:00:00.000000Z", "submitted_date": null, "submitted_by": null, "approved_date": null, "approved_by": null, "ordered_date": null, "ordered_by": null, "received_date": null, "received_by": null } }

Update a Purchase Order

Request

Replaces the purchase order's own fields.

  • Any nullable field you omit or send as null is cleared, so fetch the order first and send it back with your changes.
  • line_items replaces the whole list: a line with an id is updated, a line without one is added, and any stored line you leave out is removed.
  • On a line that already exists you cannot change item_id, name or variant_options, and lines cannot be reordered.
  • Only a draft, ready_for_review or approved order can be updated. Anything later returns 400.
  • Use the status endpoint to change status.
Security
BearerAuth
Path
purchase_order_idnumberrequired

ID of the purchase order

Example: 506
Bodyapplication/json
purchase_order_numberstringrequired

Must be unique within the company. Max 20 characters.

vendorobject

The vendor the order is placed with, captured on the purchase order itself.

ship_toobject

Shipping address, captured on the purchase order itself.

bill_toobject

Billing address, captured on the purchase order itself.

notesstring

Max 4000 characters.

termsstring

Max 4000 characters.

currency_codestringrequired

3-letter ISO 4217 currency code, e.g. USD.

chargesobject

Order-level charges as decimal strings. Each defaults to 0. total is sub_total + tax + shipping_cost - discount, so a discount larger than the rest of the order is rejected with 400.

expected_delivery_datestring

When the order is expected to arrive, as an ISO 8601 timestamp.

line_itemsArray of objectsrequired

The purchase order's lines after the update, up to 100. An existing line left out is removed; an empty array removes every line.

line_items[].​idnumber

ID of an existing line on this purchase order. Omit to add a new line. An id that is not on this purchase order returns 400.

line_items[].​item_idnumber or null

ID of the Sortly item to order. Required when you add a line. On a line that already exists send it back unchanged, including null if the item was deleted.

line_items[].​namestringrequired

Name to record on the line. Max 191 characters.

line_items[].​item_numberstring

Max 25 characters.

line_items[].​quantitystringrequired

Decimal string, e.g. "10" or "2.5".

line_items[].​unit_ratestring

Price per unit as a decimal string. Omit for a line with no price.

line_items[].​unit_of_measureobjectrequired
line_items[].​unit_of_measure.​namestringrequired

Abbreviation of one of the company's units of measure, e.g. units, ft, kg. An abbreviation the company does not have is rejected with 404.

line_items[].​variant_optionsArray of objects

Up to 3 options, and only settable when you add a line. Leave them out on a line that already exists, or send them back exactly as you read them.

line_items[].​notesstring

Max 314 characters.

versionnumberrequired

The version returned by the last read of this purchase order.

curl -i -X PUT \
  https://api.sortly.co/api/v1/purchase_orders/506 \
  -H 'Authorization: Bearer <YOUR_TOKEN_HERE>' \
  -H 'Content-Type: application/json' \
  -d '{
    "purchase_order_number": "PO-2026-0042",
    "currency_code": "USD",
    "vendor": {
      "name": "Ace Plumbing Supply",
      "email": "orders@aceplumbing.example",
      "line1": "567 Main St",
      "city": "San Francisco",
      "state": "CA",
      "zip_code": "94106",
      "country": "US",
      "phone": "+14155550100"
    },
    "notes": "Deliver to the rear entrance. Call on arrival.",
    "terms": "Net 30",
    "charges": {
      "tax": "2.50",
      "shipping_cost": "3.00",
      "discount": "0"
    },
    "expected_delivery_date": "2026-08-22T00:00:00Z",
    "line_items": [
      {
        "id": 881,
        "item_id": 204,
        "name": "Copper Pipe 3/4 inch",
        "item_number": "CP-075",
        "quantity": "12",
        "unit_rate": "12.50",
        "unit_of_measure": {
          "name": "ft"
        },
        "notes": "Match existing run"
      }
    ],
    "version": 1
  }'

Responses

OK

Headers
Sortly-Rate-Limit-Maxstring
Sortly-Rate-Limit-Remainingstring
Bodyapplication/json
dataobject
Response
application/json
{ "data": { "id": 506, "purchase_order_number": "PO-2026-0042", "status": "draft", "currency_code": "USD", "charges": {}, "sub_total": "150", "total": "155.5", "line_items": [], "created_by": "Jane Smith", "created_at": "2026-08-03T08:00:00.000000Z", "updated_by": "Jane Smith", "updated_at": "2026-08-03T09:15:00.000000Z", "version": 2, "vendor": {}, "ship_to": null, "bill_to": null, "notes": "Deliver to the rear entrance. Call on arrival.", "terms": "Net 30", "expected_delivery_date": "2026-08-22T00:00:00.000000Z", "submitted_date": null, "submitted_by": null, "approved_date": null, "approved_by": null, "ordered_date": null, "ordered_by": null, "received_date": null, "received_by": null } }

Change a Purchase Order's Status

Request

Moves the purchase order to a new status and leaves every other field alone.

  • draft, ready_for_review and approved can each move to one another, or on to ordered.
  • From ordered you can request voided or closed. From partially_received or received, only closed.
  • voided and closed are final. Anything else returns 400. - received and partially_received cannot be requested here. Receiving sets them as stock arrives, which also moves an order on from ordered without a status call.
  • Moving to ordered records who ordered it and when, and puts the order's items on order in Sortly.
  • Sending the status the order already has changes nothing, so a retry is safe.
Security
BearerAuth
Path
purchase_order_idnumberrequired

ID of the purchase order

Example: 506
Bodyapplication/json
statusstringrequired

One of draft, ready_for_review, approved, ordered, voided, closed. received and partially_received cannot be requested.

versionnumberrequired

The version returned by the last read of this purchase order.

curl -i -X PUT \
  https://api.sortly.co/api/v1/purchase_orders/506/status \
  -H 'Authorization: Bearer <YOUR_TOKEN_HERE>' \
  -H 'Content-Type: application/json' \
  -d '{
    "status": "ordered",
    "version": 2
  }'

Responses

OK

Headers
Sortly-Rate-Limit-Maxstring
Sortly-Rate-Limit-Remainingstring
Bodyapplication/json
dataobject
Response
application/json
{ "data": { "id": 506, "purchase_order_number": "PO-2026-0042", "status": "ordered", "currency_code": "USD", "charges": {}, "sub_total": "150", "total": "155.5", "line_items": [], "created_by": "Jane Smith", "created_at": "2026-08-03T08:00:00.000000Z", "updated_by": "Jane Smith", "updated_at": "2026-08-04T08:00:00.000000Z", "version": 3, "vendor": {}, "ship_to": null, "bill_to": null, "notes": "Deliver to the rear entrance. Call on arrival.", "terms": "Net 30", "expected_delivery_date": "2026-08-22T00:00:00.000000Z", "submitted_date": null, "submitted_by": null, "approved_date": null, "approved_by": null, "ordered_date": "2026-08-04T08:00:00.000000Z", "ordered_by": "Jane Smith", "received_date": null, "received_by": null } }

Jobs

Jobs are work orders. Create a job, assign items to it, move it through not_started, in_progress and completed, and return items when it's done.

Operations

Changelog

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.