Troubleshooting · Developer docs

Troubleshooting

Grouped by symptom, never by module, because the person with the problem does not know which module has it. One hundred questions across ten pages, each answered with the causes in the order they usually turn out to be true.

Symptom 21 questions

API requests

Twenty-one symptoms from integrations that read and write a Core dna site through its REST and GraphQL APIs, and from front ends that submit its forms: what each one means, what causes it, and the request that works.

Most of these answer with a status that looks fine. The API answers 200 with an "Error: …" string in place of a field, 200 with the whole list for a filter it ignored, "saved successfully" for a revision that is not live, and 207 for a form submission nobody will act on. So the first check is the same for all of them: read the whole answer, not only the status, and when the answer to a write looks wrong, read the record again before changing anything.

A POST that works from the admin and answers 403 from a script on the site is the CSRF token: see 403 on a form submission or a POST from JavaScript.

My properties fields come back as "Error: Getter for property … not found", or glued together

Both answer HTTP 200, so only the body shows them.

  • Glued together: the fields in a selector are separated by commas or line breaks, never by spaces. {id name} asks for one field called idname, and every record comes back as {"idname": "Error: Getter for property idname … not found"}.
  • An error on one field: the record type has no property of that name, or it is spelt differently. The string names the class it looked in, such as Faq\Models\Api\Response\Page, which tells you which record in a nested selector it was.

The properties each record type has are on its page in the API reference. Layout area values are read with a getter and an alias, summary:areaText(summary), not as a property named after the area.

HTTP
GET /cdna-api/pages/page/1885?properties={id,name}                      fields separated by a comma
GET /cdna-api/pages/page/1885?properties={id,
  name}                                                                  or a line break
GET /cdna-api/pages/page/1885?properties={id,nav:areaText(navSection)}  an area, through a getter

More in Shaping the response with properties.

In the API reference: Shaping the response with properties.

Open this answer on its own page →
"414 Request-URI Too Long", or my long properties selector stops working

The URL is longer than the servers in front of the site accept. On this documentation site a URL of about 4,000 characters was answered and one of about 8,400 got an HTML 414 page, not a JSON error; much longer ones had the connection closed. A properties selector that reads a page with its sections and their components is the usual way to get there, since every character is percent-encoded.

Send the selector in the body of the GET instead, raw and wrapped in braces, with the header X-Body-Has-Parameters: 1. Filters, limit and offset stay in the query string.

Shell
curl -X GET 'https://www.example.com/cdna-api/pages/pages?parent.id=72&limit=20' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Accept: application/json' \
  --header 'X-Body-Has-Parameters: 1' \
  --data '{id,name,fullPath,activeRevision{id,areasContent}}'

See Send a long properties selector in a GET body and Query string or request body.

In the API reference: Request bodies.

Open this answer on its own page →
The selector in my GET body was ignored, and the full record came back

The API reads a GET body only when the request says so, with the header X-Body-Has-Parameters: 1. Without it the body is ignored and the record comes back in its default shape. With it, check three more things:

  • The body is the selector itself, raw and wrapped in braces: {id,name}, not JSON and not properties={id,name}.
  • Only the selector is read from the body. Filters, limit, offset and orderBy in the body are ignored; keep them in the query string.
  • Your HTTP library sends a body on a GET. Some drop it silently: browsers' fetch refuses one, so this is for server-side clients.

See Send a long properties selector in a GET body.

In the API reference: Request bodies.

Open this answer on its own page →
My filter returned every record

An unrecognised filter has no effect: the request answers as if it had been sent without it, with HTTP 200, the whole list, and a matching totalCount. The usual causes:

  • A suffix or a two-character operator. Only the symbol prefixes work: ! not, > and < exclusive bounds, % contains, | one of a comma-separated list. created_gte, name_contains and >=created are not recognised.
  • A key the type does not have, or a computed one such as fullPath.
  • A date it cannot read.

Check totalCount against what you expect, and test a new filter with a value that should match nothing: an unrecognised filter still returns everything.

HTTP Real answers from this documentation site
GET /cdna-api/pages/pages?created_gte=2026-10-01&properties={id}&limit=1
# 200 {"totalCount": 158, ...}      ignored: every page

GET /cdna-api/pages/pages?>created=2026-10-17 00:00:00&properties={id}&limit=1
# 200 {"totalCount": 18, ...}       applied

Every operator is in Filters, ordering and paging; confirming that a write did what was asked is in Confirming a request did what you asked.

In the API reference: Filters.

Open this answer on its own page →
A custom entity list answers a totalCount but no rows

List custom entity records with the plural identifier: /cdna-api/custom-entities/<plural identifier>. Custom entity routes take the type's identifiers exactly as defined, case included: the plural for a list, the singular for one record and for creating, editing and deleting.

