RIDING BYTES
← All insights

August 15, 2026 · 7 min read

Reading SENAITE data with the JSON API: a first session

Where the endpoints are, why samples are called analysisrequest, how to filter on catalog indexes, and the three silent failures that cost integrators the most time.

Reading SENAITE data with the JSON API: a first session

Every SENAITE installation ships with a JSON API. It needs no add-on and no configuration, and it answers with the permissions the user has in the browser. This post walks through a first session against it: the requests you make in the first ten minutes, what comes back, and the mistakes that make an integration return nothing without saying why.

The examples use curl against a local instance. Replace the host and the credentials with your own, and use HTTPS for anything that is not your own machine.

The base URL

The API lives under the site, not under the server root:

http://localhost:8080/senaite/@@API/senaite/v1

senaite is the ID of the site. Most installations keep it; if yours was created under another name, that name goes there. The quickest check that the URL is right is the version endpoint:

curl -u admin:admin \
  "http://localhost:8080/senaite/@@API/senaite/v1/version"
{"url": "http://localhost:8080/senaite/@@API/senaite/v1/version",
 "date": "unreleased", "version": "2.7.0", "_runtime": 8.5e-05}

Resources are type names

The first surprise for most integrators: resources are the internal type names, lowercased. A sample is an AnalysisRequest internally, so its resource is analysisrequest.

What you wantResource
Samplesanalysisrequest
Analysesanalysis
Clientsclient
Client contactscontact
Sample typessampletype
Worksheetsworksheet
Batchesbatch

/samples does answer, which makes the mistake easy to miss. It returns a single item: the folder behind the samples listing, not the samples.

A first listing

curl -u admin:admin \
  "http://localhost:8080/senaite/@@API/senaite/v1/analysisrequest?limit=2&fields=id,review_state"
{
  "count": 52, "pagesize": 2, "page": 1, "pages": 26,
  "next": "http://localhost:8080/senaite/@@API/senaite/v1/analysisrequest?fields=id%2Creview_state&limit=2&b_start=2",
  "previous": null,
  "items": [
    {"id": "DW-0001", "review_state": "verified"},
    {"id": "WW-0001", "review_state": "verified"}
  ]
}

count is the total number of matches, not the number of items in this response. next is a ready-made URL for the following page. The details are in the pagination post.

fields limits each item to the keys you name. Without it, a listing item carries around sixty keys: the metadata the sample catalog stores, such as getClientTitle, getSampleTypeTitle, getDateReceived, getDueDate and review_state. These come straight from the catalog, so a listing stays fast on large databases.

Response of GET /senaite/@@API/senaite/v1/analysisrequest?limit=1, formatted: count 52, pagesize 1, and the first item with keys such as getDateVerified, uid, getDueDate, url, id DW-0001, getAnalysesKeywords pH, Cond, Turb, getClientTitle and getSamplePointTitle.
The first lines of a listing item without fields: catalog metadata such as getClientTitle, getAnalysesKeywords and getDueDate, around sixty keys in total.

The full record

A listing item is a summary. For everything a sample knows, request it by its UID, the 32 character identifier every object keeps for its whole life:

curl -u admin:admin \
  "http://localhost:8080/senaite/@@API/senaite/v1/<uid>"

This returns the record itself, without the page envelope, with around 140 keys for a sample. References to other objects come back as small objects with uid, url and api_url, so a client can follow them:

"SampleType": {
  "uid": "4fc5054ebe8f416d88764809db4bcb30",
  "url": "http://localhost:8080/senaite/setup/sampletypes/sampletype-1",
  "api_url": "http://localhost:8080/senaite/@@API/senaite/v1/sampletype/4fc5054ebe8f416d88764809db4bcb30"
}

Adding complete=yes to a listing returns full records for every item. Combine it with a small limit: each item then loads the object from the database instead of reading the catalog.

Filtering on indexes

Every index of the catalog behind a resource works as a query parameter. Samples that are received but have no results submitted yet:

".../analysisrequest?review_state=sample_received"

Repeat a parameter to match any of several values:

".../analysisrequest?review_state=sample_received&review_state=to_be_verified"

Other useful sample indexes are getClientUID, getClientSampleID, getSampleTypeUID, getBatchUID and the date indexes getDateReceived, getDateVerified and getDueDate. The authoritative list comes from the API itself:

".../catalogs/senaite_catalog_sample"

returns the catalog’s indexes and its metadata schema. For free text, q searches the same text as the search box of the samples listing: sample ID, client sample ID, client order number, client reference, client name and ID, sample type, sample point and batch ID.

The results of one sample

Analyses have their own resource and their own catalog. To get the results of one sample, filter on the sample ID:

".../analysis?getRequestID=DW-0001&fields=getKeyword,getResult,getUnit,review_state"
{"count": 3, "items": [
  {"getKeyword": "Turb", "getResult": "0.8", "review_state": "verified"},
  {"getKeyword": "pH", "getResult": "7.4", "review_state": "verified"},
  {"getKeyword": "Cond", "getResult": "342", "review_state": "verified"}
]}

Results are strings, and an analysis without a result has an empty one. If a service uses result options, the stored value is the option’s value, not its text. A result below a detection limit keeps its < or > in a separate field, DetectionLimitOperand, in the full record. Read both before you treat a result as a number.

Add workflow=1 to a single record request and the response includes workflow_info: the current state and the transitions the authenticated user may perform on the object right now.

Three silent failures

These three cause most of the “the API returns nothing” requests we get.

Wrong credentials do not fail. A request with a wrong password, or none at all, gets HTTP 200 and an empty result, because an anonymous visitor may see nothing. Your client reports success and processes zero items. If count is zero where you expect data, test the credentials before anything else.

Unknown parameters are ignored. A typo such as reviewstate instead of review_state raises no error. The parameter is dropped and you get every sample. The same happens when you filter on a field name that is not an index, such as DateReceived instead of getDateReceived. While developing, compare count with what you expect.

The API sees what the user sees. Searches are filtered by the permissions of the authenticated user. A user linked to a client contact sees that client’s samples and nothing else, which is right for a customer portal and confusing for a nightly export. Give each integration its own lab user with the roles it needs, instead of a personal account.

Where the API comes from

The design is older than SENAITE. Before SENAITE existed, Ramon Bartl wrote plone.jsonapi.routes, a JSON API for the content management system SENAITE is built on. When SENAITE started in 2017, senaite.jsonapi was built on the same design; its documentation says that it shares most of its software design with plone.jsonapi.routes. That is why resources are content type names, why filters are catalog indexes, and why the API sees exactly what the user sees.

At the Plone Conference 2017 in Barcelona, Ramon showed the concept in the browser: reading content, creating, updating and deleting it, a JavaScript application that works over the API, and how to extend it. The talk came shortly after the first commit of senaite.jsonapi, and most of what it shows still applies.

Loading this video will connect you to YouTube (Google LLC, USA) and transmit your IP address. By clicking play you consent to this transfer. See our privacy policy.

Where to go next

Reading is the safe half of the API. Creating samples, setting results and running transitions use the same base URL with POST requests, and they belong on a test system before they touch production data. On the read side, the pagination post covers what every integration needs next, and the catalog post explains why some filters are fast and others do not exist.

Two people reviewing notes on paper next to a laptop

Bring this to your lab

Whether you are evaluating SENAITE, scaling it across sites, or wiring up instruments and monitoring, the team that wrote the platform is the team you talk to.