Responses & Errors
Every successful CMS API response uses a consistent envelope, regardless of whether you called a schema, list, show, or search endpoint. Errors follow a second, equally consistent shape.
Success Envelope
All responses wrap their payload under a top-level data key. List responses additionally include a meta block with pagination information.
List Response
Used by GET /{contentType} and GET /{contentType}/search.
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "10 Laravel Tips",
"slug": "10-laravel-tips",
"content": "<p>…</p>",
"category": "tech",
"views": 250,
"featured_image": "b3c1f2-d4e5.jpg",
"featured_image_url": "https://cms.appambit.com/{YOUR_APP_KEY}/cms/media/b3c1f2-d4e5.jpg",
"author_id": "a11b22c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5",
"published_at": "2025-01-20T15:30:00+00:00",
"created_at": "2025-01-19T10:00:00+00:00",
"updated_at": "2025-01-20T15:30:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 42,
"last_page": 3
}
}
meta field |
Description |
|---|---|
current_page |
The page number you requested (1-based). |
per_page |
How many items this response contains. |
total |
Total number of entries matching the request. |
last_page |
The highest valid page number. |
query |
(search only) Echoes the q parameter you sent. |
Single-entry Response
Used by GET /{contentType}/{uuid}.
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "10 Laravel Tips",
"category": "tech",
"published_at": "2025-01-20T15:30:00+00:00"
}
}
Schema Catalog Response
Used by GET /_schemas. See the endpoint reference for a full example.
Single-schema Response
Used by GET /_schemas/{contentType}. Each entry in fields describes a single field with its name, type, required, label, and searchable metadata.
Cache Headers
The API sets Cache-Control so your client or CDN can safely store responses:
| Endpoint | Cache-Control |
|---|---|
/_schemas, /_schemas/{type} |
public, max-age=3600 |
/{contentType} |
public, max-age=3600 |
/{contentType}/{uuid} |
public, max-age=3600 |
/{contentType}/search |
no-cache, no-store |
Search responses are explicitly opted out of caching so users always see the latest ranking.
Field Types
Content types can mix any of the following field types. The type value returned by the schema endpoint matches one of:
| Type | Stored as | Text-searchable | Can be filterable | Can be sortable | Notes |
|---|---|---|---|---|---|
text |
string | Yes | Yes | Yes | |
rich_text |
HTML string | Yes | — | — | |
number |
numeric | — | Yes | Yes | Use gt, gte, lt, lte for range queries. |
boolean |
"true" / "false" |
— | Yes | — | |
date |
ISO-8601 date | — | Yes | Yes | |
date_time |
ISO-8601 datetime | — | Yes | Yes | |
email |
string | Yes | Yes | — | |
url |
string | Yes | Yes | — | |
select |
string or array | — | Yes | — | Array when the field is configured as multi-select. |
media |
file id (uuid.ext) |
— | — | — | Returned with a {field}_url companion — see Media. |
relation |
UUID or array of UUIDs | — | Yes | — | Can be expanded with populate. |
json |
arbitrary JSON | — | — | — | Escape hatch for free-form data. |
How the flags work
"Text-searchable" is the only column that's decided by the field type itself — only text, rich_text, email, and url values participate in full-text search.
"Can be filterable" and "Can be sortable" mean the content type editor is allowed to enable filter/sort on the field. A field is only actually queryable with filter= or sort= once the content type author adds it to that content type's filterable_fields / sortable_fields list, which you can read from the schema endpoint.
Dates
The system timestamps created_at, updated_at, and published_at are always returned as ISO-8601 strings with an explicit offset, e.g. "2025-01-20T15:30:00+00:00".
Custom date and date_time fields are round-tripped as-stored — the API does not re-format them on read. We recommend writing these values as ISO-8601 from the dashboard so they're safe to parse directly on the client.
Media Fields
Every field of type media is stored as a bare file identifier (for example "b3c1f2-d4e5.jpg"). To spare clients from having to build CDN URLs by hand, the API automatically adds a sibling {field}_url key pointing at the hosted asset:
{
"featured_image": "b3c1f2-d4e5.jpg",
"featured_image_url": "https://cms.appambit.com/{YOUR_APP_KEY}/cms/media/b3c1f2-d4e5.jpg"
}
The {field}_url companion is included:
- On list, show, and search responses.
- When you request the media field through sparse fieldsets.
Tip
Always use the {field}_url value in your UI. Do not build URLs by concatenating paths — the hostname is configurable and the raw file id is not guaranteed to round-trip through every network hop.
Relations and populate
A relation field stores the UUID (or array of UUIDs, for multi-value relations) of a related entry. By default you receive just the UUID:
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "10 Laravel Tips",
"author_id": "a11b22c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5"
}
}
Add ?populate={field} on the single-entry endpoint to replace the UUID with the full related entry, in place:
GET /api/v1/blog_posts/550e8400-e29b-41d4-a716-446655440000?populate=author_id
X-App-Key: {YOUR_APP_KEY}
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "10 Laravel Tips",
"author_id": {
"id": "a11b22c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5",
"name": "Jane Doe",
"email": "jane@example.com",
"published_at": "2024-11-02T09:00:00+00:00",
"created_at": "2024-11-02T09:00:00+00:00",
"updated_at": "2024-11-02T09:00:00+00:00"
}
}
}
For multi-value relations (multiple: true on the content type), the populated value is an array of embedded entries. See Relation Population for the parameter rules.
Error Envelope
All 4xx and 5xx responses use this shape:
{
"error": {
"status": 400,
"code": "INVALID_FILTER",
"message": "The field 'foo' is not a valid filterable field."
}
}
| Field | Description |
|---|---|
status |
Echoes the HTTP status code. |
code |
A stable machine-readable identifier. Use this in your error handling. |
message |
A human-readable explanation. Safe to surface to developers. |
Error Code Reference
| HTTP | code |
When |
|---|---|---|
| 400 | MISSING_APP_KEY |
The X-App-Key header was not sent. |
| 400 | INVALID_APP_KEY |
No application exists for the provided app key. |
| 400 | INVALID_FILTER |
You filtered on a field that is not marked filterable, or the filter failed validation. |
| 400 | INVALID_OPERATOR |
The operator in filter[field][op] is not one of the supported operators. |
| 400 | INVALID_SORT |
You sorted on a field that is not marked sortable. |
| 400 | TOO_MANY_FILTERS |
More than 10 filter fields were included in the request. |
| 400 | QUERY_TOO_SHORT |
The q parameter for search is shorter than 2 characters. |
| 404 | CONTENT_TYPE_NOT_FOUND |
The content-type slug does not exist, or has no published entries for this tenant. |
| 404 | ENTRY_NOT_FOUND |
The requested UUID does not exist, or is not published. |
| 429 | — | Rate limit exceeded. Back off and retry with exponential delay. |
| 504 | QUERY_TIMEOUT |
The request took longer than the 5-second timeout. Narrow your filters or search term. |
Handling 504 QUERY_TIMEOUT
The server enforces a 5-second limit on dynamic queries. If your search term is too broad, or your filter matches an enormous result set, you may hit this timeout.
{
"error": {
"status": 504,
"code": "QUERY_TIMEOUT",
"message": "The query took too long to execute. Try narrowing your filters."
}
}
Common mitigations:
- Shorten or make the search term more specific.
- Add a
filter[...]that narrows the result set before searching. - Reduce
per_pageto trim response size.
Handling 429 Too Many Requests
If you exceed the plan's rate limit, the API returns 429. Clients should retry with an exponential backoff. The SDK handles this automatically; direct integrators should implement their own retry policy.
End-to-end Examples
List with a simple filter
GET /api/v1/blog_posts?filter[category]=tech&sort=-published_at&per_page=10 HTTP/1.1
Host: cms.appambit.com
X-App-Key: {YOUR_APP_KEY}
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "10 Laravel Tips",
"slug": "10-laravel-tips",
"category": "tech",
"featured_image": "b3c1f2.jpg",
"featured_image_url": "https://cms.appambit.com/{YOUR_APP_KEY}/cms/media/b3c1f2.jpg",
"author_id": "a11b22c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5",
"published_at": "2025-01-20T15:30:00+00:00",
"created_at": "2025-01-19T10:00:00+00:00",
"updated_at": "2025-01-20T15:30:00+00:00"
}
],
"meta": { "current_page": 1, "per_page": 10, "total": 8, "last_page": 1 }
}
Validation error
GET /api/v1/blog_posts?filter[unknown_field]=x HTTP/1.1
Host: cms.appambit.com
X-App-Key: {YOUR_APP_KEY}
{
"error": {
"status": 400,
"code": "INVALID_FILTER",
"message": "The field 'unknown_field' is not a valid filterable field."
}
}
Missing app key
GET /api/v1/blog_posts HTTP/1.1
Host: cms.appambit.com
{
"error": {
"status": 400,
"code": "MISSING_APP_KEY",
"message": "A valid X-App-Key header is required."
}
}