Skip to content
Settings

crud — entity lifecycle API

cyoda-go version 0.6.2

crud — entity create, read, update, delete, and transition REST API.

POST /api/entity/{format}/{entityName}/{modelVersion}
POST /api/entity/{format}
GET /api/entity/{entityId}
PUT /api/entity/{format}/{entityId}
PUT /api/entity/{format}/{entityId}/{transition}
PUT /api/entity/{format}
DELETE /api/entity/{entityId}
DELETE /api/entity/{entityName}/{modelVersion}
GET /api/entity/{entityName}/{modelVersion}
GET /api/entity/{entityId}/changes
GET /api/entity/{entityId}/transitions
GET /api/entity/stats
GET /api/entity/stats/states
GET /api/entity/stats/{entityName}/{modelVersion}
GET /api/entity/stats/states/{entityName}/{modelVersion}
GET /api/platform-api/entity/fetch/transitions

Context path prefix is CYODA_CONTEXT_PATH (default /api). All endpoints require Authorization: Bearer <token> except when CYODA_IAM_MODE=mock.

Entities are instances of models. Each entity has a UUID, a model reference (entityName, modelVersion), and a lifecycle state managed by the workflow engine. Creating an entity requires the referenced model to be in LOCKED state. All write operations run within a Cyoda transaction and return a transactionId alongside the affected entity IDs.

Body size limit on all write endpoints: 10 MiB.

POST /api/entity/{format}/{entityName}/{modelVersion} — Create a single entity

  • format (path): JSON or XML
  • entityName (path): string — model name
  • modelVersion (path): int32
  • waitForConsistencyAfter (query, optional): boolean, default false
  • transactionTimeoutMillis (query, optional): int64, default 10000

If the request body is a JSON array, the handler delegates to the collection-create path: each element is treated as a separate entity of the same model.

Response: 200 OK, application/json:

[{
"transactionId": "cb91fa80-d4a8-11ee-a357-ae468cd3ed16",
"entityIds": ["74807f00-ed0d-11ee-a357-ae468cd3ed16"]
}]

POST /api/entity/{format} — Create a collection (mixed models)

  • format (path): JSON or XML

IMPORTANT — payload is a JSON-encoded string, not an object.

The payload field must be a string containing the JSON-encoded entity body, not a nested JSON object. This is a deliberate API contract — it preserves the payload as an opaque blob through the pipeline.

Correct: "payload": "{\"category\":\"physics\"}" Wrong: "payload": {"category":"physics"} (will be rejected with errors.BAD_REQUEST)

Request body: JSON array of CreatePayload objects:

[
{
"model": { "name": "nobel-prize", "version": 1 },
"payload": "{\"category\":\"physics\",\"year\":\"2024\"}"
}
]

Each item may reference a different model. All items are created in a single transaction.

Response: 200 OK, application/json:

[{
"transactionId": "cb91fa80-d4a8-11ee-a357-ae468cd3ed16",
"entityIds": [
"74807f00-ed0d-11ee-a357-ae468cd3ed16",
"72428380-0704-11ef-a357-ae468cd3ed16"
]
}]

GET /api/entity/{entityId} — Read a single entity by UUID

  • entityId (path): UUID string
  • pointInTime (query, optional): RFC 3339 date-time — load entity state at this instant
  • transactionId (query, optional): UUID — load entity state as of the end of this transaction

pointInTime and transactionId are mutually exclusive; supplying both returns 400 BAD_REQUEST.

Response: 200 OK, application/json:

{
"type": "ENTITY",
"data": { "category": "physics", "year": "2024" },
"meta": {
"id": "74807f00-ed0d-11ee-a357-ae468cd3ed16",
"state": "NEW",
"creationDate": "2025-08-01T10:00:00Z",
"lastUpdateTime": "2025-08-01T10:00:00Z",
"transactionId": "cb91fa80-d4a8-11ee-a357-ae468cd3ed16",
"transitionForLatestSave": "loopback"
}
}

PUT /api/entity/{format}/{entityId} — Update a single entity (loopback transition)

  • format (path): JSON or XML
  • entityId (path): UUID
  • If-Match (header, optional): transaction ID of last read — optimistic concurrency; if the entity was modified since, returns 412 Precondition Failed
  • transactionTimeoutMillis (query, optional): int64, default 10000
  • waitForConsistencyAfter (query, optional): boolean, default false

