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:
| Pattern | Resolves 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).
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:
@statusis absent if the entity has no lifecycle map assigned.source/targetare only present for relationship entities.relationshipsvalues are UUID strings atdepth=1; full entity objects atdepth>=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:
PUT /properties/<prop>/@upload-- obtain a freshblob_idandupload_url.PUT <upload_url>(directly to S3) -- send the file bytes. This request goes to the blob store, not to EP.Core.PUT /(update entity) -- set the property value to theblob_idUUID 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:
- Entities are created first (in array order).
- 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:
| Field | Type | Description |
|---|---|---|
entities | array | Ordered list of entity creation objects (same fields as the create/update format plus type_name) |
entities[].type_name | string | Required. The entity type in entity_type/<name> format |
entities[].key | string | Optional. Entity key |
entities[].properties | object | Optional. Property values |
entities[].relationships | object | Optional. Inline relationships |
entities[].traits | object | Optional. Trait instances |
relationships | array | Ordered list of relationship creation objects |
relationships[].type_name | string | Required. Relationship type name (e.g. "has_ingredient") |
relationships[].source | string | Required. Source entity in type_name/key format (may reference a batch entity) |
relationships[].target | string | Required. Target entity in type_name/key format (may reference a batch entity) |
relationships[].properties | object | Optional. 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>" }
| Status | Meaning |
|---|---|
400 Bad Request | Invalid input, validation failure, or illegal transition |
401 Unauthorized | Missing or invalid JWT token |
404 Not Found | Entity, type, property, or relationship not found |
405 Method Not Allowed | HTTP verb not supported on this route |
409 Conflict | Operation could not be completed (e.g. purge blocked, action failed) |
500 Internal Server Error | Unexpected server-side failure |