Common headers (all model routes)
HeaderNotes
AuthorizationRequired. Bearer <token>
X-Tenant-IdRequired.
GET
/models
List models

Returns all tenant models and all global models.

Response

{
  "models": [
    {
      "id": 1,
      "modelKey": "blog_post",
      "name": "Blog Post",
      "description": null,
      "isGlobal": false,
      "status": "active",
      "fields": []
    }
  ]
}
GET
/models/:id
Get a model

Returns a single model with its fields. Returns 404 if not found.

POST
/models
Create a model

Creates a new tenant model. Requires admin or modeller role.

Request body

FieldNotes
modelKeyRequired. Unique identifier key.
nameRequired.
descriptionOptional.
fieldsOptional array of field definitions.

Field definition shape

{
  "fieldKey": "title",
  "name": "Title",
  "fieldType": "string",
  "sortOrder": 1,
  "required": true,
  "defaultValue": null,
  "allowedModelIds": []
}

Alternate: a TypeScript type as the body

Send Content-Type: text/plain with a TS interface/type declaration instead of JSON, and the model is created from it directly — no fields array needed.

interface BlogPost {
  title: string;
  viewCount: number;
  published?: boolean;
  publishedAt?: Date;
  status: 'draft' | 'published';
  tags?: string[];
  author: Author;
}

The interface name becomes modelKey/name. Each property becomes a field: string→string, number→decimal, boolean→boolean, Date→datetime, a string-literal union→enum (the literals become validationRules.options), T[]/Array<T>→an array field, Imaginary/Imaginary[]→imaginary/imaginary_array (reserved sentinel names, checked before the rule below), and a capitalized identifier (like Author above)→a content reference, resolved against an existing model whose modelKey matches that name (snake_cased). A property without ? is required. This also works on PUT /models/:id and POST /space/:id/models.

PUT
/models/:id
Replace a model

Replaces a model's definition including all fields. Automatically migrates existing content rows — adds default values for new required fields and removes data for deleted fields. Returns a migration summary. Requires admin or modeller role. Returns 403 for global models. Accepts the same TS-type text/plain body as POST /models above.

Response

{
  "model": {},
  "migration": {
    "contentUpdated": 3,
    "fieldsAdded": [
      "summary"
    ],
    "fieldsRemoved": [
      "legacy_field"
    ],
    "fieldsChanged": []
  }
}
DELETE
/models/:id
Delete a model

Permanently deletes a model and its fields. Returns 409 if content exists for the model or if another model's field references it. Returns 403 for global models. Requires admin or modeller role.

POST
/models/:id/validate
Validate content data

Validates a data object against the model's required fields without writing to the database.

Request body

{
  "data": {
    "title": "Hello",
    "body": "World"
  }
}

Response

{
  "valid": true,
  "errors": []
}
POST
/models/:id/validate-change
Validate proposed field changes

Dry-runs a proposed field list against existing content to identify migration issues before calling PUT /models/:id. Requires admin or modeller role.

Response

{
  "valid": true,
  "affectedContent": 12,
  "changes": {
    "fieldsAdded": [
      "summary"
    ],
    "fieldsRemoved": [],
    "fieldsChanged": []
  },
  "errors": []
}
GET
/models/:id/content
List content for a model

Returns paginated content items for the model.

Query parameters

ParameterNotes
pageDefault 1.
pageSizeDefault 20, max 100.
statusOptional filter.