Request body: updated entity JSON/XML payload.

Response: 200 OK, application/json:

{
"transactionId": "733e7180-c055-11ef-a357-ae468cd3ed16",
"entityIds": ["cdcff600-bab1-11ee-a357-ae468cd3ed16"]
}

PUT /api/entity/{format}/{entityId}/{transition} — Update a single entity with a named transition

  • format (path): JSON or XML
  • entityId (path): UUID
  • transition (path): string — transition name defined in the model’s workflow
  • If-Match (header, optional): transaction ID
  • transactionTimeoutMillis (query, optional): int64, default 10000
  • waitForConsistencyAfter (query, optional): boolean, default false

Response: 200 OK, same shape as loopback update.

PUT /api/entity/{format} — Update a collection (mixed entities)

  • format (path): JSON (only supported format today; single-item PUT endpoints still accept XML)
  • transactionWindow (query, optional): int32, default 100, max 1000 — max items accepted in one batch; batches over the window are rejected with 400 BAD_REQUEST
  • transactionTimeoutMillis (query, optional): int64, default 10000
  • waitForConsistencyAfter (query, optional): boolean, default false

IMPORTANT — payload is a JSON-encoded string, not an object.

The payload field in each update item must be a string containing the JSON-encoded entity body, not a nested JSON object (same contract as collection create).

Correct: "payload": "{\"category\":\"physics\"}" Wrong: "payload": {"category":"physics"} (will be rejected with errors.BAD_REQUEST)

Request body: JSON array of update items:

[
{
"id": "8824c480-c166-11ee-9e63-ae468cd3ed16",
"payload": "{\"category\":\"physics\",\"year\":\"2024\"}",
"transition": "UPDATE"
}
]

If any entity in the collection is not found, the entire operation fails and no entities are updated (all-or-nothing).

Response: 200 OK, application/json, EntityTransactionResponse array (one element — the whole collection runs in a single transaction):

[
{
"transactionId": "733e7180-c055-11ef-a357-ae468cd3ed16",
"entityIds": ["8824c480-c166-11ee-9e63-ae468cd3ed16"]
}
]

DELETE /api/entity/{entityId} — Delete a single entity by UUID

  • entityId (path): UUID

Response: 200 OK, application/json:

{
"id": "a2242880-8d30-11ef-9e63-ae468cd3ed16",
"modelKey": {
"name": "nobel-prize",
"version": 4
},
"transactionId": "9fe62d00-a727-11ef-9e63-ae468cd3ed16"
}

DELETE /api/entity/{entityName}/{modelVersion} — Delete all entities for a model

  • entityName (path): string
  • modelVersion (path): int32

Response: 200 OK, application/json:

[{
"deleteResult": {
"idToError": {},
"numberOfEntitites": 42,
"numberOfEntititesRemoved": 42
},
"entityModelClassId": "31134900-d9cb-11ee-b913-ae468cd3ed16"
}]

GET /api/entity/{entityName}/{modelVersion} — List all entities for a model (paginated)

  • entityName (path): string
  • modelVersion (path): int32
  • pageSize (query, optional): int32, default 20
  • pageNumber (query, optional): int32, default 0

Response: 200 OK, application/json, array of entity envelopes (same shape as single-entity GET).

GET /api/entity/{entityId}/changes — Get entity change history metadata

  • entityId (path): UUID
  • pointInTime (query, optional): RFC 3339 — view history as it existed at this time

Response: 200 OK, application/json, array of change entries:

[
{
"changeType": "CREATED",
"timeOfChange": "2025-08-01T10:00:00Z",
"user": "admin",
"transactionId": "cb91fa80-d4a8-11ee-a357-ae468cd3ed16"
},
{
"changeType": "UPDATED",
"timeOfChange": "2025-08-02T09:00:00Z",
"user": "admin",
"transactionId": "733e7180-c055-11ef-a357-ae468cd3ed16"
}
]
  • changeType: CREATED, UPDATED, or DELETED
  • transactionId: present only when hasEntity is true (i.e., entity payload exists at that version)

GET /api/entity/{entityId}/transitions — List available transitions for an entity

  • entityId (path): UUID
  • pointInTime (query, optional): RFC 3339
  • transactionId (query, optional): UUID — derive point-in-time from transaction submit time

