# Objects API

Use the Objects API for JSON entries with or without a contract. It reads the entry's JSON directly, including native entries that have no content type. JSON-backed entries converted from the older content model work through the same endpoints. Existing Entries integrations keep their current URLs and payloads.

Files use the [Files API](/docs/api/files). Stored form submissions stay separate from objects and use the form's submissions workflow. The Objects endpoints do not list or read file references or legacy linked submissions.

## Availability and permissions

All routes are under `/api/v1/{workspace}/objects` and require a workspace API key. The global Objects setting and workspace activation must permit Objects; disabled workspaces return 404. Production requires `OBJECTS_ENABLED=true` and an activated workspace. These endpoints are not limited to local development and do not activate or migrate a workspace themselves.

| Operation | Method and path | API ability | Creator permission |
| --- | --- | --- | --- |
| List published entries | `GET /objects` | `entries:read` | `list-entries` |
| Read a published entry | `GET /objects/{objectRef}` | `entries:read` | `view-entry` |
| Query drafts and published entries | `POST /objects/query` | `entries:query` | `list-entries` |
| Create an entry | `POST /objects` | `entries:write` | `create-entry` |
| Update an entry | `PATCH /objects/{objectRef}` | `entries:write` | `update-entry` |
| Delete an entry | `DELETE /objects/{objectRef}` | `entries:delete` | `delete-entry` |

`{objectRef}` is the numeric object **id** returned by this API. It is different from the entry's ULID (`entry_id`) and its bucket-relative address (`ref`). Use `entry_id` in relationship values. A reference rename or bucket move preserves both IDs.

Published reads always exclude drafts, even for wildcard keys. Use the separately scoped query endpoint to inspect drafts. Keys are still restricted to their workspace and their creator's permissions. These requests share the existing entries quota: by default 500 units per minute per API key, with one unit per request. See [API access](/docs/platform/api-access).

## Create an entry

```http
POST /api/v1/acme/objects
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

```json
{
  "ref": "products/inflatable-pillow",
  "status": "published",
  "data": {
    "name": "Inflatable pillow",
    "price": 24.95,
    "available": true,
    "dimensions": { "width": 40 },
    "tags": ["camping"]
  }
}
```

The unwrapped 201 response includes:

```json
{
  "id": 42,
  "entry_id": "01HY8M7M7D4RT5N6P2Q3R4S5T6",
  "bucket_id": 3,
  "bucket": "general",
  "ref": "products/inflatable-pillow",
  "kind": "entry",
  "contract": null,
  "data": {
    "name": "Inflatable pillow",
    "price": 24.95,
    "available": true,
    "dimensions": { "width": 40 },
    "tags": ["camping"]
  },
  "locale": "en-US",
  "locales": ["en-US"],
  "status": "published",
  "lock_version": 0,
  "created_at": "2026-10-15T12:00:00.000000Z",
  "updated_at": "2026-10-15T12:00:00.000000Z",
  "validation": {
    "mode": null,
    "issues": [],
    "checked_at": "2026-10-15T12:00:00.000000Z",
    "pending": false,
    "failed": false
  }
}
```

`data` must be a JSON object. Numbers, booleans, nulls, arrays and empty objects retain their JSON types. No contract or content type is required. Omitted `bucket_id` uses General; omitted `status` uses draft. References are unique across entries and files within their bucket.

Instead of `ref`, supply `path` to generate a reference from a folder path and the new entry's server-generated ULID. Use an empty or null path for the bucket root. Do not supply both `ref` and `path`, or your own `id` or `entry_id`. The maximum write request size is 1 MiB.

Optional `contract` selects validation: omitted/null means Automatic on creation, `false` means No contract, and a string selects that exact workspace contract name. See [Contracts](/docs/content-modeling/contracts).

## List published entries

```http
GET /api/v1/acme/objects?bucket=general&prefix=products&per_page=50
Authorization: Bearer YOUR_API_KEY
```

| Parameter | Behavior |
| --- | --- |
| `bucket` | Optional bucket API name; omitted means all buckets in this workspace. |
| `bucket_id` | Optional numeric bucket ID, instead of `bucket`. |
| `ref` | Exact bucket-relative reference. Combine with a bucket to identify one address. |
| `prefix` | A folder path, matching that exact reference and descendants; `products` excludes `products-old`. Leading/trailing slashes are ignored. `%` and `_` are literal characters. |
| `view` | `summary` (default) omits `data` and does not load entry documents; `full` includes content. |
| `locale` | Configured locale, default `en-US`; `*` includes the base document and all translations in full views. |
| `per_page` | Page size, default 50, range 1–100. |
| `cursor` | Opaque cursor returned by this endpoint. |

The response uses cursor pagination:

```json
{
  "data": [
    {
      "id": 42,
      "entry_id": "01HY8M7M7D4RT5N6P2Q3R4S5T6",
      "bucket_id": 3,
      "bucket": "general",
      "ref": "products/inflatable-pillow",
      "kind": "entry",
      "contract": null,
      "locale": "en-US",
      "locales": ["en-US"],
      "status": "published",
      "lock_version": 0,
      "created_at": "2026-10-15T12:00:00.000000Z",
      "updated_at": "2026-10-15T12:00:00.000000Z",
      "validation": { "mode": null, "issues": [], "checked_at": "2026-10-15T12:00:00.000000Z", "pending": false, "failed": false }
    }
  ],
  "path": "https://app.example.com/api/v1/acme/objects",
  "per_page": 50,
  "next_cursor": null,
  "next_page_url": null,
  "prev_cursor": null,
  "prev_page_url": null
}
```

Follow `next_page_url` while it is non-null. Ordering is object ID ascending. Pages contain at most the requested number of entries, with one extra row examined to detect another page; no total count is calculated. Pagination is not a snapshot of concurrent changes.

## Read a published entry

```http
GET /api/v1/acme/objects/42
Authorization: Bearer YOUR_API_KEY
```

Returns the same full, unwrapped object shape as creation. A missing, foreign, draft, non-JSON or file object returns 404.

Use `?locale=nl-NL` to read that language in `data`. A missing translation returns `data: null`, without fallback. `?locale=*` returns the base language in `data` and a `translations` object keyed by locale. JSON relationships and file IDs are returned as stored, without expansion or generated download URLs.

Reads resolve the current effective contract without rewriting content or running validation. `validation.issues` contains stored diagnostics; `pending` and `failed` explain whether those diagnostics are awaiting a newer check or a check failed. `checked_at` is the last completed check time. Opening a published object through the API does not acquire an editor lock or change its version.

## Query native JSON

```http
POST /api/v1/acme/objects/query
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

