Parameters every endpoint shares
| Parameter | On | Does |
|---|---|---|
properties | Every endpoint | The fields and relations to return; below |
limit | Lists | Records per response. Without it a list returns every match, except jobs and custom entity records, which page by 100 and refuse more than 500 |
page | Lists | The page of limit records, counting from 1 |
offset | Lists | Records to skip. It wins over page |
orderBy | Lists | field:ASC or field:DESC, comma-separated. Stored fields only |
retrieveUnpublished=1 | Lists | Includes unpublished records, for backend users. One record read by its id comes back either way |
| Anything else | Lists | A filter on a stored field or a relation; below |
A limit, page or offset that is not a whole number is a 400 that names it.
Filters
| Write | Matches |
|---|---|
slug=apis | Equal |
!slug=apis | Not equal |
>id=100, <id=100 | Greater, less. There is no greater-or-equal |
%25name=guide | Contains (%, encoded as %25) |
%7Cid=3,4,9 | Any of a list (|, encoded as %7C) |
parent.id=12 | A field of a relation |
parent=NULL | An empty relation |
Filter on stored fields, named exactly as in the record's model: not a computed field such as fullPath, not >=, and not a suffix form such as id_gt. Compare the totalCount with the unfiltered list's to confirm a filter applied. On pages, posts, books, FAQ and custom entity records the name lives on the revision, so filter on activeRevision.name.
Shaping the response with properties
| Write | Returns |
|---|---|
{id,name,fullPath} | Those fields |
{id,parent{id,name}} | A relation, with its own fields |
{id,title:activeRevision.name} | A field under another name, following a dotted path |
{id,created(format:"Y-m-d")} | A getter given an argument, by name |
{id,activeRevision{intro:areaText(intro)}} | An area's text; areas belong to the revision |
{pages{id,name}} | A list under its plural, data.pages |
Separate fields with commas. Spaces are removed before the list is split, so {id name} asks for one field called idname, which comes back as "Error: Getter for property idname … not found" in a 200. The names are the model's, listed in the Reference.
On a POST or PUT, properties goes in the query string. On a GET whose selector is too long for a URL, send it as the body, raw and in braces, with the header X-Body-Has-Parameters: 1; the example at the foot of this page does. Only properties is read from a GET body: filters, limit and orderBy stay in the query string, and a JSON body on a GET is ignored. Send the selector with its braces.
Request bodies
Send JSON with Content-Type: application/json, or a form. A body that is not valid JSON is read as empty, so a PUT with a stray comma answers success and changes nothing. Send Accept: application/json on every request, so that errors come back as JSON too.
| To set a relation, send | The API |
|---|---|
7 or {"id": 7} | Points it at record 7 |
{"id": 7, "name": "Guides"} | Points it at record 7 and edits record 7 |
{"name": "Guides"} | Creates a record and points at it |
null | Clears it. 0 is not an id |
[3, 4] on a to-many relation | Replaces the whole set; it does not add |
published is a date and time: true publishes now, a future time schedules, false or null unpublishes. Making a revision live never changes it.
The response
| Key | Holds |
|---|---|
result | success or error, always |
data | The record or records |
totalCount | On a list, the matches before paging |
message | What happened, on writes and errors |
validationErrors | The fields that failed, on a 422 |
| Request | data is |
|---|---|
A list with a bare selector, {id,name} | The list |
A list without properties, or with {pages{…}} | An object holding the list under the plural: data.pages |
| One record with a bare selector | The record |
One record without properties | An object holding it under the singular: data.page |
| A create or an edit | The record under the singular, with a message |
A few routes answer outside the envelope: the file upload, the OAuth endpoints and synchronous web events. Their pages show what they return.
Status codes
| Status | Means |
|---|---|
| 200 | Success, creates included |
| 400 | A parameter the API cannot read, such as a limit that is not a whole number, or a bad list of ids |
| 401 | No credential was found |
| 403 | A credential was found and refused, a CSRF token is missing, or the user may not do this |
| 404 | No such route or record. Also what a signed-in member gets on a route that is not for members |
| 405 | The route does not take this method, such as GET /cdna-api/graphql |
| 409 | A conflict: deleting the live revision, or continuing a form submission that is already complete |
| 422 | Validation failed; validationErrors says where |
| 500 | The server could not complete the request |
Form submissions add 207 (stored, a later step still has errors) and 202 (a payment is pending); Form submissions has the details.
Errors
A failed validation names each field you sent, with a list of the rules it failed:
{
"result": "error",
"validationErrors": {
"email": [
{ "message": "This value should not be blank.", "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3", "propertyPath": "email" }
]
},
"message": "This action failed validation"
}
Read the keys of validationErrors to put each message next to its field. Log message, but do not show it: it can hold class names. On a development site an error can also carry a trace.
When a 200 is not what you expected
| Symptom | Cause |
|---|---|
| A filtered list returns every record | The filter names a field the record does not store; Filters |
A field is "Error: Getter for property … not found" | A misspelt field, or two fields with only a space between them |
| An edit succeeds and the site does not change | The new revision is not live. Send "activeRevision": true inside the patchRevision or revisions object (at the top level it takes a revision id); and send originRevision with patchRevision, or it copies the newest revision, not the live one |
A PUT succeeds and nothing changed | The body was not valid JSON; or, on a custom entity record, an area was set at the top level instead of in patchRevision |
| A patch changed the wrong component | Positions drift after a removal; Collection operations |
A list has a totalCount and no rows | Custom entity records listed by the singular identifier; lists take the plural |
| A date area holds the wrong date | Dates are stored as sent, in any format |
| A job never starts | Its entity event's method is not one the record fires; Entity Events |
Routes not documented here
The admin uses routes of its own under /cdna-api, such as its record picker and tree views. They are internal, change without notice, and are left out of this reference.