Start here Last updated

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.

KeyCanCosts
Read onlyEvery GETFree on every plan
Read and writeAlso create, change, hand over, archiveFree 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 a quantity, and optional minQuantity (the low-stock line) and maxQuantity (fill up to).
  • unique: one physical thing. A drill, a laptop, a camera. It has a serial and can be with one person at a time: currentHolder and currentDueAt.

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 toCallMovement type
Add or remove stockPOST /items/{id}/adjust with a signed deltaadjust
Move items to another placePOST /items/movetransfer
Hand things to a personPOST /items/check-outcheck_out
Take them backPOST /items/check-incheck_in
Change a due datePOST /items/extenddue_change
Undo a stock changePOST /movements/reversethe 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 a 409 naming it.
  • Return one item with POST /items/check-in. Add problem to 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" }
}
StatuscodeMeans
400BAD_REQUESTThe input is wrong. Validation errors list each field in data.issues.
401UNAUTHORIZEDNo key, or the key is revoked, expired or mistyped.
403FORBIDDENThe key can’t do this, for example a write with a read key.
404NOT_FOUNDNot in this workspace, or outside the key’s folders.
409CONFLICTClashes with the current state: already out, reserved, code taken.
429Rate 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.