REST API · v1

API Reference

Every unilayer resource is available over a JSON REST API. This page lists every endpoint with the permission it needs, its parameters and example payloads. For concepts and setup, see the documentation.

Base URL

https://<your-host>/api/v1

All paths below are relative to this base URL unless stated otherwise. Request and response bodies are JSON (Content-Type: application/json).

Authentication

Send the user's API token as a bearer token with every request. The only endpoints that don't need a token are GET / and the /health endpoints.

curl https://unilayer.example.com/api/v1/collections \
  -H "Authorization: Bearer $TOKEN"

Each endpoint below shows the permission it requires in a chip at the top right, e.g. collections:read. A write permission always includes the matching read.

Scoped collection permissions

The endpoints for collections and items also accept scoped permissions. A collection's scope is the part of its name before the first underscore. For example, shop_orders and shop_products both have the scope shop, so collections:shop:read grants read access to both.

Responses & errors

Items

Item endpoints wrap your data in an envelope. Your fields are in data and system values are in metadata, so they can never collide.

// single item
{
  "data": { "title": "Try unilayer", "done": false },
  "metadata": {
    "id": "550e8400-e29b-41d4-...",
    "index": 1,
    "createdAt": "2026-09-29T10:00:00Z",
    "updatedAt": null
  }
}
// list of items
{
  "data": [ { ... }, { ... } ],
  "metadata": {
    "count": 2,
    "limit": 100,
    "offset": 0,
    "hasMore": false
  }
}

All other endpoints return the resource or list directly, without an envelope.

Errors

Errors return a consistent body with an error code, a message and, where it applies, the field that caused the error.

{
  "success": false,
  "errors": [
    { "code": "VALIDATION_ERROR", "message": "Field 'title' is required",
      "field": "title", "statusCode": 400 }
  ],
  "requestId": "0HN7...",
  "timestamp": "2026-09-29T10:00:00Z",
  "apiVersion": "1.0"
}
StatusMeaning
400Invalid input: validation failure, bad filter or sort, unknown field, malformed ID. Creating something whose name already exists also returns 400
401Missing or invalid token, or missing permission. Changing an immutable role or policy also returns 401
404Resource not found
409Conflict, e.g. deleting or truncating a collection that another collection still references
429Rate limit exceeded
503Cluster node can't accept writes right now, or image transform queue is full

Rate limits

Limits are counted per client IP in fixed one-minute windows. Requests over a limit get 429 Too Many Requests.

PolicyLimitApplies to
global1000 / minCollections, items, assets, users
write50 / minEndpoints that change collections, items or schemas, asset create/upload, user creation, cluster snapshots
auth10 / minAuthentication endpoints

Cluster behaviour

In cluster mode you can send any request to any node:

  • Writes are forwarded to the leader automatically and acknowledged once a majority has them.
  • Reads are answered by the node that receives them and can lag the leader by about one heartbeat.
  • If there's no leader, writes return 503. After a 503 caused by lost leadership, the outcome of the write is unknown, so check before you retry.
Resource

Collections

Collections are typed tables. Collection names must start with a letter and can contain letters, digits, _ and - (max. 63 characters). Field names must start with a letter or _ and can contain letters, digits and _. They can't start with unilayer_ or be a PostgreSQL reserved word.

Field definition

PropertyTypeDescription
namestringRequired. Field name
typestringTEXT, INTEGER, REAL, BOOLEAN, DATETIME, JSON (plus aliases, see docs)
nullableboolDefault true
isRequiredboolMakes the field non-nullable
defaultValuestringDefault value
isUniqueboolAdds a unique index
isIndexedboolAdds an index
isReferenceboolMakes the field a foreign key to another collection
referenceCollectionstringThe referenced collection
GET/collectionscollections:read · scoped
List collections

Returns the names of the collections you can see. If you only have scoped permissions, the list contains only the collections in your scopes.

Query

