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.
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, and classification 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/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. Missing and retired entities are excluded, with no request option to include them. 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.
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 |
| 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
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.
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_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.