Entity curation API
The entity curation API lets applications create and curate catalog entities, maintain shared vocabulary values such as tags and domains, and read the active entity catalog.
All endpoints require authentication and return the standard DataDios response envelope:
{
"STATUS": "SUCCESS",
"MESSAGE": "",
"DATA": {}
}
A refused request has STATUS: "FAIL" and a machine-readable code in DATA.reason. Applications
should branch on DATA.reason, never on the text in MESSAGE.
Entity references use the public shape {guid, name}. Responses do not expose internal
identifiers, relationship keys, or search fields. Attributes are addressed and returned by name
within their owning entity type.
Write entities
| Method and path | Purpose | Success status |
|---|---|---|
POST /entities | Create an entity within an existing entity type | 201 when created; 200 when the name already exists |
PATCH /entities/{guid}/attributes | Set or clear attribute values on an entity | 200 |
POST /entities/vocabulary | Create a vocabulary value, such as a tag, domain, or environment | 201 when created; 200 when the value already exists |
PATCH /entities/{guid}/name | Rename a user-created entity or vocabulary value | 200 |
DELETE /entities/vocabulary/{guid} | Remove a vocabulary value from every entity and delete it | 202 |
DELETE /entities/attributes/{type_guid}/{name} | Withdraw an attribute from an entity type | 202 |
POST /entities/curation-jobs | Apply attribute values across many entities | 202 |
Create an entity
{
"entity_type": "<entity type guid>",
"name": "Customer Orders",
"attributes": {
"domain": ["Finance"],
"tags": ["PII", "GDPR"]
}
}
POST /entities is create-or-get, not create-or-update. Names are compared after trimming spaces
and ignoring letter case. If the name already exists, the API returns that entity with
"created": false and writes nothing.
One case does not return the existing entity: if the name you supplied is one that another value
was renamed onto, the create is refused with 409 name_already_in_use. A value keeps its
original identity through a rename, so a value created as "PII" and renamed to "Sensitive" is still
identified as "PII" — creating "Sensitive" would therefore mint a second, separate value displaying
the same text in the same type. The refusal is what prevents that. Use the existing value instead
of creating a new one.
The reverse also follows from the same rule: creating "PII" after that rename returns the same
value with "created": false, and its name in the response reads "Sensitive" — the name you
asked for identified it, and the response reports what it is called now. Match on the returned
guid, not on the name you sent.
If a create request includes attributes but finds an existing entity, the request is refused
with 409 entity_already_exists. Use PATCH /entities/{guid}/attributes to update an entity that
may already exist.
From the DataDios release after v1.13.0,
entities of a type DataDios manages, such as dashboard, can't be created through this API. The
request is refused with 409 type_instances_managed, because DataDios adds those entries itself
(see Dashboards in the catalog). You can still set
their attributes. When you set an attribute that refers to entities by name, a
name of a managed type must be one that already exists: naming a dashboard that doesn't exist is
refused with the same 409 type_instances_managed, rather than creating it.
Set or clear attributes
{
"attributes": {
"tags": ["PII"],
"cost_center": []
}
}
An empty list clears that attribute. An attribute omitted from the request is left unchanged. The operation is all-or-nothing: if one value is refused, nothing in the request is written.
Create or rename a vocabulary value
Create a value:
{
"entity_type": "<tag type guid>",
"name": "PII"
}
Rename a value:
{
"name": "Sensitive PII"
}
PATCH /entities/{guid}/name renames any entity you created, not only a vocabulary value — the
same call renames a tag value and an ordinary entity. Values discovered from a source and built-in
values keep their source-owned names and cannot be renamed. Two values in one type cannot be given the same name: a
rename onto a name another value already displays is refused with 409 name_already_in_use.
Creating a value is matched loosely, renaming is matched exactly. Creating "pii" where "PII" already exists returns the existing value rather than a second one, because a new value is matched on its name with spaces trimmed and letter case ignored. A rename compares the stored name exactly, so it is possible to end up with "PII" and "pii" side by side in one type. Treat a value's name as a label for people and its guid as its identity — do not use letter case to tell two values apart.
A rename changes only the display name. Existing assignments, filters, and counts continue to refer to the same value.
A rename cannot be undone by the system and is not recorded in an audit trail. The previous name is returned in the rename response and is not stored anywhere afterwards, so keep it if you need it. Renaming also reorders the value within name-sorted listings: if you are paging through a list while a rename happens, the renamed row can appear twice or not at all on the pages you have left to fetch. Start the listing again if you need an exact set.
Delete a vocabulary value or withdraw an attribute
Both delete endpoints accept an optional confirmation body:
{
"confirm": true
}
When the operation affects more than 10,000 entities, omitting confirmation returns
409 confirmation_required. Only the JSON boolean true is accepted; the string "true" is not
confirmation. Operations affecting 10,000 or fewer entities do not require confirmation.
Withdrawing an attribute is irreversible and has no retention window. The attribute disappears
from reads and writes when the request is accepted, while stored values are removed in the
background. Attributes cannot be renamed or have their value type changed. The built-in domain,
env, tags, classification, and groups attributes cannot be withdrawn.
Curate many entities
{
"entity_guids": ["<entity guid>", "<entity guid>"],
"entity_type": "<entity type guid>",
"set_values": {
"domain": ["Finance"]
},
"delta_values": {
"tags": {
"add": ["PII"],
"remove": ["Legacy"]
}
}
}
set_values supplies the complete desired list for an attribute. delta_values adds and removes
individual values. An attribute can appear in only one of these request sections.
A bulk request accepts at most 25,000 entities and 10 attributes. If entity_type is supplied,
attribute names are resolved against that type and the built-in attributes. If it is omitted,
only built-in attributes are available.
A guid that no longer resolves is reported under DATA.skipped.entity_not_found, with a count and
a sample of up to 50 guids. It appears in the submit response and in the background job's status,
so losing the submit response does not lose the report. Entities skipped later because an attribute
does not apply or the entity was retired are added to the same place as the job runs. If no guid
resolves, the request is refused with 400 no_entities_resolved.
Track background operations
The two delete endpoints and the bulk curation endpoint return 202 with DATA.request_id. Poll
the existing POST /async_status_check endpoint with:
{
"request_id": "<request id>",
"job_type": "entity_bulk_curate"
}
Use the job type that matches the submitted operation:
| Operation | job_type |
|---|---|
| Delete a vocabulary value | entity_vocabulary_delete |
| Withdraw an attribute | entity_attribute_withdraw |
| Curate many entities | entity_bulk_curate |
Large removals are processed in batches of 5,000. An entity updated at the same time can briefly remain filterable by the removed value; a background repair corrects that temporary state.
Read entities
| Method and path | Purpose | Success status |
|---|---|---|
POST /entities/filter | Filter one entity type by attribute values | 200 |
POST /entities/catalog | One name-sorted page spanning several entity types | 200 |
POST /entities/lookup | Read the rows for a list of guids you already hold | 200 |
POST /entities/info | Read schema drift and curated attributes for entities by guid | 200 |
POST /entities/facets | List values for one attribute or count named attribute/value pairs | 200 |
GET /entities?type={guid} | Browse active entities of one type | 200 |
GET /entities/{guid} | Read an active entity profile | 200 |
GET /entities/{guid}/children | List direct active children | 200 |
GET /entities/resolve/{guid} | Resolve a live guid or remembered rename | 200; unknown guid returns 404 |
Read endpoints return active entities only, with no request option to include missing or retired
entities. POST /entities/info is the exception: it answers for missing and retired entities too.
Review and audit views are separate from this API.
Filter entities
{
"entity_type": "<entity type guid>",
"terms": [
{
"attribute": "domain",
"values": ["Finance"]
},
{
"attribute": "tags",
"values": ["PII", "GDPR"],
"any": true
}
],
"page_size": 50
}
Terms combine with AND. Values within a term also combine with AND unless that term sets
"any": true, which changes only that term to OR. Every filter is scoped to entity_type because
the same attribute name can mean different things on different types.
Catalog entities
POST /entities/catalog is one name-sorted page spanning several entity types — the catalog
screen's table. It is not a widened filter: it has its own request shape and its own cursor kind.
{
"entity_types": ["<entity type guid>", "<entity type guid>"],
"terms": [{ "attribute": "domain", "values": ["Finance"] }],
"name_prefix": "orders",
"page_size": 50
}
entity_types selects which types the page spans; an empty list means every type. terms filters
the same way POST /entities/filter does. name_prefix matches case-insensitively. All fields are
optional.
From the DataDios release after v1.13.0, four entity types no longer exist: View,
Materialized view, Procedure and Function. Objects of those kinds are now catalogued as
Table, and each row carries its own kind in sub_type — view, materialized view,
procedure or function. A plain table's sub_type is null.
The same arrangement already applied to files, which share one file type and carry their
extension in sub_type.
What changes for you:
- Listing entity types returns four fewer. A saved request naming one of their guids is answered
404 entity_type_not_found. - Selecting Table in the catalog now returns views and routines alongside tables. Use
sub_typeto tell them apart, or to filter client-side. - An entity keeps its guid and its name. Only its type and
sub_typechange. - Attributes declared on Table are untouched, and so are the built-in
domain,env,tags,classificationandgroupsattributes, which every type shares.
Every attribute you declared on View, Materialized view, Procedure or Function is deleted by the upgrade, together with every value recorded against it and every label built from those values. An attribute belongs to one entity type, so an attribute declared on View has no owner once the type is gone. This is irreversible and has no retention window.
Read the values out before you move to the release after v1.13.0: POST /entities/catalog over
each of the four types returns their rows with the curated values. Afterwards you can declare the
same attribute on Table and record the values again from what you saved.
The response carries two fields no other read returns, alongside the usual entities and
next_cursor:
{
"STATUS": "SUCCESS",
"MESSAGE": "",
"DATA": {
"entities": [{ "guid": "<entity guid>", "name": "orders" }],
"count": 1250,
"count_capped": true,
"matched_definitions": [
{ "guid": "<attribute def guid>", "name": "domain" }
]
}
}
count is the total matching the request, capped at a fixed ceiling; count_capped tells you
whether the number is exact or a floor. matched_definitions lists every attribute definition a
filter term's attribute name matched — useful because the same attribute name can belong to more
than one type when the request spans several types, and a caller seeing only rows cannot tell which
definition it filtered on.
Sending more than 10,000 entity type guids in one request is refused with
400 invalid_request ("Malformed request body"), and an invalid name_prefix with
400 invalid_name_prefix.
A request with no type selection, no filter term and no name_prefix is the catalog's landing
state: it reads every type the schema has, and is answered normally.
Look up entities by guid
POST /entities/lookup is the read to use when you already hold a set of entity guids and want
their current rows in one call — for example, guids you stored earlier or received from another
read. Send up to 500 guids in one request:
{ "guids": ["<guid>", "<guid>"] }
It answers with the same rows every other list read returns, and it is not paged: your guid list is the page, so no cursor comes back.
{
"STATUS": "SUCCESS",
"MESSAGE": "",
"DATA": {
"entities": [{ "guid": "<entity guid>", "name": "orders" }],
"forwarded": {}
}
}
Three behaviours are worth knowing before you build on it:
- A guid that names no active entity is left out of the response, not treated as an error. There are several ordinary reasons for this, so treat an absent guid as normal rather than building a list of cases to guard against: cataloguing runs in the background, so an object can be visible elsewhere in the platform before its entity exists; the entity may since have been retired; and a data source shared with you is catalogued under its owner's account, not yours. Read the response by guid and show a row with no attributes when a guid is absent.
- Order is not preserved and repeated guids collapse. Match rows to your list by guid rather than by position.
- A renamed entity comes back under its new guid. If an entity has been renamed since you
stored its guid, the lookup follows the rename and returns the current row — under the new guid,
not the one you sent. The
forwardedmap in the response tells you which guid answered which request ({"<guid you sent>": "<guid that answered>"}), and is present even when empty. Check it for any guid you do not find among the returned rows.
Sending more than 500 guids is refused with 400 too_many_guids. An empty list is refused with
400 invalid_request, the same code any malformed body gets — the list is a required field, not a
value checked once the request is otherwise valid.
Read entity information
POST /entities/info answers "what do you know about these entities?" for guids you already hold.
It returns one result per guid: whether the guid still names an entity, and one entry per
information type you asked for in include. There are two types:
schema_drift— the schema changes the drift job recorded for that entity within the lastdays.curated_attributes— every attribute value the entity carries now, built-in and custom.
It has two modes:
summary, the default, takes up to 100 guids and returns one state and asummaryfor each.itemsstays empty. One call behind a table page; send more guids in batches.detailtakes exactly one guid and one information type and returns itsitemsa page at a time. The drill-down behind one row.
{
"guids": ["<guid>", "<guid>"],
"include": ["schema_drift", "curated_attributes"],
"mode": "summary",
"days": 30
}
{
"guids": ["<guid>"],
"include": ["schema_drift"],
"mode": "detail",
"days": 30,
"page_size": 50
}
| Field | Rule |
|---|---|
guids | Required. 1 to 100 distinct guids; repeats collapse. The format is not checked — a guid that names nothing comes back not_found, so one bad guid does not fail the batch. |
include | Required. The information types to return: schema_drift, curated_attributes. |
mode | summary (default) or detail. |
days | Whole number, 1 to 365, default 30. How far back schema_drift reads. curated_attributes ignores it. |
page_size | detail only. 1 to 100, default 50. A drift page can hold fewer items: it also stops once its diffs pass about 4 MB. Follow next_cursor; do not count items. |
X-Pagination-Cursor header | detail only. Send the previous response's next_cursor to get the next page. A cursor works only with the same guid, include and days. |
The response keys results by the guid you sent:
{
"STATUS": "SUCCESS",
"MESSAGE": "",
"DATA": {
"results": {
"<guid>": {
"state": "resolved",
"entity": {
"guid": "<guid>",
"name": "orders",
"entity_type": "table"
},
"info": {
"schema_drift": {
"state": "has_data",
"reason": null,
"summary": {
"highest_impact": "HIGH",
"change_count": 20,
"latest_change_at": "2026-10-01T09:30:00"
},
"items": []
},
"curated_attributes": {
"state": "has_data",
"reason": null,
"summary": {
"value_count": 2,
"truncated": false,
"attributes": [
{
"attribute_name": "owner",
"ordinal": 0,
"value": "finance-team"
},
{
"attribute_name": "tags",
"ordinal": 0,
"value": "PII",
"reference": { "guid": "<tag guid>", "name": "PII" }
}
]
},
"items": []
}
}
},
"<old guid>": {
"state": "unresolved",
"reason": "renamed",
"new_guid": "<guid>"
},
"<unknown guid>": { "state": "unresolved", "reason": "not_found" }
}
}
}
In detail mode items is filled and DATA.next_cursor appears beside results while another
page exists. It is absent on the last page. Drift items come newest first:
"items": [
{
"version": 12,
"sync_time": "2026-10-01T09:30:00",
"inherited": false,
"object_id": "sales:public:orders",
"version_summary": { "...": "..." },
"object_details": { "...": "..." }
}
]
A data source's items are per-sync summaries — version, sync_time, inherited and
version_summary, with no object_id or object_details.
A column's change is recorded with its table, so a column's item is cut from the table's record:
{
"version": 12,
"sync_time": "2026-10-01T09:30:00",
"inherited": false,
"object_id": "sales:public:orders",
"version_summary": null,
"object_details": {
"diff_type": "update",
"meta_diff": {
"attributes": {
"AMOUNT": {
"is_diff": true,
"property_diff": { "DATA_TYPE": [true, "BIGINT", "INTEGER"] }
}
}
}
}
}
object_idis the table's, andobject_detailsholds this column's entry only, never its siblings'.- Each property is
[changed, new value, old value]. An added column has empty old values and a dropped column has empty new values. diff_typesays what happened to the table:update,insertordelete.inheritedistruewhen the table itself was added or dropped, so the column is listed without a change of its own. It isfalseeverywhere else.version_summaryis null, because the table's summary describes the whole table.
Reading a column through its table's records is limited to 10 seconds. If that read fails, the
columns in the request return error for schema_drift and every other entity keeps its answer.
A NUL character in a table's record comes back as U+FFFD in a column's item.
Some connectors record drift for the column itself. Those items come back as stored, like a
table's: the column's own object_id, its version_summary and its full object_details.
Guid states. Every guid gets a state, and unresolved carries a reason:
state | reason | Meaning | entity and info returned? |
|---|---|---|---|
resolved | none | An active entity | Yes |
unresolved | missing | Not found by the last sync; it may come back | Yes |
unresolved | retired | Gone | Yes |
unresolved | renamed | Renamed. new_guid is the current guid; ask again with it | No |
unresolved | not_found | No record: it never existed, its data source was deleted, or it is not catalogued yet | No |
Information states. Every information type answers with the same four states, and each type carries its own, so one type being unavailable does not affect the others in the same response:
state | Meaning |
|---|---|
has_data | The type holds something for this entity. summary is filled. |
no_data | The type holds nothing for this entity. summary is null. |
unavailable | The type cannot answer for this entity. reason says why. |
error | This type failed; other types are unaffected. |
For schema_drift:
state | reason | Meaning |
|---|---|---|
has_data | null | At least one drift record within days. |
no_data | null | No drift record within days. |
unavailable | drift_not_supported | The connector has no drift support, or the entity belongs to no data source. |
highest_impact is HIGH, MEDIUM, LOW, or null when no record carried an impact score.
For a column it is usually null: the impact is scored for the table as a whole, so it is not
reported on the column. A column's change_count adds one for each sync whose table record shows
a change to the column, including the sync that added or dropped the table.
For curated_attributes, has_data means the entity carries at least one value and no_data
means it carries none. It is never unavailable.
- Each attribute is one value:
attribute_name,ordinal(its position within a multi-valued attribute) andvalue. A value that is another entity, such as a tag or a domain, also carriesreferencewith that entity'sguidandname. - In
summarymode the values are insummary.attributes. An entity with more than 200 values, or whose values weigh more than 40 KiB (attribute names, text, JSON and number values, and the names of referenced entities), returnstruncated: trueand an emptyattributes;value_countstill gives the total. Read that entity indetailmode. - A number value is a JSON number when that is exact: a whole number up to 2^53, or any value
a float holds without loss. Any other number is sent as decimal text: plain decimal text up to
40 characters, sign and decimal point included, such as
"12345678901234567890123456789012345678", and exponent form past that, such as"1E+5000". A number inside a JSON value is sent as stored. - A date value is ISO-8601 text in UTC with no offset, such as
"2026-10-01T10:00:00". Drift'ssync_timeandlatest_change_athave the same form. - In
detailmode the values are initems, ordered by attribute name, andsummaryholds onlyvalue_count. - A withdrawn attribute's values are left out.
- A value that points at an entity no longer active has a null
valueandreference. - Missing entities, and entities retired because they were dropped, still return their attributes. An entity retired by a rename returns none: its attributes moved to the entity it was renamed to.
A detail request for attributes, and one of the items it returns:
{
"guids": ["<guid>"],
"include": ["curated_attributes"],
"mode": "detail",
"page_size": 50
}
"items": [
{
"attribute_name": "tags",
"ordinal": 0,
"value": "PII",
"reference": { "guid": "<tag guid>", "name": "PII" }
}
]
Seven drift behaviours to know before you build on it:
no_dataforschema_driftis not proof that nothing changed. A record exists only for a sync that found a change, and the same state covers a data source with drift turned off (switch it on from the Drift tab of the data source dialog).- A database or schema reads
has_datawhenever one of its tables changed. Itschange_countcan be 0 in that case. - Drift follows a rename. The new guid returns the history recorded under the old name.
- Missing and retired entities still return their drift, including the record of the drop.
- Drift recorded before it was linked to entities is linked by the upgrade. A record whose object is no longer listed in the data source stays hidden.
- The first drift sync of a data source records every object, so everything reads
has_datafor that window. - Only connectors that support drift answer. The rest return
unavailable.
If every information type you asked for fails, the whole request returns 500 internal_error
instead of an error state. So a request for one type never returns error.
More than 100 distinct guids is refused with 400 too_many_guids, and a cursor that is malformed
or was issued for a different guid, include or days with 400 invalid_cursor. Everything else
is 400 invalid_request: an unknown include, detail with more than one guid or more than one
include, a page_size or a cursor in summary mode, days outside 1-365, and a page_size
outside 1-100.
Read facets
List the values for one attribute:
{
"entity_type": "<entity type guid>",
"attribute": "tags",
"page_size": 50
}
Count a named set of attribute/value pairs:
{
"entity_type": "<entity type guid>",
"pairs": [
{
"attribute": "domain",
"value": "Finance"
},
{
"attribute": "tags",
"value": "PII"
}
]
}
A facet request must contain exactly one of attribute or pairs. The pair-count response echoes
the supplied attribute and value beside each count.
Stored facet counts can temporarily be higher than the number of visible filter results. Counts include entities in the temporary missing state, while reads return active entities only. A zero-count facet value is still returned.
Browse and paginate
Browse one entity type with GET /entities?type={guid}. Filter, browse, children, and facet-value
responses use cursor pagination:
{
"STATUS": "SUCCESS",
"MESSAGE": "",
"DATA": {
"entities": [
{
"guid": "<entity guid>",
"name": "orders"
}
],
"next_cursor": "<opaque cursor>"
}
}
next_cursor is omitted on the final page. Store it unchanged, then send it only in the
X-Pagination-Cursor request header on the next call:
X-Pagination-Cursor: <opaque cursor from the previous page>
Do not put cursors in a JSON body or query string; those forms are refused. Do not decode,
construct, modify, share between users, or reuse a cursor on another endpoint. A malformed,
truncated, modified, endpoint-foreign, or cross-user cursor returns 400 invalid_cursor.
The facet pair-count shape is not paginated. Supplying either page_size or
X-Pagination-Cursor with pairs returns 400 invalid_request.
Page size must be between 1 and 500. The API does not return a total result count.
Read a profile
GET /entities/{guid} returns the entity, its type and parent, and values grouped by attribute
name:
{
"STATUS": "SUCCESS",
"MESSAGE": "",
"DATA": {
"entity": {
"guid": "<entity guid>",
"name": "orders"
},
"type": {
"guid": "<entity type guid>",
"name": "table"
},
"parent": {
"guid": "<parent guid>",
"name": "public"
},
"attributes": {
"domain": [
{
"guid": "<domain guid>",
"name": "Finance"
}
],
"tags": [
{
"guid": "<tag guid>",
"name": "PII"
}
],
"score": [7]
}
}
}
type and parent are each null when that related row is retired; parent is also null for
a root entity. A retired referenced value is returned as null rather than exposing stale
identity information.
Read direct children
GET /entities/{guid}/children returns only the entity's direct active children. It does not
return ancestors, breadcrumbs, a recursive subtree, missing entities, or retired entities.
Resolve a guid
GET /entities/resolve/{guid} has two successful outcomes.
A live guid, or an old guid that forwards to a live renamed entity, returns:
{
"outcome": "entity",
"entity": {
"guid": "<entity guid>",
"name": "orders"
}
}
When a rename is remembered but its successor is no longer active, the response returns:
{
"outcome": "alias_memory",
"alias": {
"old_name": "orders_old",
"successor_guid": "<successor guid or null>",
"method": "<rename method>",
"established_at": "<timestamp>"
}
}
successor_guid remains available while the successor row exists, including when it is retired,
and is null only after that row is gone. An unknown guid returns 404 entity_not_found.
Remembered renames are resolved in one request; callers do not follow alias chains themselves.
Request rules and limits
Unrecognized fields and non-object JSON bodies are refused with 400 invalid_request; they are
not silently ignored. Other limits are also refusals, never silent truncation:
| Limit | Maximum |
|---|---|
| Filter terms | 50 |
| Values in one filter term | 50 |
| Named facet pairs | 50 |
| Guids in one lookup | 500 |
| Guids in one info request | 500 (distinct) |
| Items in one info detail page | 100 |
| Bulk entities | 25,000 |
| Attributes in one bulk request | 10 |
| Text value length | 1,000 characters |
| JSON value size | 16 KB |
| Number precision | 38 significant digits |
| Request body size | 8 MB |
Curated values cannot contain a NUL character.
Refusal codes
These lists cover the codes reachable through the endpoints on this page, not every refusal code the entity API has — a handful more belong to endpoints (attribute definitions, entity deletion, the review queue) outside this guide's scope.
400: Fix the request
invalid_request, invalid_cursor, invalid_page_size, invalid_filter_shape,
attribute_not_filterable, type_mismatch, not_in_allowed_values, cardinality_violation,
unknown_value_type, unknown_def, ref_type_mismatch, empty_value, too_many_values,
too_many_defs, def_in_both_request_shapes, no_entities_resolved, value_too_long,
value_too_large, number_too_precise, nul_byte_in_value, too_many_allowed_values,
too_many_guids, invalid_type_selection, invalid_name_prefix.
404: A named object does not exist
entity_not_found, entity_type_not_found, unknown_entity_type, attribute_not_found,
value_not_found, parent_not_found, def_not_found, referent_not_found.
409: The target's state or references refuse the operation
entity_already_exists, confirmation_required, already_withdrawn,
system_def_not_withdrawable, not_a_vocabulary_value, attribute_withdrawn, entity_retired,
referent_retired, name_already_in_use, rename_not_allowed, type_instances_managed,
type_has_live_instances, type_owns_attribute_defs, referenced_as_allowed_ref_type,
referenced_as_default_value.
The last four codes mean that something still points at the target being removed.
413: The request body is too large
request_too_large. The body exceeded 8 MB and was refused before it was read, so nothing
about its contents was checked. Send fewer entities or fewer values per request. A bulk
request that stays inside the per-value limits above can still exceed this ceiling, because
those limits apply per value and this one applies to the whole body.