Skip to main content

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 pathPurposeSuccess status
POST /entitiesCreate an entity within an existing entity type201 when created; 200 when the name already exists
PATCH /entities/{guid}/attributesSet or clear attribute values on an entity200
POST /entities/vocabularyCreate a vocabulary value, such as a tag, domain, or environment201 when created; 200 when the value already exists
PATCH /entities/{guid}/nameRename a user-created entity or vocabulary value200
DELETE /entities/vocabulary/{guid}Remove a vocabulary value from every entity and delete it202
DELETE /entities/attributes/{type_guid}/{name}Withdraw an attribute from an entity type202
POST /entities/curation-jobsApply attribute values across many entities202

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:

Operationjob_type
Delete a vocabulary valueentity_vocabulary_delete
Withdraw an attributeentity_attribute_withdraw
Curate many entitiesentity_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 pathPurposeSuccess status
POST /entities/filterFilter one entity type by attribute values200
POST /entities/facetsList values for one attribute or count named attribute/value pairs200
GET /entities?type={guid}Browse active entities of one type200
GET /entities/{guid}Read an active entity profile200
GET /entities/{guid}/childrenList direct active children200
GET /entities/resolve/{guid}Resolve a live guid or remembered rename200; 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:

LimitMaximum
Filter terms50
Values in one filter term50
Named facet pairs50
Bulk entities25,000
Attributes in one bulk request10
Text value length1,000 characters
JSON value size16 KB
Number precision38 significant digits
Request body size8 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.