```json
{
  "bucket": "general",
  "prefix": "products",
  "fields": ["name", "price", "dimensions.width"],
  "filter": {
    "@status": { "eq": "published" },
    "available": { "eq": true },
    "price": { "lt": 50 }
  },
  "sort": [{ "field": "price", "direction": "asc", "type": "number" }],
  "locale": "en-US",
  "offset": 0,
  "limit": 20
}
```

The response contains `data`, an array of full object envelopes, and `pagination: { "offset": 0, "limit": 20, "hasMore": false }`. Each envelope retains its IDs, reference, status, version and diagnostics; `fields` projects only its inner `data`. In this example, the inner data is `{ "name": "Inflatable pillow", "price": 24.95, "dimensions": { "width": 40 } }`.

Queries include **draft and published entries by default**. The `@status` filter narrows that set. Neither a contract nor compatibility field definitions are needed. Filtering and sorting execute in the database before pagination. SQLite and MySQL are supported.

| Property | Behavior |
| --- | --- |
| `bucket`, `bucket_id`, `ref`, `prefix` | Same selection rules as listing. |
| `fields` | Up to 100 data property paths. Omitted means the complete selected-language document; `[]` returns `{}`. Missing properties return null. Parent and child selections cannot overlap. |
| `filter` | JSON object of conditions, combined with AND. `{}` or omission means no additional conditions. |
| `sort` | Up to five `{ "field", "direction", "type"? }` objects. Direction is `asc` or `desc`. Optional type is `string`, `number`, or `boolean`. |
| `locale` | One configured locale, default `en-US`. Applies to filters, sorting and response data; no wildcard or implicit fallback. |
| `offset` | Default 0; maximum 10,000. |
| `limit` | Default 20; range 1–100. |

Data paths address nested JSON object properties, such as `dimensions.width`. Property names support letters, digits, hyphens and underscores, with up to 12 dot-separated steps and 256 characters. Arrays and objects can be selected as whole values. Paths do not traverse relationship IDs, array indexes, or property names containing dots.

