JSON API
Every SENAITE site ships with a REST API that speaks JSON. It needs no add-on and no configuration. Other systems use it to read samples and results, to create records and to drive the workflow. The API is provided by senaite.jsonapi, whose documentation covers every route in detail.
The examples below use curl against a local site at
http://localhost:8080/senaite with the default admin:admin login. The
responses are real, shortened to the parts that matter. Use HTTPS and an
account of its own for anything that is not a local test site.
Base URL
The API lives under the site, not under the server root:
http://localhost:8080/senaite/@@API/senaite/v1
A quick check that the API answers:
curl -u admin:admin \
http://localhost:8080/senaite/@@API/senaite/v1/version
{
"url": "http://localhost:8080/senaite/@@API/senaite/v1/version",
"version": "2.7.0",
"date": "unreleased"
}
This is the answer of the 2.x Docker image, which runs the development
version; a 2.6.0 installation reports 2.6.0.
Authentication
The API uses the same users and permissions as the web interface. A request sees exactly what the user would see in the browser, no more.
The simplest way is HTTP Basic authentication on every request, as in the examples on this page. A client can also log in once and keep the session cookie:
curl -c cookies.txt -X POST \
-d "__ac_name=admin&__ac_password=admin" \
http://localhost:8080/senaite/@@API/senaite/v1/login
{
"authenticated": true,
"username": "admin",
"roles": ["Manager", "Authenticated"]
}
Later requests pass -b cookies.txt instead of -u.
Resources Are Type Names
A resource is the content type name in lower case: client,
analysisservice, sampletype, worksheet. Samples are the one name
that surprises: their type is AnalysisRequest, so the resource is
analysisrequest.
/samples is not a list of samples. It returns the folder that holds
them, a single item of type Samples.
Listing
curl -u admin:admin \
"http://localhost:8080/senaite/@@API/senaite/v1/client?limit=2"
{
"count": 3,
"pagesize": 2,
"page": 1,
"pages": 2,
"next": "http://localhost:8080/senaite/@@API/senaite/v1/client?b_start=2&limit=2",
"previous": null,
"items": [
{
"uid": "1dae3a845cda4d8b9a2fee650ef3ca0c",
"id": "client-1",
"title": "Lakeside Brewery",
"portal_type": "Client",
"review_state": "active",
"getClientID": "LB",
"url": "http://localhost:8080/senaite/clients/client-1",
"api_url": "http://localhost:8080/senaite/@@API/senaite/v1/client/1dae3a845cda4d8b9a2fee650ef3ca0c"
},
{
"uid": "b6f1eb48090f43caad17519097cd1b52",
"id": "client-2",
"title": "Hillside Dairy",
"portal_type": "Client",
"review_state": "active",
"getClientID": "HD"
}
]
}
count is the total number of matches, pagesize the number of items on
this page. A listing returns the catalog's summary of each object, which
is fast but does not contain every field. Add complete=yes to get the
full records, and fields= to limit them to the fields you need:
curl -u admin:admin \
"http://localhost:8080/senaite/@@API/senaite/v1/client?complete=yes&fields=title,ClientID,Phone"
Paging
limit sets the page size (25 by default), b_start the index of the
first item. The response carries ready made links in next and
previous; follow next until it is null:
curl -u admin:admin \
"http://localhost:8080/senaite/@@API/senaite/v1/client?b_start=2&limit=2"
{
"count": 3,
"pagesize": 2,
"page": 2,
"pages": 2,
"next": null,
"previous": "http://localhost:8080/senaite/@@API/senaite/v1/client?limit=2&b_start=0",
"items": [
{"title": "Riverbank Water Works"}
]
}
Sort with sort_on and sort_order, for example
?sort_on=sortable_title&sort_order=ascending, so that pages do not
shift between requests.
One Object
Every item carries its uid. Fetch the full record by type and UID, or by
UID alone:
curl -u admin:admin \
http://localhost:8080/senaite/@@API/senaite/v1/client/b6f1eb48090f43caad17519097cd1b52
curl -u admin:admin \
http://localhost:8080/senaite/@@API/senaite/v1/b6f1eb48090f43caad17519097cd1b52
Both return the same envelope with one item that contains every field of
the object, for example ClientID, Phone and EmailAddress.
Filtering
Query parameters that match a catalog index filter the listing:
curl -u admin:admin \
"http://localhost:8080/senaite/@@API/senaite/v1/client?getClientID=HD"
{
"count": 1,
"items": [
{"title": "Hillside Dairy", "getClientID": "HD"}
]
}
Which indexes exist depends on the catalog. /catalogs lists the
catalogs, and /catalogs/<id> lists the indexes of one of them:
curl -u admin:admin \
http://localhost:8080/senaite/@@API/senaite/v1/catalogs/senaite_catalog_client
{
"id": "senaite_catalog_client",
"indexes": ["Creator", "Title", "UID", "allowedRolesAndUsers",
"client_searchable_text", "created", "getClientID", "getId",
"getName", "id", "is_active", "object_provides", "path",
"portal_type", "review_state", "sortable_title", "title"]
}
Writing
Run write requests against a test site first. They change data with the permissions of the user, and there is no undo.
Create an object with POST /create, naming the type and the folder it
goes into:
curl -u admin:admin -X POST \
-H "Content-Type: application/json" \
-d '{"portal_type": "Client", "parent_path": "/senaite/clients",
"title": "Lakeside Brewery", "ClientID": "LB"}' \
http://localhost:8080/senaite/@@API/senaite/v1/create
The response contains the new object, including its uid.
Change fields with POST /update/<uid>:
curl -u admin:admin -X POST \
-H "Content-Type: application/json" \
-d '{"Phone": "+1 555 0100"}' \
http://localhost:8080/senaite/@@API/senaite/v1/update/b6f1eb48090f43caad17519097cd1b52
{
"count": 1,
"items": [
{
"title": "Hillside Dairy",
"Phone": "+1 555 0100",
"modified": "2026-10-14T10:37:22+00:00"
}
]
}
Creating samples, entering results and running workflow transitions work the same way; the senaite.jsonapi documentation describes the routes and the fields they expect.
Three Silent Failures
These cost integrators the most time, because the API answers with
200 OK and no error:
- Wrong credentials. With a wrong password the request runs as an
anonymous user and returns an empty list:
"count": 0, status 200. If a listing is unexpectedly empty, check the login first. - Misspelled parameters. Parameters that match no index are ignored.
?getClientId=HD(lower cased) returns all three clients instead of one. - Missing permissions. A user who may not see an object does not get it in a listing either. Use a dedicated API user with the roles the integration needs.