With a properties selector, a list called on the singular still answers 200 with the right totalCount but empty data; without one, the records come back even so. One record on the plural is a 404.

HTTP Real answers from this documentation site's sandbox type
GET /cdna-api/custom-entities/apiSandbox?properties={id}       singular for a list
# 200 {"result": "success", "totalCount": 1, "data": []}

GET /cdna-api/custom-entities/apiSandboxes?properties={id}     plural: the rows come back

GET /cdna-api/custom-entities/apiSandboxes/2?properties={id}   plural for one record
# 404 {"result": "error", "message": "Page or record not found"}

Both identifiers are on the type's definition (GET /cdna-api/custom-entities/definitions). The routes are listed in the Custom Entities reference.

In the API reference: List records.

Open this answer on its own page →
The API answers 404 for a record that exists

A 404 with Page or record not found means the route, the method or the caller did not match, as often as it means the record is missing. Check, in this order:

  • The method. Creating goes to the type's route without an id: POST /cdna-api/pages/page. A POST to a record's own route, /pages/page/1885, is a 404; editing it is PUT.
  • Singular or plural. One record is the singular (pages/page/1885), a list the plural (pages/pages). On custom entities, the plural with an id is a 404.
  • The record type. Books, units and book pages share ids with pages but each has its own route: a book page is books/page/{id}, not pages/page/{id}.
  • Who is asking. A signed-in site member is refused with a 404 on everything outside the visitor routes, so the refusal looks like a missing route. A backend user's OAuth token or API key is the credential for this.
  • A repeated delete. The second DELETE of a record answers 404: the first one worked.

Every status the API answers with is in Responses and errors; the member and backend-user difference is in Authentication and authorisation.

Open this answer on its own page →
My edit saved, but the site did not change

Pages, blog posts, book pages, FAQ records and custom entity records are versioned. A write makes a new revision, and the site shows only the live one. A patchRevision or revisions write is saved as an inactive revision unless it carries "activeRevision": true inside it, so the request answers "saved successfully" and the live page stays as it was.

Two other causes look the same:

  • The record is unpublished, or scheduled. Your credential is a backend user's, which reads unpublished records by id; a visitor gets a 404. Check signed out.
  • The page cache. A page served from the cache shows the old content until it is refreshed.
HTTP
PUT /cdna-api/pages/page/1885?properties={id,activeRevision{id},lastRevision}
Content-Type: application/json

{
  "patchRevision": {
    "originRevision": 3488,
    "activeRevision": true,
    "areaContent(navSection)": "API sandbox"
  }
}

# without "activeRevision": true, lastRevision moves and activeRevision does not

An inactive revision already saved can be made live by its id: {"activeRevision": <revision id>} on the record's edit route. See Making a revision live, rolling back and publishing.

In the API reference: Edit a page.

Open this answer on its own page →
"Invalid reference to related records at activeRevision; expecting an id" when I send activeRevision: true

activeRevision means two things depending on where it is. Inside patchRevision or revisions, true makes the revision being written the live one. At the top level of the body it is the record's relation to its live revision, so it takes a revision id, and true there is refused with a 422 and nothing is saved. Move it inside the revision object.

HTTP Real answers from this documentation site's sandbox page
PUT /cdna-api/pages/page/1885
{"activeRevision": true, "patchRevision": {"originRevision": 3488, "areaContent(navSection)": "API sandbox"}}
# 422 "Invalid reference to related records at activeRevision; expecting an id for a single record or a list of ids for multiple records"

PUT /cdna-api/pages/page/1885
{"patchRevision": {"originRevision": 3482, "activeRevision": true, "areaContent(navSection)": "devdocs"}}
# 200: the new revision, 3483, is live

PUT /cdna-api/pages/page/1885
{"activeRevision": 3482}
# 200: an existing revision made live again, by its id

See Making a revision live, rolling back and publishing.

In the API reference: Make an earlier revision live.

Open this answer on its own page →
Updating one area emptied the others

The write used revisions, which builds a whole new revision from what the request sends: every layout area left out is empty in it. To change one area and keep the rest, use patchRevision, which copies a revision and changes only the keys you send, and name the area with areaContent(name). Send originRevision with the live revision's id: without it the copy is taken from the newest revision, which may not be the live one.

A top-level areaContent(name), outside any revision object, is no help either: it answers success and changes nothing.

HTTP A real answer on this documentation site's sandbox page
PUT /cdna-api/pages/page/1885?properties={id,activeRevision{id}}
Content-Type: application/json

{
  "patchRevision": {
    "originRevision": 3482,
    "activeRevision": true,
    "areaContent(navSection)": "devdocs"
  }
}