Metadata filters and sorting use `@id` (numeric object ID), `@entry_id` (ULID), `@bucket_id`, `@ref`, `@status`, `@lock_version`, `@created_at`, and `@updated_at`. A data property named `status` remains separate from publication status `@status`. Metadata is always included in the envelope and does not belong in `fields`.

| Operator | Behavior |
| --- | --- |
| `eq`, `ne` | Compare same-type JSON scalars. Numbers and decimals are compatible; strings, numbers and booleans remain distinct. `null` matches missing or null; `ne: null` requires a present non-null value. Other `ne` operands exclude missing/null values and other types. |
| `lt`, `lte`, `gt`, `gte` | Numeric comparison for numbers or case-sensitive lexical comparison for strings, including consistently formatted ISO dates. |
| `in` | OR-match 1–100 scalar operands using `eq` semantics. |
| `startsWith`, `endsWith`, `contains` | Case-sensitive literal string matching. Empty strings and surrounding whitespace are preserved; `%` and `_` are not wildcards. |
| `$and`, `$or` | Nonempty lists of condition objects, for example `{ "$or": [{ "price": { "lt": 20 } }, { "sale": { "eq": true } }] }`. |

Missing/null sort values come last in either direction. Explicit sorts have an object-ID ascending tie-breaker. Without a type hint, mixed scalars sort by type (boolean, number, string) and then value; descending reverses this order. Typed sorts reject non-null values of other types. Numeric sorts require exact `DECIMAL(38,12)` values. Whole arrays and objects cannot be sorted.

