Next Restaurant 6.1.0: move your data in, run payroll, and connect other systems Next Inventory 6.2.0: a REST API and webhooks for your other systems Next DB 6.1: Auto-Updating Tables, Printing, Phone Layout and SEO Next Task 6.0.0: a task manager for Joomla Next Inventory 6.1.1: smoother Check Out, no more list limits, and much faster PHP2Shell: Is Your Joomla Site Safe? What to Do Today Introducing Next Boards: Kanban project and task boards for Joomla Building the Future of Joomla Extensions
NextSoftware

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.

REST API page on the Settings tab: reads per minute set to 300 and changes per minute set to 120, a note on the protection against unknown tokens, and a link to App Manager to switch the API off

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 5xx answer is not kept, so the request can be retried with the same key.
  • GET requests 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]" }

All Extensions