Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.
L1–L3
Unit 04 · Custom entities
Custom entities over the API and MCP
Custom entitiesPage 25 of 35Updated
List, read, create and change custom entity records over the REST API, read them through GraphQL, reach them through the site's MCP server, and import or export them in bulk: what is specific to custom entities.
Who this is for. Developers building a headless front end, an importer or an agent that works with custom entity records. After this page you can pick the right route for each job, write relationships, and run an import that can be repeated safely. The API guide covers the API in depth; this page gives only what is specific to custom entities.
Every request here is made as a backend (admin) user: send an OAuth access token, the preferred credential, or an API key, the older option (Authentication and authorisation). Reads are not limited to one site; writes are, and a write to a record of another site answers 404.
PUT or DELETE /cdna-api/custom-entities/<identifier>/records with ids (bulk)
A list takes the plural identifier; everything else takes the singular. Using the wrong form changes the response rather than raising an error: on this site, /custom-entities/apiSandboxes/2 (the plural with an id) answers 404 Page or record not found, and /custom-entities/apiSandbox (the singular for a list) also answers 200; asked for with properties, it reports "totalCount": 1 but an empty data, so list with the plural identifier.
Without properties, the answer is keyed by the path segment you used: data.apiSandbox for one record, data.apiSandboxes for a list. With properties, data is the record or the list itself. Lists take the usual filters, ordering, properties and paging: 100 records a page by default and 500 at most, and a limit that is not a number or is out of range is refused with a 400 rather than clamped.
HTTP
GET /cdna-api/custom-entities/courses?properties={id,slug,name}&limit=20
Authorization: Bearer <access token>
An edit makes a new revision, as a page edit does, and goes live in the same request when "activeRevision": true is inside patchRevision (patchRevision, revisions, or a revision by id). An id in the body of a create is refused with a 400. In the body of an edit it is dropped, because the id in the URL always wins, so you can PUT back a record you have read, id and all.
Writing relationships
Relationships are written on the record's own route, in relatedRecords, keyed by the relationship definition's identifier: a record id, or null, for a relationship whose maximum is 1; a list of ids for one that takes several. Only the relationships named change, and a list replaces what was there. On a definition apiSandbox with a single relationship mainRelated and a list relationship seeAlso:
{"result": "success", "data": {"id": 2, "relatedRecords": {"mainRelated": 6, "seeAlso": [6]}}, "message": "The API sandbox has been saved successfully."}
Live at once. Relationships belong to the record, not to a revision, so there is nothing to activate.
Clearing. Send null for a single relationship and [] for a list.
Refusals are 422 responses that name the relationship: an unknown identifier (Relationship with specified identifier not found for the record), a list sent to a single relationship (Invalid format for record; expecting record id or null), an id that matches no record (Record with the specified ID could not be found, at relatedRecords.seeAlso[0]).
Reading. Ask for properties={id,relatedRecords{id,slug,name}}: every relationship comes back, with those fields for each related record.
Reading backwards. Filter the owning side's list with ?relationships.records.id=6. Its totalCount counts links, not records.
GraphQL
The GraphQL API at POST /cdna-api/graphql reads custom entity records and never writes them. A definition is addressed as CustomEntities_CustomEntity_<identifier>, with the filter, orderBy, first and offset arguments and the fields you select:
graphql
query {
CustomEntities_CustomEntity_course(first: 10) {
id
slug
name
}
}
A site's MCP server offers custom entity definitions and records as pairs of list and change tools, with a tool that describes each entity's schema. An agent connected to the site can read the course catalogue, or add a store, with the same permissions as the user who signed it in. MCP, for editors and agents covers connecting and permissions.
Bulk import and export
Every record has an external id, a text field for the record's id in another system, and lists can be filtered by it: ?externalId=crm-1001. An import that sets it can be run again safely: look the record up by external id, update it when found and create it otherwise, and a second run changes nothing. External ids are not unique by themselves, so the look-up above is what keeps an import idempotent. The external-id pattern covers retries and idempotency.
The bulk route, PUT or DELETE /cdna-api/custom-entities/<identifier>/records with ids, publishes, unpublishes or deletes up to fifty records a call; its PUT sets published and nothing else. An empty list, or an id that is not a positive whole number, is refused outright. Every other change is one request per record.
To export, list the records with properties selecting only the fields you need, page by page, or have an Orchestration job render each record as a row.