Reading Data records
Data holds the facts your products share: a dog and its sire, a breed registry, a price list, a store. A model says what a kind of record looks like, and a record is one live entry of a model. There is no draft and no publishing step: a saved record is what the API returns.
Records live in a partition. A delivery key is bound to exactly one, and reads nothing outside it.
Prefer a client to hand-built URLs: @ebitex/data-sdk for JavaScript, in a browser or on a server, and
Ebitex.Data.Delivery for .NET. Both build the queries below, page with a cursor for you, and report errors with the codes in this page.
Get a key
Section titled “Get a key”Open Data, choose Settings and Delivery keys, and create a key for the partition. The secret is shown once. Send it as a bearer token:
GET /data/delivery/v1/modelsAuthorization: Bearer <your key>There are two kinds of key, and what you restrict decides which you have.
| Kind | How you make it | What it reads |
|---|---|---|
| Server key | No origin and no IP restriction | Every model and field in the partition |
| Browser-safe key | One or more allowed origins, or allowed IP ranges | Only models marked public, and only the fields marked public inside them |
A browser-safe key can sit in the code you send to a browser, because it only works from the origins you listed. A server key must never reach a browser.
Something a key may not read answers exactly as something that does not exist: 404 for a record, and
absent from every list. A browser-safe key cannot filter, sort or count by a private field, and the error
it receives is the same one a field that does not exist receives.
Discover what you can ask
Section titled “Discover what you can ask”GET /data/delivery/v1/models names the partition the key is bound to (partition.key), lists the models the key may read and
the fields it may see. A field a model’s owner made queryable says which of filter, sort and facet it supports:
{ "partition": { "key": "default" }, "models": [ { "key": "dog", "name": "Dog", "abstract": false, "parent": null, "fields": [ { "id": "name", "name": "Name", "type": "text", "mandatory": true, "list": false, "localized": false, "queryable": { "filter": true, "sort": true, "facet": false } }, { "id": "sire", "name": "Sire", "type": "component", "mandatory": false, "list": false, "localized": false, "queryable": { "filter": true, "sort": false, "facet": false } } ] } ]}A model can inherit from another. Asking for a model’s records also returns the records of every model that inherits from it.
Read a record
Section titled “Read a record”GET /data/delivery/v1/records/{key} returns one record:
{ "key": "rex-of-drakeshead", "model": "dog", "name": "Rex of Drakeshead", "version": 4, "updatedAtUtc": "2026-10-09T08:15:00Z", "values": { "name": "Rex of Drakeshead", "colour": "black", "sire": { "mode": "reference", "provider": "data", "key": "ace-of-drakeshead" } }}A record’s key never changes, so it is safe to store. values holds the fields the key may see, by field
id. A field that points at another record is a reference; one that embeds a document is an inline
value: { "mode": "inline", "model": "…", "document": { … } }.
Follow references
Section titled “Follow references”Add generations to put each referenced record beside its pointer, as record, in the same shape.
generations=3 on a dog returns its parents, grandparents and great-grandparents in one request.
GET /data/delivery/v1/records/rex-of-drakeshead?generations=3generationsis 0 to 5. The default, 0, returns the record alone.expand=sire,damfollows only those reference fields, wherever they appear. Without it every reference is followed.- A request expands at most 200 records. When that cuts the result short,
meta.truncatedistrueand you have everything up to that point. - A record that is its own ancestor is expanded once; the repeat stays a pointer. A record that appears in two branches, such as a common ancestor, is expanded in each.
- A reference the key may not read, or whose target no longer exists, stays a pointer without
record.
List, filter and page
Section titled “List, filter and page”GET /data/delivery/v1/records lists records, 25 at a time.
| Parameter | Meaning |
|---|---|
model |
A model key. Needed for filters, field sorts and facets |
search |
Records whose name, or any word in it, starts with this text, without regard to case (fly finds Drakeshead Flycatcher; ly does not) |
filter[field]=value |
Narrow by a queryable field. Several filters are combined with AND |
sort |
name (default), updated, or a field the model made sortable. Prefix - for descending |
facets |
Comma-separated queryable fields, up to 5, to count |
limit |
1 to 100 |
cursor |
The nextCursor of the previous page |
keys |
Up to 50 record keys, read in one request, in the order given |
generations, expand |
As for a single record |
Filters
Section titled “Filters”filter[field] means equal. Another operator goes in a second pair of brackets:
filter[weight][gt]=20.
| Operator | Field types | Meaning |
|---|---|---|
eq |
all | Equal to the value |
in |
text, number, reference | Equal to any of a comma-separated list |
prefix |
text | Starts with the value, without regard to case |
gt, gte, lt, lte |
number, date | Greater or less than the value |
exists |
all | true for records that have a value, false for those that do not |
Dates are RFC 3339 instants such as 2026-06-01T00:00:00Z, unless the field is a calendar date, which
takes 2026-06-01, or a time of day, which takes 16:00 or 16:00:30 and compares as a clock time (02:00
is earlier than 16:00). Neither has a time zone, so pass them as plain strings. A reference field filters by the key of the
record it points at, so filter[sire]=rex-of-drakeshead lists a sire’s offspring.
GET /data/delivery/v1/records?model=dog&filter[colour]=black&filter[weight][gte]=20&sort=-born&limit=10Paging
Section titled “Paging”When more records follow, nextCursor is set. Send it back, with the same query, as cursor:
{ "items": [ … ], "nextCursor": "eyJm…" }nextCursor is null on the last page. A cursor continues from the last record you saw, so it stays
correct while records are added or changed, and no record repeats or is skipped on account of another.
It belongs to one query: using it with a different sort, filter or search is a 400 invalid_cursor.
Records with no value for the field you sort by come last, in either direction.
Counts
Section titled “Counts”facets=colour adds, for each field, its 20 most common values and how many records have each. The counts
cover every record the filters match, not just the page.
The built-in facet model counts the matches per concrete model, under the same filters. When the kind of record is
the model rather than a field, as with a dog and a bitch beside each other under a shared parent, model=animal&facets=model
answers “how many of each” in the one request. It needs no declaration, and it counts only models the key may read.
{ "facets": { "colour": [ { "value": "black", "count": 12 }, { "value": "tan", "count": 7 } ] } }Use Data records from Content and Forms
Section titled “Use Data records from Content and Forms”You do not need code to show a Data record on a Content page or to offer records as the options of a Forms field. Both read the same delivery API with a server key for the partition, held by the organization:
- Content. Activate the ebitex Data plugin, then register it as a provider with a delivery key and the key of the partition that key reads. Registration checks the two agree and refuses a partition the key does not read. Each Data model then appears as a read-only Contract, and a Component field can reference a record. The record is read when a page is delivered, so a change in Data reaches the page within about five minutes.
- Forms. A Lookup field can use the ebitex Data choice source with a delivery key and a model key. A visitor’s text finds records whose name starts with it; the option’s value is the record key.
While a Content page references a record, Data refuses to delete it (409 record_in_use) and lists the pages.
Caching and limits
Section titled “Caching and limits”Every successful response carries an ETag. Send it back as If-None-Match and an unchanged response is
304 Not Modified. Responses are marked private, no-cache: they are meant to be revalidated, not stored by
a shared cache.
Each key has a requests-per-minute budget, set when the key is created (600 by default). Beyond it the API
answers 429 rate_limited with a Retry-After header.
Errors
Section titled “Errors”| Status | error |
Meaning |
|---|---|---|
| 400 | field_not_queryable |
The field cannot be filtered, sorted or counted that way, or does not exist, or is private to your key |
| 400 | invalid_filter, invalid_limit, invalid_generations, invalid_keys, invalid_facets, invalid_search, invalid_cursor, model_required |
The query is malformed; the message says how |
| 401 | missing_api_key, invalid_api_key |
No key, or an unknown, revoked or expired one |
| 402 | payment_required |
The organization’s subscription needs attention |
| 403 | scope_denied, app_not_available, origin_denied, ip_denied |
The key is not allowed this, here |
| 404 | record_not_found, model_not_found |
There is no such record or model that the key may read |
| 429 | rate_limited |
The key spent its budget for this minute |
See the API reference for every operation.