Skip to Content
Errors & limits

Errors & limits

How errors arrive

The API follows the GraphQL convention: errors come back in an errors array, each with a message and an extensions.code. Branch on extensions.code (and, for the cases below that say so, on the message), not on the HTTP status.

  • The request was rejected before running (a field that does not exist, a query that is too deep or too complex, malformed variables): HTTP 400, and the body has no data key.
  • The request ran but a field failed (missing token, module off, invalid form input…): HTTP 200, data is null (or partial), and the error carries the path of the field that failed.
  • Two exceptions: a rate limit answers HTTP 429, and a suspended project answers HTTP 403. The body has the same shape as above.

Error reference

extensions.codeHTTPWhen
UNAUTHENTICATED200project-auth is missing (no token), or is not a valid token or matches no project (not matched token).
MODULE_DISABLED200The operation belongs to a feature the project has not switched on.
BAD_REQUEST200Input failed validation (for example a page limit over 200, or an invalid contact form).
NOT_FOUND200The record you asked for does not exist or is not published.
CONFLICT200The request clashes with existing data, for example an address that is already subscribed.
CONTACT_RECIPIENTS_NOT_CONFIGURED200sendContactRequest on a project with no contact recipients set up.
INTERNAL_SERVER_ERROR200An unexpected error on our side. The message is Internal server error.
TOO_MANY_REQUESTS429You hit a rate limit. Wait for the seconds in Retry-After.
PROJECT_SUSPENDED403The token is valid, but the project is suspended. Retrying does not help: contact support@cms-craft.com.
GRAPHQL_VALIDATION_FAILED400Unknown field or argument, depth limit, or an introspection query.
BAD_USER_INPUT400A variable is missing or has the wrong type.
QUERY_TOO_COMPLEX400The query is over the complexity limit.
BAD_REQUEST405A mutation sent with GET (mutations need POST).

Examples

Missing token:

{ "errors": [ { "message": "no token", "locations": [{ "line": 1, "column": 3 }], "path": ["publicProjectInfo"], "extensions": { "code": "UNAUTHENTICATED" } } ], "data": null }

A wrong, malformed or regenerated token gives the same shape with "message": "not matched token". So does a new token in the first 2 seconds after you regenerate it (Authentication).

Module not enabled:

{ "errors": [ { "message": "Module \"ORDERS\" is not enabled for this project", "path": ["createPublicOrder"], "extensions": { "code": "MODULE_DISABLED", "moduleType": "ORDERS" } } ], "data": null }

Not found (for example getPublicContentBySeoUrl with an unknown URL):

{ "errors": [ { "message": "Content not found", "path": ["getPublicContentBySeoUrl"], "extensions": { "code": "NOT_FOUND" } } ], "data": null }

Unknown field:

{ "errors": [ { "message": "Cannot query field \"usersFindAll\" on type \"Query\".", "locations": [{ "line": 1, "column": 3 }], "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } } ] }

Rate limits

Free during beta. Paid plans will follow; we will announce pricing and give notice before any charge. We monitor usage during the beta, and limits may change with notice in the changelog.

Requests are counted per operation (each root field of a query counts on its own), over a 60-second window. The numbers below are the current defaults:

OperationsLimit per minute
Most queries200 (per API token or per IP, see below)
publicProducts, publicCategories, publicCategoryTree, getPublicPets, sitemapData, publicConnectionItems, publicConnectionSchedule30 per IP
createPublicOrder, createPublicBooking20 per IP
sendContactRequest, subscribeNewsletter, unsubscribeNewsletter, submitRegistration, cancelRegistration, submitAdoptionRequest, publicOrderByNumber5 per IP
publicBookingByReference5 per IP, and 60 per API token

The 200-per-minute limit is counted per API token when the project is a developer project and cms-craft does not host its site (the usual case if you build your own frontend). Otherwise it is counted per client IP address. The other limits are always counted per IP.

When you go over, the response has HTTP 429, a Retry-After header with the number of seconds to wait, and this error:

{ "errors": [ { "message": "Too many requests", "locations": [{ "line": 1, "column": 12 }], "path": ["cancelRegistration"], "extensions": { "code": "TOO_MANY_REQUESTS" } } ], "data": null }

Every response carries the counters as headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (seconds), plus a suffixed set for the operation’s group, for example X-RateLimit-Limit-read.

What this means in practice:

  • Your server is one IP. When your site calls the API from its server, every visitor shares that server’s per-IP limits. A contact form handled by your server allows 5 submissions per minute for the whole site; cache reads (the Quickstart caches every query until the content changes) instead of querying per visit.
  • Builds count too. A static build that renders hundreds of pages one query each can hit the 200-per-minute limit. Prefer list queries over one query per page, limit build concurrency, and retry after Retry-After.

Query limits

  • Depth: at most 10 levels of nesting. Deeper queries fail with GRAPHQL_VALIDATION_FAILED: 'MyQuery' exceeds maximum operation depth of 10.
  • Complexity: at most 1000 points per request. Every selected field costs 1 point, and some list queries (such as publicProducts, publicCategories, sitemapData) add a fixed 10 to 50. Over that, QUERY_TOO_COMPLEX: Query complexity 1200 exceeds maximum allowed complexity 1000. Ask only for the fields you render.
  • Introspection is disabled; use the downloadable schema.