Skip to main content
Version: 2026-05

REST API Reference

All endpoints are prefixed with /api/v1/. Every response body is JSON; every request body must be JSON where applicable.

Authentication

When the server is configured with an OIDC provider, every request (except GET /version and OPTIONS) must carry a valid JWT:

Authorization: Bearer <token>

Missing or invalid tokens return 401 Unauthorized. Without auth configuration the server runs in admin mode and accepts all requests without a token.

Content-Type and CORS

All responses carry Content-Type: application/json. CORS headers are set on every response:

Access-Control-Allow-Origin: <configured origin, default *>
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

Addressing Entities

Any single entity can be addressed in four equivalent ways:

PatternResolves by
/api/v1/by-id/<uuid>Exact entity version UUID -- pins a specific immutable snapshot
/api/v1/by-root/<uuid>Root UUID -- always resolves to the latest version
/api/v1/by-key/<type_name>/<key>Type-scoped key -- always resolves to the latest version
/api/v1/by-type/<type_name>/<key>Alias for by-key

All four prefixes accept the same sub-paths described in this document.

See The Meta Model for the conceptual explanation of entity addressing.

Depth Parameter

Every GET that returns entity data accepts ?depth=<n> (integer, default 1).

  • depth=1 -- relationships are returned as arrays of UUID strings.
  • depth>=2 -- each relationship is expanded to a full entity object (recursively up to the requested depth).
warning

Use depth deliberately. Large depth values on entities with many relationships can return very large response payloads.


Entity Wire Format

Response Shape

{
"id": "<uuid>",
"type_id": "<uuid>",
"root_id": "<uuid>",
"@status": { "name": "new", "number": 0 },
"@key": "potato",
"@self": "vegetable/potato",
"@label": "Potato",
"source": "<uuid>",
"target": "<uuid>",
"metadata": {
"previous": "<uuid>",
"created_by": "user-name",
"created_on": "2024-01-01T00:00:00Z",
"package_name": "my.package"
},
"properties": { "weight": 5, "name": { "en_us": "Potato" } },
"relationships": { "has_tag": ["<rel-uuid>", "..."] },
"lists": { "items": ["..."] },
"traits": {
"is_semantically_tagged": {
"type_id": "<uuid>",
"properties": { "semantic_tag": "http://schema.org/Potato" },
"relationships": {}
}
}
}

Notes:

  • @status is absent if the entity has no lifecycle map assigned.
  • source / target are only present for relationship entities.
  • relationships values are UUID strings at depth=1; full entity objects at depth>=2.
  • Null/missing optional fields are omitted from the output.

Create / Update Input Format

Used as the request body for entity creation (POST) and update (PUT):

{
"key": "potato",
"label": "Potato",
"package_name": "my.package",
"lc_status": 0,
"properties": {
"weight": 5,
"flag": true,
"name": { "en_us": "Potato", "de_de": "Kartoffel" },
"content": { "html": "<p>...</p>" }
},
"relationships": {
"has_tag": [
{
"target": "tag/organic",
"properties": { "relevance": 0.9 }
}
]
},
"traits": {
"is_semantically_tagged": {
"properties": { "semantic_tag": "http://schema.org/Potato" }
}
},
"lists": {
"ingredients": ["..."]
}
}

The key field is optional. See Key Resolution for the precedence rules.

All fields are optional during an update -- a new immutable version is created carrying over any fields not explicitly provided.

Relationship target values use the type_name/key wire format (e.g. "tag/organic") or a UUID string.


System / Meta Routes

GET /version

Returns the server build SHA. Does not require authentication.

Response 200:

{ "version": "abc1234..." }

OPTIONS *

Returns 204 No Content for all CORS preflight requests.


Collection Routes

GET /api/v1/by-type/<type_name>

Returns all entities of the given type as a JSON array.

Query params: ?depth=<n>

Response 200: Array of entity objects.

Response 404: { "error": "EntityType not found" }


POST /api/v1/by-type/<type_name>

Creates one or more entities of the given type.

Request body: Either a single input object, or a JSON array of objects for bulk creation.

When an array is supplied, all entities are created atomically -- any validation failure rolls the entire batch back.

Response 200 (single): The created entity object.

Response 200 (array): A JSON array of the created entity objects, in request order.

Response 400 (single): { "error": "<validation message>" }

Response 400 (array): { "error": "<validation message>", "index": N } -- zero-based index of the failing element. The entire batch is rolled back.

Response 404: { "error": "EntityType not found" }


GET /api/v1/by-trait/<trait_name>