# 200 {"id": 1885, "activeRevision": {"id": 3483}}: navSection changed, the five sections kept

If the layout areas were lost already, make the revision before the write live again by its id: see Roll back a page to an earlier revision. The steps are in Change one area of a page without touching the rest.

In the API reference: Edit a page.

Open this answer on its own page →
My patch changed the wrong section, with no error

The collection's positions had drifted. Removing an element from a component collection does not renumber the ones after it, and a position you send is matched against the stored positions. On a collection A, B, C, D, removing 2 leaves A, C, D stored as 1, 3, 4; a later "remove 3", "patch 3" or "replace 3" then acts on C, the second element shown, and answers 200. The read shape has no positions, so a client cannot see the drift.

Appending and inserting are safe. To reorder, remove or change an element on a collection whose history you do not know, read it, change the list in your code, and write it back whole with areaContent(name), which also renumbers it.

The script is in Add, reorder or remove a section on many pages; every collection key is in Editing component collections.

In the API reference: Remove elements.

Open this answer on its own page →
A multiselect area answers 422 "Unexpected associative array encountered"

A multiselect area is stored as text, so it takes its chosen keys as a JSON-encoded string, not as an array. It reads back as that same string in areasContent; areaValues(name) gives you the list.

JavaScript
"areaContent(colours)": ["red", "blue"]          // 422 Unexpected associative array encountered
"areaContent(colours)": "[\"red\",\"blue\"]"     // stored

// in code
body.patchRevision['areaContent(colours)'] = JSON.stringify(['red', 'blue']);

// reading it as a list
// ?properties={id,colours:areaValues(colours)}

The value each input type takes, and how it reads back, is in Writing areas. On custom entity records area values are not validated, so a wrong key is saved rather than refused.

In the API reference: Edit a page.

Open this answer on its own page →
Custom fields look lost after a write

Usually they are not: the answer to the write leaves them out. A write's answer lists the custom field rows as that request left them, so a field emptied in the request, and every row a customFieldValues list did not name, is missing from it while still stored. Read the record again to see what it holds.

Two cases do lose data:

  • Products, product categories and people take a customFieldValues list as the complete set, and delete the rows it leaves out.
  • null for a field empties it.

Writing by name avoids both: {"customFields": {"fieldName": value}} changes the fields named and nothing else, on every type that has custom fields.

See Custom fields and Sync values from another system into custom fields.

In the API reference: Edit a menu element.

Open this answer on its own page →
"Could not find the referenced record with id 0"

A relation was sent as 0, which the API reads as the id of a record and cannot find. To clear a relation, send null; to leave it alone, leave the key out. Many client libraries and forms turn an empty value into 0, so convert it to null before sending.

The same message with another number means no record of that type has that id. An image or file id that does not exist is reported under image or file rather than under the field's name.

JavaScript
{"parent": 0}       // 422 Could not find the referenced record with id 0
{"parent": null}    // clears it
{}                  // leaves it unchanged

One exception: a custom URL is cleared with {"customUrl": {"url": ""}}, not with null. The rules for relations are in A relation as an id or an object.

Open this answer on its own page →
A custom URL is set after creating the record, and cleared with an empty string
  • Setting it: a custom URL is set after creating the record, with {"customUrl": {"url": "/campaign"}} on its edit route; check fullPath in that answer to confirm it took effect.
  • Clearing it: send an empty URL, {"customUrl": {"url": ""}}.

Start the path with a slash: it is stored exactly as sent, and one without breaks every link built from fullPath. A URL another record already holds is a 422, "This value is already used."

See Set and clear a custom URL through the API and Custom URLs and slugs.

In the API reference: Edit a page.

Open this answer on its own page →
My DELETE timed out: did it go through?

Ask for the record by its id with a backend credential. A 404 "Page or record not found" means it is gone; the record coming back means the delete did not happen, and sending it again is safe. Sending the DELETE again also answers the question: a second delete of a record that is already gone answers the same 404, and changes nothing.

A DELETE is the easy case. A POST that timed out may have created its record, and sending it again creates another; look for it before retrying, by an externalId you sent with it, for example.

HTTP
DELETE /cdna-api/custom-entities/apiSandbox/7
# 200 {"result": "success", "data": [], "message": "The Record has been successfully deleted."}

DELETE /cdna-api/custom-entities/apiSandbox/7
# 404 {"result": "error", "message": "Page or record not found"}

Which writes are safe to repeat, and how to make the others safe, is in Retries and writes that may have applied and Import records so that running the import twice changes nothing.

In the API reference: Delete a page.

Open this answer on its own page →
My OAuth token stopped working after four hours: 403 "Token doesn't exist or is not valid"

An access token lasts four hours (expires_in 14400), and an expired one answers 403, not 401. Exchange the refresh token at /oauth/token for a new one, with client_id and, for a server, client_secret in the body; then send the request again.