pointInTime and transactionId are mutually exclusive; supplying both returns 400 BAD_REQUEST. When neither is provided, the current time is used.

Response: 200 OK, application/json, array of available transition names (as returned by the workflow engine).

GET /api/platform-api/entity/fetch/transitions — List available transitions (platform-api format)

  • entityClass (query, required): string in Name.Version format, e.g., Offer.1
  • entityId (query, required): UUID string

Response: 200 OK, application/json, array of available transition names.

GET /api/entity/stats — Entity count statistics across all models

Response: 200 OK, application/json:

[
{ "modelName": "nobel-prize", "modelVersion": 1, "count": 42 },
{ "modelName": "family-member", "modelVersion": 3, "count": 7 }
]

GET /api/entity/stats/states — Entity count by state across all models

  • states (query, optional): comma-separated list of state names to filter by; maximum 1000 entries

Response: 200 OK, application/json:

[
{ "modelName": "nobel-prize", "modelVersion": 1, "state": "NEW", "count": 10 },
{ "modelName": "nobel-prize", "modelVersion": 1, "state": "APPROVED", "count": 32 }
]

GET /api/entity/stats/{entityName}/{modelVersion} — Entity count for a specific model

  • entityName (path): string
  • modelVersion (path): int32

Response: 200 OK, application/json, single ModelStatsDto.

GET /api/entity/stats/states/{entityName}/{modelVersion} — Entity count by state for a specific model

  • entityName (path): string
  • modelVersion (path): int32
  • states (query, optional): list of state names to filter by; maximum 1000 entries

Response: 200 OK, application/json, array of ModelStateStatsDto.

All entity read operations return entities in the standard envelope:

{
"type": "ENTITY",
"data": { ... },
"meta": {
"id": "74807f00-ed0d-11ee-a357-ae468cd3ed16",
"modelKey": { "name": "nobel-prize", "version": 1 },
"state": "NEW",
"creationDate": "2025-08-01T10:00:00.000000000Z",
"lastUpdateTime": "2025-08-01T10:00:00.000000000Z",
"transactionId": "cb91fa80-d4a8-11ee-a357-ae468cd3ed16",
"transitionForLatestSave": "UPDATE"
}
}
  • type — always "ENTITY"
  • data — the entity’s JSON payload (decoded with json.Number for numeric precision)
  • meta.id — UUID string
  • meta.modelKey — object with name (string) and version (int32) identifying the model; present in single-entity GET /entity/{id} responses. Omitted from list/search results because the model is already part of the request path (/api/entity/{entityName}/{modelVersion}).
  • meta.state — current workflow state string
  • meta.creationDate — RFC 3339 with nanoseconds
  • meta.lastUpdateTime — RFC 3339 with nanoseconds
  • meta.transactionId — present when a transaction ID exists
  • meta.transitionForLatestSave — transition name that produced the latest save. Valid values: "loopback" (loopback update with no transition supplied by the client) or the named transition string. Known bug (#94): the server currently stores the literal "workflow" for engine-driven initial-state writes; there is no valid "workflow" value and this is tracked for fix.

The If-Match header on update endpoints accepts a transaction ID. If the entity’s meta.transactionId does not match the value in If-Match, the server returns 412 Precondition Failed. This provides optimistic concurrency without distributed locking.

To use: read the entity (GET /entity/{id}), note meta.transactionId, include it in If-Match on the subsequent update.

  • errors.ENTITY_NOT_FOUND404 — entity UUID does not exist
  • errors.MODEL_NOT_FOUND404 — model referenced during create does not exist
  • errors.MODEL_NOT_LOCKED409 — model exists but is not in LOCKED state; entities cannot be created until the model is locked
  • errors.VALIDATION_FAILED400 — payload fails schema validation against the model
  • errors.CONFLICT409 — transaction conflict (retryable)
  • errors.IDEMPOTENCY_CONFLICT409 — reserved; not yet implemented (#91). Future contract: returned on collection create/update when the Idempotency-Key header is re-used with a different payload body
  • errors.TRANSITION_NOT_FOUND404 — named transition does not exist in the workflow
  • errors.BAD_REQUEST400 — malformed request, invalid UUID, conflicting query parameters, states filter exceeds 1000 entries

Create a single entity:

curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"category":"physics","year":"2024"}' \
"http://localhost:8080/api/entity/JSON/nobel-prize/1"

Read an entity:

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16"

Read an entity at a point in time:

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16?pointInTime=2025-08-01T10:00:00Z"

Update an entity with loopback transition:

curl -s -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: cb91fa80-d4a8-11ee-a357-ae468cd3ed16" \
-d '{"category":"chemistry","year":"2024"}' \
"http://localhost:8080/api/entity/JSON/74807f00-ed0d-11ee-a357-ae468cd3ed16"

Update an entity with a named transition:

curl -s -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"category":"chemistry","year":"2024"}' \
"http://localhost:8080/api/entity/JSON/74807f00-ed0d-11ee-a357-ae468cd3ed16/APPROVE"

