Dynamic Content
One generic set of endpoints serves every content model in your CMS. The model slug becomes the route segment.
Endpoints
The controller (DynamicContentController) resolves {modelSlug} to a content_models row and applies its api_config JSON to filter visible fields, default sort, and per-page caps.
List entries
GET /api/v1/blog-posts?per_page=10&sort=-published_at&filter[locale]=en
200 OK
{
"data": [
{
"id": "01H…",
"slug": "hello-world",
"title": "Hello world",
"status": "published",
"locale": "en",
"data": {
"title": "Hello world",
"excerpt": "First post.",
"body": "<p>…</p>",
"tags": ["intro"],
"hero_image": { "id": "…", "url": "/media/…", "alt": "…" }
},
"seo": { "title": "…", "description": "…" },
"published_at": "2026-05-29T10:00:00Z"
}
],
"meta": { "page": 1, "per_page": 10, "total": 137 },
"error": null
}
Query parameters
| Param | Example | Notes |
|---|---|---|
page | 2 | 1-based |
per_page | 20 | Capped by model's api_config.max_per_page |
sort | -published_at,title | Comma-separated; prefix - for descending |
filter[status] | published | Equality filter on top-level columns |
filter[locale] | en | Defaults to the site's default locale if model is localized |
filter[data.tags] | launch | Filter on a JSON path in the data column |
q | hello | Full-text on the model's title field + any text fields flagged searchable |
include | author,hero_image | Resolve relations and media inline |
fields | title,slug,excerpt | Limit returned top-level keys |
By default, only entries with status = published are returned. Drafts are visible only with API keys that have an admin role attached.
Get a single entry
You can address an entry by its UUID or by its slug. When a slug is used, the response defaults to the requested locale.
GET /api/v1/blog-posts/hello-world?include=author,related_posts
GET /api/v1/blog-posts/01H4Z… // by UUID
Single-type models
Single-type models always return one entry — no listing semantics. Address them as GET /api/v1/{modelSlug} (returns one object, not an array) or GET /api/v1/{modelSlug}/single as a verbose alias.
Create an entry
POST /api/v1/blog-posts
X-API-Key: …
X-API-Secret: …
Content-Type: application/json
{
"title": "New post",
"slug": "new-post",
"status": "published",
"locale": "en",
"data": {
"title": "New post",
"excerpt": "Quick hello.",
"body": "<p>Hello!</p>"
},
"seo": { "title": "New post", "description": "Quick hello." }
}
201 Created
{ "data": { "id": "01H…", … }, "error": null }
Required and validated fields come from the model's content_fields definitions. If you omit a required field, you get a 422 with details.
Update an entry
PUT /api/v1/blog-posts/01H4Z…
Content-Type: application/json
{
"status": "published",
"data": { "excerpt": "Updated excerpt." }
}
200 OK
{ "data": { "id": "01H…", … }, "error": null }
Partial updates are accepted: any keys you omit are left untouched. Each save creates a revision.
Delete an entry
DELETE /api/v1/blog-posts/01H4Z…
204 No Content
Soft delete only. The entry is hidden but recoverable from the admin trash.
Errors specific to this surface
| Status | Code | When |
|---|---|---|
| 404 | MODEL_NOT_FOUND | {modelSlug} doesn't match any active model |
| 404 | ENTRY_NOT_FOUND | UUID / slug not found in this model + locale |
| 422 | VALIDATION_FAILED | Returns per-field error map |
| 409 | SLUG_CONFLICT | Slug already exists for this model + locale |
entry.created / entry.updated; transitions to published emit entry.published. See Admin: Webhooks.