﻿# crud — entity lifecycle API

Entities are instances of models. Each entity has a UUID, a model reference (`entityName`, `modelVersion`), and a lifecycle state managed by the workflow …

<em>cyoda-go version <a href="https://github.com/Cyoda-platform/cyoda-go/releases/tag/v0.6.2">0.6.2</a></em>

# crud

## NAME

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

## SYNOPSIS

```
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`.

## DESCRIPTION

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.

## ENDPOINTS

**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`:

```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:

```json
[
  {
    "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`:

```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`:

```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`:

```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:

```json
[
  {
    "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):

```json
[
  {
    "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`:

```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`:

```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:

```json
[
  {
    "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`:

```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`:

```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`.

## ENTITY ENVELOPE

All entity read operations return entities in the standard envelope:

```json
{
  "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.

## OPTIMISTIC CONCURRENCY

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

- `errors.ENTITY_NOT_FOUND` — `404` — entity UUID does not exist
- `errors.MODEL_NOT_FOUND` — `404` — model referenced during create does not exist
- `errors.MODEL_NOT_LOCKED` — `409` — model exists but is not in `LOCKED` state; entities cannot be created until the model is locked
- `errors.VALIDATION_FAILED` — `400` — payload fails schema validation against the model
- `errors.CONFLICT` — `409` — transaction conflict (retryable)
- `errors.IDEMPOTENCY_CONFLICT` — `409` — 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_FOUND` — `404` — named transition does not exist in the workflow
- `errors.BAD_REQUEST` — `400` — malformed request, invalid UUID, conflicting query parameters, states filter exceeds 1000 entries

## EXAMPLES

**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"
```

## SEE ALSO

- 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

## See also

- [`cyoda help models`](/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`](/help/search/) — Search operates against a specific entity model `(entityName, modelVersion)`. Two modes are supported:
- [`cyoda help workflows`](/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`](/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`](/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`](/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`](/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`](/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`](/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`](/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`](/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`.

## Raw formats

- [`/help/crud.json`](/help/crud.json) — full descriptor (matches `GET /help/{topic}` envelope)
- [`/help/crud.md`](/help/crud.md) — body only