The query body is limited to 64 KiB, with at most eight nested filter groups and 100 groups/operators/`in` operands. Arrays must be JSON arrays and filters must be JSON objects. Unsupported operators, malformed JSON, invalid locales, excessive limits and unknown properties return 422 with an error path. Array membership filters, relationship expansion, aggregations and grouped reporting are not part of this endpoint; existing typed integrations can continue using [entry queries](/docs/api/entries#query-entries).

## Update an entry

```http
PATCH /api/v1/acme/objects/42
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

```json
{
  "ref": "products/inflatable-pillow",
  "lock_version": 0,
  "data": { "name": "Updated pillow", "price": 29.95 }
}
```

Supply the current `lock_version` and the complete replacement `data` object. Return the saved reference in `ref`, or supply `path` instead to place the entry in a folder. Omitted status and bucket are preserved. Moving a migrated entry to another bucket is rejected. The response increments `lock_version`; a stale version returns 422.

Omitted `contract` preserves the current choice; `null` selects Automatic, `false` selects No contract, and a name selects that workspace contract explicitly. Contract validation checks every saved language.

Optional `translations` replaces the entire translation map, for example `{ "nl-NL": { "name": "Kussen" } }`. Omission preserves translations; `{}` removes them. Translation values must be JSON objects keyed by configured locales other than `en-US`. Creation does not accept translations; add them with an update. Publication status remains shared across languages.

## Delete an entry

```http
DELETE /api/v1/acme/objects/42
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

```json
{ "lock_version": 1 }
```

Returns 204 with no body. Obtain the current version from a read or query first. Missing/negative versions or stale versions return 422 without deleting anything. A pending review also blocks deletion. Missing, foreign and file objects return 404; legacy linked form submissions are read-only and return 403.

Deletion removes the entry from normal listings, queries and delivery immediately, and moves it to **Trash for 30 days**. Both native and converted JSON entries use recoverable deletion. Related entries and files are retained, including fields previously configured to cascade; deleting a JSON entry no longer deletes its dependencies. Comments and tags are retained for recovery. An audit record and entry-deleted event retain workspace ownership, and relationship-bearing contracts are queued for revalidation. Files and form submissions retain their separate deletion workflows.

## Revisions, comparison and recovery

Successful changes automatically preserve JSON content, translations, locale selection, bucket/reference, contract selection, publication status and scheduled publication metadata. Changes through Objects, the editor, and compatibility Entries writers are covered, including pipeline and AI changes made through those writers. No-op content saves do not create duplicate snapshots. Revision IDs increase over time; `lock_version` remains an independent concurrency token.

Existing content is captured before its first change after this feature is installed. History cannot reconstruct earlier changes or recover entries deleted before installation. Opening history is read-only. Contracts themselves, file binaries, tags and comments are not versioned by content revisions; Trash separately retains existing tags and comments for recovery.

### Access

History includes drafts, old values and deleted content. It therefore requires the dedicated **`entries:history`** scope, rather than `entries:read`. The key creator must retain list and view entry permissions. Restore also requires **`entries:write`** and the creator's create/update permissions. Wildcard keys include these abilities. All operations remain workspace-scoped and require Objects activation.

### List and read revisions

```http
GET /api/v1/acme/objects/42/revisions?per_page=25
GET /api/v1/acme/objects/42/revisions/123
Authorization: Bearer YOUR_API_KEY
```

Lists return a cursor-paginated `{ "data": [...], "next_page_url": ... }` response, newest first. `per_page` defaults to 25 and is limited to 100. Follow the returned next-page URL. List rows include `id`, `object_id`, `entry_id`, `event`, `restored_from`, `actor`, `source` and `created_at`; they omit the content snapshot. The detail endpoint returns the same metadata plus `snapshot`, including `data` and `translations`, as an unwrapped JSON object. JSON objects, arrays, nulls, booleans, numbers and strings retain their types.

Sources identify user, API, system, queued job, pipeline or AI operations where the writer supplies that context; API key identity is retained internally without recording its secret. A baseline records the state when history began, not an inferred original author or historical timestamp. Submissions and files do not have entry revision endpoints.

### Compare two revisions

```http
GET /api/v1/acme/objects/42/revisions/compare?from=123&to=124
Authorization: Bearer YOUR_API_KEY
```

Returns `{ "from": 123, "to": 124, "changes": [...], "truncated": false }`. Each change has `path`, `operation` (`added`, `removed`, `changed`), `before` and `after`. Paths are JSON Pointers: literal slashes and tildes are escaped, and absent values remain distinct from explicit null through the operation. Nested objects are compared by property; arrays are compared as whole ordered values. At most 500 differences are returned. If truncated, read each revision's complete snapshot. Both revisions must belong to the requested object and workspace.

### Restore an earlier version

```http
POST /api/v1/acme/objects/42/restore
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "revision_id": 123, "lock_version": 7 }
```

Restores content, translations/locales, bucket/reference and contract selection as a **new revision**, with `restored_from` pointing to the selected revision. History is never rewritten. Current publication status and schedule are retained: restoring an already published entry changes the content delivered to connected apps immediately. This feature does not introduce a separate working draft beside live content.

The current contract rules and relationships must accept every restored locale. Pending reviews, another editor's lock on a web request, missing buckets/content types/contracts, stale versions and occupied references block restore. A failed restore changes neither content nor history. A revision never replaces an unrelated entry.

### Recover a deleted entry

```http
GET /api/v1/acme/objects/trash?per_page=25
Authorization: Bearer YOUR_API_KEY
```

Trash is cursor-paginated, newest deletion first. Rows include `object_id`, `entry_id`, `bucket_id`, `ref`, `lock_version`, `deleted_at` and `expires_at`, without document bodies. Use the **Trash row's** version token, not the entry's former token:

```http
POST /api/v1/acme/objects/42/restore
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "lock_version": 8 }
```

Omit `revision_id` to recover the deleted content, or supply a revision belonging to that object to recover an older version. Recovery preserves the original object and entry IDs, restores retained tags and comments, and always returns the entry as a **draft with no publication schedule**. It does not automatically republish it. Related data can change or be deleted independently; references do not restore deleted file binaries or other objects.

Successful restore/recovery returns `{ "id", "entry_id", "ref", "lock_version", "status" }`. Versions continue increasing across deletion and recovery. Missing, foreign, expired or already-purged objects return 404. Missing/stale tokens, reference conflicts and current validation failures return 422. Restore must succeed atomically before Trash is removed.

Expired items stop appearing in Trash and history and cannot be recovered even if the cleanup job has not yet run. The hourly `objects:prune-trash` task permanently removes expired archives, their revisions and retained comments in batches of at most 100 objects with a 20-second run budget. Active object history is retained until the object is deleted and its recovery window expires. Workspace deletion removes its history immediately; bucket/type deletion is not an object recovery operation and may make an old version unrestorable.

## Related guides

- [Contracts](/docs/content-modeling/contracts) — optional validation and automatic reference patterns.
- [API access](/docs/platform/api-access) — keys, abilities, origins and rate limits.
- [Entries API](/docs/api/entries) — compatibility API and typed queries.
- [Files API](/docs/api/files) — upload, read and delete files.
- [Errors and validation](/docs/api/errors-and-validation) — shared error response format.