Delete a single entity:

curl -s -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16"

Delete all entities for a model:

curl -s -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/nobel-prize/1"

List all entities for a model (page 0, size 20):

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/nobel-prize/1?pageSize=20&pageNumber=0"

Get entity change history:

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16/changes"

Get available transitions:

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16/transitions"

Create a multi-model collection:

curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[{"model":{"name":"nobel-prize","version":1},"payload":"{\"category\":\"physics\",\"year\":\"2024\"}"}]' \
"http://localhost:8080/api/entity/JSON"

Get statistics by state for a model:

curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/api/entity/stats/states/nobel-prize/1"
  • models
  • search
  • workflows
  • errors.ENTITY_NOT_FOUND
  • errors.MODEL_NOT_FOUND
  • errors.MODEL_NOT_LOCKED
  • errors.VALIDATION_FAILED
  • errors.CONFLICT
  • errors.TRANSITION_NOT_FOUND
  • openapi
  • cyoda help models — A model is a named, versioned schema registered per tenant. Every entity in the system is an instance of exactly one model. Models are identified by (entityName, modelVersion). The model ID is a deterministic UUID v5 derived from that key: UUID.newSHA1(NameSpaceURL, "{entityName}.{modelVersion}").
  • cyoda help search — Search operates against a specific entity model (entityName, modelVersion). Two modes are supported:
  • cyoda help workflows — A workflow definition is a named finite state machine attached to an entity model. Workflows are stored per model reference (entityName, modelVersion). A model may have multiple workflow definitions; the engine selects the matching one per entity using the workflow-level criterion field evaluated at entity creation time. When no criterion matches, the engine uses the default built-in workflow.
  • cyoda help errors ENTITY_NOT_FOUND — No entity with the given ID exists in the tenant’s data store, or the entity existed at a point-in-time that precedes the requested snapshot. Also returned for audit log lookups when the specified event or message cannot be found.
  • cyoda help errors MODEL_NOT_FOUND — The entity type or model name specified in the request does not exist in the tenant’s model registry. Occurs when creating entities with an unknown type, importing data that references a missing model, or performing model lifecycle transitions on a model ID that does not exist.
  • cyoda help errors MODEL_NOT_LOCKED — Entity creation and bulk write operations require the model to be in the LOCKED lifecycle state. Models in DRAFT or unlocked-for-editing state reject writes to prevent schema changes from affecting in-flight data.
  • cyoda help errors VALIDATION_FAILED — Unlike BAD_REQUEST (which covers parse failures), this error is returned when the payload is parseable but violates the registered model schema — for example, a required field is missing, a value is out of the allowed range, or a workflow guard condition is not satisfied. The error detail includes the specific validation failure.
  • cyoda help errors CONFLICT — The server detected that the entity was modified by another writer between the time it was read and the time the current write was committed. Normal outcome under concurrent load.
  • cyoda help errors IDEMPOTENCY_CONFLICT — The idempotency key is supplied via the Idempotency-Key HTTP header on collection create and update requests. See crud for the request shape.
  • cyoda help errors TRANSITION_NOT_FOUND — Entity workflow state machines define explicit transitions between states. This error fires when a transition is triggered that does not exist in the model’s workflow definition for the entity’s current state. Also occurs when the transition name is misspelled or when the entity is in a terminal state that allows no further transitions.
  • cyoda help openapi — cyoda-go generates its OpenAPI 3.1 specification from the embedded api/openapi.yaml file compiled into the binary at build time. The spec is served at /openapi.json with runtime-patched server URLs. The Scalar API Reference UI is served at /docs and loads the spec from /openapi.json.