How the API thinks
Items, codes, folders and people, the logbook that every change goes through, custody, and the rules for pages, errors, limits and retries.
Keys and scopes
Send a key on every request as Authorization: Bearer itk_live_…. The x-api-key header works too.
| Key | Can | Costs |
|---|---|---|
| Read only | Every GET | Free on every plan |
| Read and write | Also create, change, hand over, archive | Free on every plan |
A key opens one workspace and acts as the person who created it: writes carry that person’s name in the logbook, and a key never sees more folders than its creator. Only an editor can create a key, and a key never takes a seat of its own. If its creator leaves the workspace, their keys stop working; if they stop being an editor, their write keys do. No key can manage billing, members, other keys or webhooks. Those stay in the app.
Items: quantity or unique
Every item has a mode.
quantity: counted stock. Cable ties, batteries, gloves. It has aquantity, and optionalminQuantity(the low-stock line) andmaxQuantity(fill up to).unique: one physical thing. A drill, a laptop, a camera. It has aserialand can be with one person at a time:currentHolderandcurrentDueAt.
availability.state sums it up: in_stock, low or empty for counted stock; free, booked (reserved right now), out or overdue for unique items.
Codes
Every item gets an ItemTally QR code when it’s created, like IT-000007. You can attach barcodes that are already on the thing (an EAN from the packaging, a Code 128 asset tag) with POST /items/{itemId}/codes.
To find an item from a scan, use GET /items/lookup?value=…. It accepts a bare code or a full label URL and answers with itemId (or folderId for a shelf label), or nulls if nothing matches.
Folders are places
Folders are physical places in a tree: a room, a shelf, a bin. Set kind to vehicle or site for vans and job sites, which get their own stock levels. An item lives in one folder (folderId); null means Unfiled. GET /folders returns the whole tree.
People
People are anyone who can be handed something. They don’t need an ItemTally account. Give them an email and they get receipts and reminders.
The logbook: why you never set a quantity
Quantity and custody never change by editing a field. Every change is a movement, a line in an append-only logbook, and the item’s numbers are derived from those lines. That’s why PATCH /items/{id} has no quantity field.
| You want to | Call | Movement type |
|---|---|---|
| Add or remove stock | POST /items/{id}/adjust with a signed delta | adjust |
| Move items to another place | POST /items/move | transfer |
| Hand things to a person | POST /items/check-out | check_out |
| Take them back | POST /items/check-in | check_in |
| Change a due date | POST /items/extend | due_change |
| Undo a stock change | POST /movements/reverse | the opposite line |
You’ll also see stock_in, stock_out and count_set lines that the app posts (deliveries, opening stock, stock counts). Nothing is deleted. A correction is a new line, so every number can be traced.
Custody: hand over, return, extend
In the app these are Hand over, Return and Extend. In the API and the logbook they’re check-out, check-in and extend.
- Hand over several items to one person in one call, with an optional
dueAt. It’s all or nothing: if one item is already out or reserved, nothing is handed over and you get a409naming it. - Return one item with
POST /items/check-in. Addproblemto flag damage. - Extend moves the due date while the person keeps it.
- Reserve an item for future dates with
POST /reservations. A hand-over that clashes with someone else’s reservation is refused. - Kits hand over and return a whole set in one call.
GET /movements/out-now lists everything that’s out right now, soonest due first.
Pages
GET /items, GET /movements and GET /items/{itemId}/movements are paged:
curl "https://api.itemtally.com/v1/items?page=2&pageSize=200" \
-H "Authorization: Bearer $ITEMTALLY_KEY"
They answer { items, total, page, pageSize }. pageSize goes up to 200. Keep asking for the next page until you’ve seen total rows. The other lists (folders, people, kits, tags, reservations) are short and come back whole, as a plain array.
Errors
Errors use the HTTP status and a JSON body with a sentence you can show a person:
{
"code": "CONFLICT",
"status": 409,
"message": "Bosch GLM 50 laser measure is already with Petra S. Return it first.",
"data": { "itemId": "01a143af-c3af-75bc-9e6e-6ffb54a28485" }
}
| Status | code | Means |
|---|---|---|
| 400 | BAD_REQUEST | The input is wrong. Validation errors list each field in data.issues. |
| 401 | UNAUTHORIZED | No key, or the key is revoked, expired or mistyped. |
| 403 | FORBIDDEN | The key can’t do this, for example a write with a read key. |
| 404 | NOT_FOUND | Not in this workspace, or outside the key’s folders. |
| 409 | CONFLICT | Clashes with the current state: already out, reserved, code taken. |
| 429 | Rate limit reached. See below. |
Branch on status or code. Don’t parse message; we improve the wording.
Rate limits
Each key gets 600 reads (GET) and 120 writes a minute. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (Unix seconds). Past the limit you get 429 with a Retry-After header in seconds. Wait that long and carry on.
Safe retries
Networks drop. To make a write safe to send twice:
- On a movement (adjust, hand over, return, extend), send
movementId: a UUID you generate. A repeat with the same id is applied once. - On a create (items, reservations, codes), send your own UUID as
id. A repeat answers with what the first call created.
Generate the UUID once per intended change and store it with your job, so a retry after a crash reuses it.
Dates
Send and receive ISO 8601 date-times in UTC, like 2026-10-23T15:00:00Z.
Stuck on an integration? Email [email protected] with the request you sent and the response you got. The person who built the API answers, usually within a business day.