Returns all entities that have the named trait attached.

Query params: ?depth=<n>

Response 200: Array of entity objects.


Entity Operations

The following sub-paths apply to any entity addressed by one of the four lookup prefixes.

GET /

Returns the entity.

Query params: ?depth=<n>

Response 200: Entity object.

Response 404: { "error": "Entity not found" }


PUT /

Updates the entity. Creates a new immutable version; the old version remains in history.

Request body: Create/Update input format -- only supply fields to change.

Response 200: Updated entity object.

Response 400: { "error": "Update failed: <reason>" }


DELETE /?purge=true

Permanently deletes the entire entity version chain (all versions sharing the same root_id). The ?purge=true query param is required.

Response 200: Empty body.

Response 409: Entity could not be purged.


GET /@status

Returns the current lifecycle status entity for this entity.

Response 200: Full entity object for the lc_status entity.

Response 404: { "error": "No LifeCycle available for entity!" }


PUT /@status / POST /@status

Transitions the entity to a new lifecycle status. Creates a new entity version.

Request body: Status name string or status number:

"review"

or

100

Response 200: Updated entity object.

Response 400: { "error": "<reason>" } -- invalid status or illegal transition.


GET /@incoming

Returns all relationship entities whose target points to this entity.

Response 200: Array of relationship entity objects.


GET /@outgoing

Returns a map of all outgoing relationships keyed by relationship type UUID.

Query params: ?depth=<n>

Response 200:

{
"<rel-type-uuid>": ["<rel-uuid>", "..."]
}

GET /@referenced-by

Returns all entities (of any type) that hold a relationship pointing at this entity.

Response 200: Array of entity UUIDs.


GET /@history

Returns the complete version chain for this entity (newest first).

Response 200: Array of entity objects.


GET /@type[/...]

Returns the EntityType entity that describes this entity's type. All entity sub-paths can be appended, allowing recursive traversal:

GET /api/v1/by-id/<uuid>/@type
GET /api/v1/by-id/<uuid>/@type/properties/name

GET /@previous[/...]

Navigates to the immediately preceding entity version. All entity sub-paths can be appended.

Response 404: No previous version exists.


GET /@source[/...]

For relationship entities: navigates to the source entity. Sub-paths can be appended.


GET /@target[/...]

For relationship entities: navigates to the target entity. Sub-paths can be appended.


POST /@action/<action_name>

Executes a named action declared on the entity's type via has_action.

Response 200:

{ "result": "<action output string>" }

Response 409: Action execution failed.


Property Routes

GET /properties

Returns all properties of the entity as a flat map.

Response 200:

{
"weight": 5,
"name": { "en_us": "Potato" }
}

GET /properties/<prop_name>

Returns the value of a single property.

Response 200: Property value (bare JSON -- string, number, object, etc.)

Response 404: Property does not exist.


GET /properties/<prop_name>/@download

For BLOB-type properties: redirects the client to a presigned S3 download URL (valid for 5 minutes).

Response 302: Location header contains the presigned URL. The client should follow the redirect to fetch the binary data directly from the blob store.

Response 404: Property does not exist or the BLOB reference is empty.

Response 400: Property is not a BLOB type.


PUT /properties/<prop_name>/@upload

For BLOB-type properties: generates a presigned S3 upload URL for a new blob object. The blob is not yet associated with the entity -- the caller must complete the three-step workflow.

Response 200:

{
"blob_id": "<uuid>",
"upload_url": "<presigned S3 PUT URL>"
}

The presigned URL is valid for 5 minutes.

Full BLOB upload workflow:

  1. PUT /properties/<prop>/@upload -- obtain a fresh blob_id and upload_url.
  2. PUT <upload_url> (directly to S3) -- send the file bytes. This request goes to the blob store, not to EP.Core.
  3. PUT / (update entity) -- set the property value to the blob_id UUID string received in step 1.

See BLOB Properties for the full conceptual documentation.


Relationship Routes

Relationship instances are themselves full entities (with their own UUID, properties, and metadata).

GET /relationships

Returns all relationships on this entity grouped by relationship type name.

Response 200:

{
"has_tag": ["<rel entity object>", "..."],
"has_version": ["<rel entity object>", "..."]
}

GET /relationships/<rel_name>

Returns all relationship instances of the given type.

Response 200: Array of relationship entity objects.

Response 404: { "error": "RelationshipType '<rel_name>' not found in EntityType '<type>'!" }


POST /relationships

Creates one or more relationships in bulk. The body is a map of relationship type names to arrays of targets.