limitint, default 100
offsetint, default 0
sortSort order
["shop_orders", "shop_products", "tasks"]
POST/collectionscollections:write · scoped
Create collection

Creates the collection and its database table.

{
  "name": "shop_orders",
  "fields": [
    { "name": "number", "type": "TEXT", "nullable": false, "isUnique": true },
    { "name": "total", "type": "REAL" },
    { "name": "customer", "type": "TEXT",
      "isReference": true, "referenceCollection": "shop_customers" }
  ]
}
201 Collection info400 invalid definition / already exists
GET/collections/{name}/infocollections:read · scoped
Get collection info

Returns the collection's metadata, its fields and its system fields.

{
  "name": "tasks",
  "tableName": "unilayer_tasks",
  "createdAt": "2026-09-29T10:00:00Z",
  "updatedAt": null,
  "isSystem": false,
  "fields": [ { "name": "title", "type": "TEXT", "nullable": false, ... } ],
  "systemFields": [ { "name": "unilayer_id", "type": "UUID", "isPrimaryKey": true, ... } ]
}
200404
GET/collections/{name}/fieldscollections:read · scoped
List fields

Returns the collection's field definitions as an array.

GET/collections/{name}/existscollections:read · scoped
Check existence

Returns true or false.

PUT/collections/{name}/schemacollections:write · scoped
Replace schema

Sends the complete list of fields. Fields that are new get added, fields that are missing get removed, and matching fields are kept. You can't change the name or type of an existing field.

{ "fields": [ { "name": "title", "type": "TEXT" }, { "name": "priority", "type": "INTEGER" } ] }
204400404
POST/collections/{name}/fieldscollections:write · scoped
Add field
{
  "name": "sku",
  "type": "TEXT",
  "options": { "isRequired": false, "isUnique": true, "isIndexed": false,
               "maxLength": 64, "defaultValue": null }
}

To add a reference, set options.isForeignKey: true and options.referenceCollection.

200400 invalid / field exists404 collection
PUT/collections/{name}/fields/{fieldName}collections:write · scoped
Update field

Replaces the field's options. Indexes and unique constraints are updated to match. The field's type can't be changed.

{ "options": { "isRequired": true, "isUnique": true } }
Options you leave out are reset to their defaults, so always send the full set.
200404
DELETE/collections/{name}/fields/{fieldName}collections:write · scoped
Delete field

Drops the field's column and all of its data.

200404
POST/collections/{name}/truncatecollections:write · scoped
Truncate collection

Deletes all items but keeps the schema. If other collections reference this one, add ?cascade=true. That also truncates every collection that references it, directly or indirectly. Without it, the request fails with 409 and the error names the collections that reference this one.

204404409 referenced
DELETE/collections/{name}collections:write · scoped
Delete collection

Drops the collection, its table and all of its data. If another collection still references it, the request fails with 409 and the error names the collections that reference it.

204404409 referenced
Resource

Items

Items are the rows of a collection. The {id} in the paths below is the item's unilayer_id (a UUID).

GET/collections/{name}collections:read · scoped
Query items

Query

ParameterDescription
filterfield:op:value. Separate conditions with , for AND and | for OR. Operators: eq ne gt ge gte lt le lte like in isnull notnull
sortasc/desc (insertion order), or field, field:desc, f1,f2:desc
limitPage size, default 100
offsetItems to skip, default 0
fieldsComma-separated field selection, e.g. title,customer.name. Use * for all fields
GET /collections/shop_orders?filter=total:gte:100,status:in:open,paid&sort=total:desc&limit=20

References are expanded into nested objects, up to 3 levels deep. The response uses the list envelope, and metadata.hasMore tells you whether there's another page.

200400 bad filter/sort404
GET/collections/{name}/{id}collections:read · scoped
Get item

Returns a single item in the item envelope.

200404
POST/collections/{name}collections:write · scoped
Create item

Send the item's fields as a plain JSON object. unilayer sets the system fields itself.