If the refresh itself fails with 400 invalid_grant, the refresh token is no longer good: it expires sixty days after it was issued, and it can be exchanged fifty times. On the fiftieth exchange the answer carries a new refresh token and the old one stops working, so a client that kept only the first one is locked out after fifty refreshes. Store the refresh token from every answer. When it has expired, someone has to sign in as the integration's user again.

Shell
curl https://www.example.com/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=THE_REFRESH_TOKEN \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET

# 200 {"token_type": "Bearer", "expires_in": 14400, "access_token": "CDNA-A-…", "refresh_token": "…"}
#      store both: the refresh_token may be a new one

A token module that does this is in Connect a server integration with OAuth and keep its token fresh; the flow is in Authentication and authorisation.

In the API reference: Refresh an access token.

Open this answer on its own page →
GraphQL answers 500 "Currently, only Query GraphQL requests are supported by this API"

The GraphQL endpoint reads; it does not write: send only query in the body, with every value straight in the query text rather than in variables, and write through the REST routes instead.

Begin the body text with the word query, and name each list by its type, Page, BlogPost, MenuElement. Separate fields with commas or line breaks, never spaces. Pass first to limit a list, and send the request as POST.

HTTP The one shape it accepts, with a real answer from this documentation site
POST /cdna-api/graphql
Content-Type: application/json

{"query": "query { Page(first: 3, filter: {parent: null}) { id, name, fullPath } }"}

# 200 {"result": "success", "data": {"Page": [{"id": 1, "name": "Home", "fullPath": "/"}]}}

See The GraphQL API and the GraphQL reference.

In the API reference: Run a query.

Open this answer on its own page →
My form submission through the API answered 207

A 207 is stored but not complete. Some answers failed the form's rules; they are kept, marked invalid, and the message reads "The Form Submission has been saved successfully, but it contains validation errors." Nothing that counts or acts on completed submissions sees it, so a client that treats every 2xx as success thanks the visitor for a submission nobody will read.

Show the errors from validationErrors, keyed by element name, and keep the submission's token from the answer. When the visitor corrects them, send every answer again to PUT /cdna-api/forms/submission/{token}, which completes the same submission: a new POST would start a second one. Treat 200 as done and 202 as "go and pay".

Every status is in Submitting a form through the API; a working server route is in Submit a Core dna form from a React or Vue front end.

In the API reference: Submit a form.

Open this answer on its own page →
One visitor's form shows up twice among the submissions

Three ways it happens:

  • A 207, then a new attempt. The first attempt was stored as an incomplete submission, and the corrected one was sent as a new POST, so it is a second submission. Continue the first with PUT /cdna-api/forms/submission/{token} instead of letting a correction post as a new submission; wire this into any JS-model form so it continues the original token rather than starting a second, incomplete one.
  • A retried request. A POST that timed out may have been stored, and sending it again stores another. Nothing in a lost answer can be looked up later.
  • A page-per-step visitor who reloads or opens the form in a new tab: the token is lost and a new submission begins.

Count and act on completed submissions only: a job that acts on them should run on the submission's Completed event. Where a duplicate complete submission would matter, add a Hidden element filled with an id your code makes for each attempt, and remove duplicates where the submissions are consumed.

See Submitting a form through the API.

In the API reference: Submit a form.

Open this answer on its own page →
Showing an error when a JS-model submission is rejected

Every JS-model form needs the result callback: put data-submit-result-callback="formResult" on the .coredna-js-form container and define window.formResult in the site's script. The platform calls it with the response first, for every answer including a 207, a 422 or a 500, and does nothing more when it returns false, so the callback shows the messages, keeps the answers and gives the buttons back.

The callback is in Add the platform's form scripts to a form layout, and with the jump back to an earlier step in Validation.

Open this answer on its own page →
A conditional field never appears, or never hides

The rules are applied in the browser by js-forms-conditionals.js, and each of these stops it without a visible error:

  • The rules are not on the page. The layout must print them into window['formConditionals' + form id]. The console then says "Conditionals not found for form ID" and every field stays visible.
  • The script missed its start. It starts only on DOMContentLoaded. Loaded with async, or added by another script, it can arrive later and never run.
  • No container. It works on .coredna-js-form containers with a _formId field, on page-per-step forms too.
  • The element has no id. Elements of some older forms have none, so they render without data-id and no rule can reach them.

A form that shows everything correctly and still cannot be completed is a different problem: a required field on a step hidden by a rule stays required on the server. Hide a group instead of the step.

The wiring is in Conditional logic and the js-forms-conditionals.js reference.

Open this answer on its own page →
Go deeper

Was this page helpful?

Your answer goes to the docs team.