Skip to content

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.

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/models
Authorization: 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.

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.

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": { … } }.

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=3
  • generations is 0 to 5. The default, 0, returns the record alone.
  • expand=sire,dam follows 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.truncated is true and 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.

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

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=10

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.

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 } ] } }

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.

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.

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.