{ "number": "SO-1001", "total": 249.90, "customer": "8c1f...-uuid" }
201 item envelope400 validation404
PUT/collections/{name}/{id}collections:write · scoped
Update item

Updates the fields you send and sets updatedAt. Required (non-nullable) fields must always be included.

{ "number": "SO-1001", "total": 199.90 }
204400404
DELETE/collections/{name}/{id}collections:write · scoped
Delete item
204404
Resource

Assets

Uploading an asset takes two calls: first create the asset record to get its uid, then upload the content. The file type comes from the name's extension. Allowed extensions: pdf (document), png jpg jpeg svg gif webp (image), mp4 (video), and txt or no extension (text).

Asset metadata

{
  "uid": "3f2c...-uuid",
  "name": "logo.png",
  "storagePath": "...",
  "size": 48213,
  "contentType": "image/png",
  "fileType": "image",
  "checksum": "9f86d081884c7d65...",   // SHA-256
  "metadata": { "alt": "Company logo" },
  "uploadedAt": "2026-09-29T10:00:00Z",
  "isDeleted": false,
  "deletedAt": null,
  "version": 1
}
GET/assetsassets:read
List assets

Query

limit / offsetPaging, default 100 / 0
sorte.g. Name:asc (default), UploadedAt:desc
fieldsField selection, e.g. Name,Size,Version
fileTypedocument, image, video, text
nameName contains
minSize / maxSizeSize range in bytes
includeDeletedInclude soft-deleted assets. Requires assets:write
200 metadata[]
POST/assetsassets:write
Create asset

Creates the metadata record and returns the new uid. It doesn't upload any content yet.

{ "name": "logo.png" }
201 metadata400 name missing / type not allowed
PUT/assets/{uid}assets:write
Upload content

Uploads or replaces the file content. Every upload increases version, recalculates checksum and invalidates cached image variants.

{ "contentBase64": "iVBORw0KGgoAAAANSUhEUgAA..." }
200 metadata400 no / invalid content404
GET/assets/{uid}assets:read
Download asset

Returns the file content with its content type. For images you can add transform parameters:

width, heightTarget size in px. Aspect ratio is kept when only one is set
scaleMultiplier, e.g. 0.3. Can't be combined with width/height
fitcontain (default), cover, stretch
formatwebp, png, jpg, gif
quality1–100 (default 80), for jpg / webp
GET /assets/{uid}?width=300&height=300&fit=cover&format=webp

Transformed responses include an ETag. Send If-None-Match to get 304 when the image hasn't changed.

200 file304400 not an image / bad params404503 transform queue full
GET/assets/{uid}/metadataassets:read
Get metadata
200 metadata404
PUT/assets/{uid}/metadataassets:write
Update metadata

Replaces the asset's custom key/value metadata.

{ "metadata": { "alt": "Company logo", "credit": "Design team" } }
204404
DELETE/assets/{uid}assets:write
Delete asset

Soft-deletes the asset. You can restore it later.

204404
POST/assets/{uid}/restoreassets:write
Restore asset

Restores a soft-deleted asset.

204404
Security

Users

A user has a username, exactly one role and an API token.

The plain-text apiToken is returned only when you create a user or renew their token. unilayer stores only a hash, so save the token right away.
{
  "id": "a1b2c3d4-...",
  "username": "shop-frontend",
  "role": "shop-reader",
  "apiToken": "...",        // create / renew only
  "createdAt": "2026-09-29T10:00:00Z"
}
GET/usersusers:read
List users

Query: limit (default 100), offset, sort.

GET/users/{id}users:read
Get user
200404
POST/usersusers:write
Create user

If you leave out role, the user gets the default role (user).

{ "username": "shop-frontend", "role": "shop-reader" }
201 user + apiToken400 invalid / username taken
PUT/users/{id}/roleusers:write
Change role
{ "role": "shop-editor" }
204404
POST/users/{id}/renew-tokenusers:write
Renew token