Request body:

{
"has_tag": [
{ "target": "tag/organic", "properties": { "relevance": 0.9 } }
],
"has_version": [
{ "target": "version/1.0" }
]
}

Response 200: Updated entity object (with new relationships included).

Response 400: { "error": "<reason>" }


POST /relationships/<rel_name>

Creates one or more relationship instances of a specific type. Body may be a single object or an array.

Request body (single):

{
"target": "tag/organic",
"properties": { "relevance": 0.9 }
}

Request body (multiple):

[
{ "target": "tag/organic" },
{ "target": "tag/seasonal" }
]

Response 200: Updated entity object.

Response 400: { "error": "<reason>" }

Response 404: Relationship type not declared on this entity type.


GET /relationships/<rel_name>/<rel_root_id>[/...]

Returns a single relationship instance identified by its root UUID. All entity sub-paths can be appended, allowing traversal through the relationship entity:

GET /relationships/has_tag/<rel-root-uuid>
GET /relationships/has_tag/<rel-root-uuid>/@target
GET /relationships/has_tag/<rel-root-uuid>/@target/properties/name

Response 404: Relationship not found.


PUT /relationships/<rel_name>/<rel_root_id>

Updates the properties of a single relationship instance.

Request body: Create/Update input format.

Response 200: Updated entity object.


DELETE /relationships/<rel_name>/<rel_root_id>

Removes the named relationship instance.

Response 200: Updated entity object (without the removed relationship).

Response 500: { "error": "Could not remove relationship!" }


Batch Upload

POST /api/v1/@upload

Creates multiple entities and relationships in a single atomic operation. All items are committed together or none at all -- if any entity or relationship fails validation, the entire batch is rolled back.

Processing is two-phased:

  1. Entities are created first (in array order).
  2. Relationships are created after all entities, so relationship sources and targets may freely reference entities created in the same batch.

Request body:

{
"entities": [
{
"type_name": "entity_type/recipe",
"key": "lasagna",
"properties": { "name": "Lasagna" }
},
{
"type_name": "entity_type/vegetable",
"key": "pea",
"properties": { "name": { "en_us": "Pea" }, "weight": 1 }
}
],
"relationships": [
{
"type_name": "has_ingredient",
"source": "recipe/lasagna",
"target": "vegetable/pea",
"properties": { "count": 2 }
}
]
}

Request fields:

FieldTypeDescription
entitiesarrayOrdered list of entity creation objects (same fields as the create/update format plus type_name)
entities[].type_namestringRequired. The entity type in entity_type/<name> format
entities[].keystringOptional. Entity key
entities[].propertiesobjectOptional. Property values
entities[].relationshipsobjectOptional. Inline relationships
entities[].traitsobjectOptional. Trait instances
relationshipsarrayOrdered list of relationship creation objects
relationships[].type_namestringRequired. Relationship type name (e.g. "has_ingredient")
relationships[].sourcestringRequired. Source entity in type_name/key format (may reference a batch entity)
relationships[].targetstringRequired. Target entity in type_name/key format (may reference a batch entity)
relationships[].propertiesobjectOptional. Edge property values

Response 200:

{
"entities": ["<entity object>", "..."],
"relationships": ["<entity object>", "..."]
}

Response 400:

{
"error": "<validation message>",
"phase": "entities",
"index": 1
}

phase is either "entities" or "relationships"; index is the zero-based position of the failing item within that array.

Response 405: Any method other than POST.


Blob Store Routes

Direct access to a named blob store by store_id and blob_id, without going through an entity property. Useful when the caller already knows the blob coordinates. The only currently registered store is "default".

GET /api/v1/blob_store/<store_id>/<blob_id>

Returns a presigned download URL for the given blob object.

Response 200:

{ "url": "<presigned S3 GET URL>" }

Response 404: Store not found.

PUT /api/v1/blob_store/<store_id>/<blob_id>

Returns a presigned upload URL for the given blob ID.

Response 200:

{ "url": "<presigned S3 PUT URL>" }

Response 404: Store not found.


Error Responses

All error responses follow the same shape:

{ "error": "<human-readable message>" }
StatusMeaning
400 Bad RequestInvalid input, validation failure, or illegal transition
401 UnauthorizedMissing or invalid JWT token
404 Not FoundEntity, type, property, or relationship not found
405 Method Not AllowedHTTP verb not supported on this route
409 ConflictOperation could not be completed (e.g. purge blocked, action failed)
500 Internal Server ErrorUnexpected server-side failure