REST API conventions
Base URL and format
https://<your-site>/api/index.php/v1/nextinventory
Requests and responses are JSON. Send Content-Type: application/json with a body. A request
with no Accept header is answered in JSON.
Authentication
Authorization: Bearer <token>, or X-NextInventory-Token: <token>. Every request needs one;
there are no anonymous endpoints.
Success
| Request | Status | Body |
|---|---|---|
| List | 200 |
{ "data": [ … ], "meta": { "total": 412 } } |
| Read one | 200 |
{ "data": { … } } |
| Create | 201 |
{ "data": { "id": 123 } } |
| Update, finalize | 200 |
{ "data": { "id": 123 } } |
| Publish / unpublish | 200 |
{ "data": { "id": 123, "published": true } } |
| Delete | 204 |
(empty) |
Every response carries Cache-Control: no-store.
Errors
{ "errors": [ { "title": "Recipient is required for check-out", "code": "BAD_REQUEST" } ] }
| Status | code |
Meaning |
|---|---|---|
| 400 | BAD_REQUEST |
Validation or a stock rule refused the request. title says why. |
| 400 | INVALID_JSON |
The body is not a JSON object. |
| 400 | INVALID_IDEMPOTENCY_KEY |
The Idempotency-Key header is not 1 to 191 printable characters. |
| 401 | MISSING_TOKEN |
No token was sent. |
| 401 | INVALID_TOKEN |
Unknown, disabled, revoked or expired token. |
| 403 | API_DISABLED |
The API Tokens app is off in App Manager. |
| 403 | ACCOUNT_UNUSABLE |
The token owner's Joomla account is blocked, unactivated or must reset its password. |
| 403 | ACCESS_DENIED |
The owner cannot manage Next Inventory, or their role does not allow this. |
| 403 | TASK_NOT_ALLOWED |
This endpoint is not available to tokens (task lane only). |
| 403 | MISSING_SCOPE |
The token lacks the scope; title names it. |
| 403 | FORBIDDEN |
The owner's role does not allow this particular change. |
| 403 | APP_DISABLED |
The app behind this endpoint is off in App Manager; meta.app names it. |
| 403 | DEMO_LOCKED |
The site is a demo; writes are refused. |
| 404 | NOT_FOUND |
No such row, or no such endpoint. |
| 409 | IDEMPOTENCY_IN_PROGRESS |
A request with this Idempotency-Key is still running. Retry shortly. |
| 412 | PRECONDITION_FAILED |
The record changed since the If-Match ETag was read. Read it again. |
| 422 | IDEMPOTENCY_KEY_REUSED |
This Idempotency-Key was already used for a different request. |
| 429 | RATE_LIMITED |
Too many reads or changes for this token this minute. Wait the Retry-After seconds. |
| 429 | TOO_MANY_FAILED_LOGINS |
More than 20 unknown tokens from your address in 5 minutes. Check the token, then wait the Retry-After seconds. |
| 500 | SERVER_ERROR |
Logged on the site; contact the site administrator. |
Rate limits
Each token may make a set number of requests per minute, with reads (GET) and changes
(everything else) counted apart: 300 reads and 120 changes by default. An administrator changes
them on the Settings tab of Configuration → REST API (0 means no limit). Every request
counts, refused ones too. Tokens are counted separately, so one busy integration never slows
another down.

Every answer tells you where you stand:
| Header | Meaning |
|---|---|
RateLimit-Limit |
The limit that applied to this request (reads or changes) |
RateLimit-Remaining |
How many more of those you may send this minute |
RateLimit-Reset |
Seconds until the minute starts again |
Slow down when RateLimit-Remaining gets low. Over the limit, the answer is 429 RATE_LIMITED
with a Retry-After header giving the seconds to wait.
Unknown tokens. After more than 20 requests with an unknown token from one address within 5
minutes, further unknown tokens from that address are answered 429 TOO_MANY_FAILED_LOGINS until
the 5 minutes are over. A valid token is never refused by this, so an integration sharing an
office address with a misconfigured one keeps working.
Idempotency-Key: safe retries
Send an Idempotency-Key header with any POST, PATCH or DELETE, a unique value per
request (a UUID is ideal). If the connection drops and you send the same request again with the
same key, Next Inventory does not run it a second time: it answers with the first response,
plus the header Idempotent-Replayed: true.
curl -X POST "$BASE/stock-movements" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 6f1c2a9e-3b4d-4c55-9a1e-2f0b7d8c9e10" \
-H "Content-Type: application/json" \
-d '{"movement_type":"add_stock","is_draft":false,"warehouse_id":1,"items":[{"item_id":42,"quantity":10}]}'
- Keys are kept for 24 hours, separately for each token.
- The same key with a different request (another body, endpoint or id) is refused with
422. - While the first request is still running, a retry gets
409; try again a moment later. - A
5xxanswer is not kept, so the request can be retried with the same key. GETrequests ignore the header: reading twice is always safe.
ETags: concurrent edits
Reading one record (GET /items/{id}, /categories/{id}, /stock-movements/{id} and so on)
returns an ETag header, a fingerprint of the stored record. A successful PATCH, status change
or finalize returns the new one.
Send the ETag back as If-Match on PATCH,
DELETE, /status or /finalize. If the record changed in the meantime (in the Next Inventory
screens or from another system), the answer is 412 PRECONDITION_FAILED and nothing is
changed: read the record again, re-apply your change, and send it with the new ETag.
curl -i "$BASE/items/42" -H "Authorization: Bearer $TOKEN" # ... ETag: "9f2c…"
curl -X PATCH "$BASE/items/42" -H "Authorization: Bearer $TOKEN" \
-H 'If-Match: "9f2c…"' -H "Content-Type: application/json" -d '{"unit_price": 12.5}'
If-Match is optional; without it the last write wins. Lists carry no ETag. To find out when
records change, subscribe a webhook instead of polling.
Paging, filtering, sorting
Lists take limit (default 20) and offset. meta.total is the count across all pages.
GET /items?limit=100&offset=200&category_id=3&low_stock=1&search=drill
GET /transactions?item_id=17&date_from=2026-11-01&date_to=2026-11-30
Lists that support it also take ordering (a column name) and direction (asc or desc).
Partial updates (PATCH)
PATCH changes only the fields you send. Everything else keeps its stored value:
curl -X PATCH …/items/17 -H "Authorization: Bearer …" -H "Content-Type: application/json" \
-d '{ "unit_price": 7.25 }'
Stock movements, transfers and adjustments have no PATCH. Saving one replaces all of its lines, so a partial body would delete lines. Create them whole, or create a draft, then finalize it.
Types and dates
Numbers are JSON numbers and flags are booleans. created and modified are ISO 8601 in UTC
(2026-11-11T09:30:00Z). Business dates (movement_date, transfer_date, adjustment_date)
are plain YYYY-MM-DD. A field the row does not have is null, never missing.
Custom fields
GET /items/{id} returns custom_fields, a list of { name, label, field_type, value }. To
write one, send it as cf_<name> on POST or PATCH:
{ "cf_supplier_email": "[email protected]" }