Issues a new token and invalidates the old one immediately.

200 user + apiToken404
DELETE/users/{id}users:write
Delete user
204404
GET/users/rolesusers:read
List assignable role names

Returns the role names as strings. Useful for filling a role picker.

GET/users/me/permissionsany valid token
My permissions

Returns the permissions the calling token has. Useful for showing or hiding UI features.

["collections:shop:read", "assets:read"]
Security

Roles

A role bundles policies. The built-in admin role is immutable, so any attempt to change it returns 401.

{
  "id": "8765...-uuid",
  "name": "shop-reader",
  "description": "Read access to the shop",
  "createdAt": "2026-09-29T10:00:00Z",
  "isImmutable": false,
  "policies": ["shop-read"]
}
GET/rolessecurity:read
List roles
GET/roles/{name}security:read
Get role by name
200404
POST/rolessecurity:write
Create role
{ "name": "shop-reader", "description": "Read access to the shop" }
201 role400 invalid / name taken
PUT/roles/{roleId}security:write
Update role
{ "description": "Read-only shop access" }
200 role400 not found401 immutable
DELETE/roles/{roleId}security:write
Delete role
204400 not found401 immutable
POST/roles/{roleId}/policies/{policyId}security:write
Assign policy to role
204
DELETE/roles/{roleId}/policies/{policyId}security:write
Remove policy from role
204
Security

Policies

A policy is a named list of permission strings (see permissions). The built-in fullaccess policy is immutable.

{
  "id": "1234...-uuid",
  "name": "shop-read",
  "description": "Read all shop_* collections and assets",
  "createdAt": "2026-09-29T10:00:00Z",
  "isImmutable": false,
  "permissions": ["collections:shop:read", "assets:read"]
}
GET/policiessecurity:read
List policies
GET/policies/{name}security:read
Get policy by name
200404
POST/policiessecurity:write
Create policy
{
  "name": "shop-read",
  "description": "Read all shop_* collections and assets",
  "permissions": ["collections:shop:read", "assets:read"]
}
201 policy400 invalid / name taken
PUT/policies/{policyId}security:write
Update policy

Both properties are optional. If you send permissions, it replaces the whole list.

{ "description": "...", "permissions": ["collections:shop:read"] }
200 policy400 not found401 immutable
DELETE/policies/{policyId}security:write
Delete policy
204400 not found401 immutable
POST/policies/{policyId}/permissions/{permission}security:write
Add permission
POST /policies/1234...-uuid/permissions/assets:write
204
DELETE/policies/{policyId}/permissions/{permission}security:write
Remove permission
204
Operations

System

These endpoints live at the server root, not under /api/v1, unless the path says otherwise.

GET/public
Status & version
{
  "status": "OK",
  "message": "unilayer API is running",
  "authenticated": false,
  "version": "...",
  "commitHash": "...",
  "buildDate": "..."
}
GET/health · /health/live · /health/readypublic
Health checks

/health reports the overall status of the database, Valkey and storage. /health/live and /health/ready are meant for orchestrator probes. In cluster mode, /health reports Degraded (still HTTP 200) when a certificate is close to expiring.

POST/api/v1/graphqlcollections:read / write
GraphQL

A GraphQL API generated from your collections, with the same permissions as REST. The explorer is at GET /api/v1/graphql/ui and the SDL download at GET /api/v1/graphql/schema. See the GraphQL guide.

GET/api/v1/openapi/{document}.json—
OpenAPI document

A machine-readable description of this API, for client generators. An interactive reference is served at /api/v1.

POST/api/v1/cluster/snapshotcluster:admin
Request cluster snapshot

Asks this node to take a snapshot at the next log entry it applies. The response comes back right away: 202 means the request was accepted, not that the snapshot is done. Returns 400 if cluster mode is off. The cluster:admin permission is part of the built-in fullaccess policy.

202400