Skip to content

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.

JSON
{
  "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}.

JSON
{
  "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:

JSON
{
  "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:

Default (no populate)
{
  "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}
With populate
{
  "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:

JSON
{
  "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.

JSON
{
  "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_page to 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}
JSON
{
  "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}
JSON
{
  "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
JSON
{
  "error": {
    "status": 400,
    "code": "MISSING_APP_KEY",
    "message": "A valid X-App-Key header is required."
  }
}