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"
}
| Status | Meaning |
|---|---|
400 | Invalid input: validation failure, bad filter or sort, unknown field, malformed ID. Creating something whose name already exists also returns 400 |
401 | Missing or invalid token, or missing permission. Changing an immutable role or policy also returns 401 |
404 | Resource not found |
409 | Conflict, e.g. deleting or truncating a collection that another collection still references |
429 | Rate limit exceeded |
503 | Cluster 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.
| Policy | Limit | Applies to |
|---|---|---|
global | 1000 / min | Collections, items, assets, users |
write | 50 / min | Endpoints that change collections, items or schemas, asset create/upload, user creation, cluster snapshots |
auth | 10 / min | Authentication 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 a503caused by lost leadership, the outcome of the write is unknown, so check before you retry.
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
| Property | Type | Description |
|---|---|---|
name | string | Required. Field name |
type | string | TEXT, INTEGER, REAL, BOOLEAN, DATETIME, JSON (plus aliases, see docs) |
nullable | bool | Default true |
isRequired | bool | Makes the field non-nullable |
defaultValue | string | Default value |
isUnique | bool | Adds a unique index |
isIndexed | bool | Adds an index |
isReference | bool | Makes the field a foreign key to another collection |
referenceCollection | string | The referenced collection |
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
limit | int, default 100 |
offset | int, default 0 |
sort | Sort order |
["shop_orders", "shop_products", "tasks"]
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" }
]
}
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, ... } ]
}
Returns the collection's field definitions as an array.
Returns true or false.
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" } ] }
{
"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.
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 } }
Drops the field's column and all of its data.
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.
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.
Items
Items are the rows of a collection. The {id} in the paths below is the item's unilayer_id (a UUID).
Query
| Parameter | Description |
|---|---|
filter | field:op:value. Separate conditions with , for AND and | for OR. Operators: eq ne gt ge gte lt le lte like in isnull notnull |
sort | asc/desc (insertion order), or field, field:desc, f1,f2:desc |
limit | Page size, default 100 |
offset | Items to skip, default 0 |
fields | Comma-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.
Returns a single item in the item envelope.
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" }
Updates the fields you send and sets updatedAt. Required (non-nullable) fields must always be included.
{ "number": "SO-1001", "total": 199.90 }
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
}
Query
limit / offset | Paging, default 100 / 0 |
sort | e.g. Name:asc (default), UploadedAt:desc |
fields | Field selection, e.g. Name,Size,Version |
fileType | document, image, video, text |
name | Name contains |
minSize / maxSize | Size range in bytes |
includeDeleted | Include soft-deleted assets. Requires assets:write |
Creates the metadata record and returns the new uid. It doesn't upload any content yet.
{ "name": "logo.png" }
Uploads or replaces the file content. Every upload increases version, recalculates checksum and invalidates cached image variants.
{ "contentBase64": "iVBORw0KGgoAAAANSUhEUgAA..." }
Returns the file content with its content type. For images you can add transform parameters:
width, height | Target size in px. Aspect ratio is kept when only one is set |
scale | Multiplier, e.g. 0.3. Can't be combined with width/height |
fit | contain (default), cover, stretch |
format | webp, png, jpg, gif |
quality | 1–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.
Replaces the asset's custom key/value metadata.
{ "metadata": { "alt": "Company logo", "credit": "Design team" } }
Soft-deletes the asset. You can restore it later.
Restores a soft-deleted asset.
Users
A user has a username, exactly one role and an API token.
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"
}
Query: limit (default 100), offset, sort.
If you leave out role, the user gets the default role (user).
{ "username": "shop-frontend", "role": "shop-reader" }
{ "role": "shop-editor" }
Issues a new token and invalidates the old one immediately.
Returns the role names as strings. Useful for filling a role picker.
Returns the permissions the calling token has. Useful for showing or hiding UI features.
["collections:shop:read", "assets:read"]
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"]
}
{ "name": "shop-reader", "description": "Read access to the shop" }
{ "description": "Read-only shop access" }
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"]
}
{
"name": "shop-read",
"description": "Read all shop_* collections and assets",
"permissions": ["collections:shop:read", "assets:read"]
}
Both properties are optional. If you send permissions, it replaces the whole list.
{ "description": "...", "permissions": ["collections:shop:read"] }
POST /policies/1234...-uuid/permissions/assets:write
System
These endpoints live at the server root, not under /api/v1, unless the path says otherwise.
{
"status": "OK",
"message": "unilayer API is running",
"authenticated": false,
"version": "...",
"commitHash": "...",
"buildDate": "..."
}/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.
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.
A machine-readable description of this API, for client generators. An interactive reference is served at /api/v1.
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.