{
  "topic": "crud",
  "path": [
    "crud"
  ],
  "title": "crud — entity lifecycle API",
  "synopsis": "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": "# crud\n\n## NAME\n\ncrud — entity create, read, update, delete, and transition REST API.\n\n## SYNOPSIS\n\n```\nPOST   /api/entity/{format}/{entityName}/{modelVersion}\nPOST   /api/entity/{format}\nGET    /api/entity/{entityId}\nPUT    /api/entity/{format}/{entityId}\nPUT    /api/entity/{format}/{entityId}/{transition}\nPUT    /api/entity/{format}\nDELETE /api/entity/{entityId}\nDELETE /api/entity/{entityName}/{modelVersion}\nGET    /api/entity/{entityName}/{modelVersion}\nGET    /api/entity/{entityId}/changes\nGET    /api/entity/{entityId}/transitions\nGET    /api/entity/stats\nGET    /api/entity/stats/states\nGET    /api/entity/stats/{entityName}/{modelVersion}\nGET    /api/entity/stats/states/{entityName}/{modelVersion}\nGET    /api/platform-api/entity/fetch/transitions\n```\n\nContext path prefix is `CYODA_CONTEXT_PATH` (default `/api`). All endpoints require `Authorization: Bearer <token>` except when `CYODA_IAM_MODE=mock`.\n\n## DESCRIPTION\n\nEntities 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.\n\nBody size limit on all write endpoints: 10 MiB.\n\n## ENDPOINTS\n\n**POST /api/entity/{format}/{entityName}/{modelVersion}** — Create a single entity\n\n- `format` (path): `JSON` or `XML`\n- `entityName` (path): string — model name\n- `modelVersion` (path): int32\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n\nIf 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.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[{\n  \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n  \"entityIds\": [\"74807f00-ed0d-11ee-a357-ae468cd3ed16\"]\n}]\n```\n\n**POST /api/entity/{format}** — Create a collection (mixed models)\n\n- `format` (path): `JSON` or `XML`\n\n**IMPORTANT — `payload` is a JSON-encoded string, not an object.**\n\nThe `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.\n\nCorrect: `\"payload\": \"{\\\"category\\\":\\\"physics\\\"}\"`\nWrong:   `\"payload\": {\"category\":\"physics\"}`   (will be rejected with `errors.BAD_REQUEST`)\n\nRequest body: JSON array of `CreatePayload` objects:\n\n```json\n[\n  {\n    \"model\": { \"name\": \"nobel-prize\", \"version\": 1 },\n    \"payload\": \"{\\\"category\\\":\\\"physics\\\",\\\"year\\\":\\\"2024\\\"}\"\n  }\n]\n```\n\nEach item may reference a different model. All items are created in a single transaction.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[{\n  \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n  \"entityIds\": [\n    \"74807f00-ed0d-11ee-a357-ae468cd3ed16\",\n    \"72428380-0704-11ef-a357-ae468cd3ed16\"\n  ]\n}]\n```\n\n**GET /api/entity/{entityId}** — Read a single entity by UUID\n\n- `entityId` (path): UUID string\n- `pointInTime` (query, optional): RFC 3339 date-time — load entity state at this instant\n- `transactionId` (query, optional): UUID — load entity state as of the end of this transaction\n\n`pointInTime` and `transactionId` are mutually exclusive; supplying both returns `400 BAD_REQUEST`.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n{\n  \"type\": \"ENTITY\",\n  \"data\": { \"category\": \"physics\", \"year\": \"2024\" },\n  \"meta\": {\n    \"id\": \"74807f00-ed0d-11ee-a357-ae468cd3ed16\",\n    \"state\": \"NEW\",\n    \"creationDate\": \"2025-08-01T10:00:00Z\",\n    \"lastUpdateTime\": \"2025-08-01T10:00:00Z\",\n    \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n    \"transitionForLatestSave\": \"loopback\"\n  }\n}\n```\n\n**PUT /api/entity/{format}/{entityId}** — Update a single entity (loopback transition)\n\n- `format` (path): `JSON` or `XML`\n- `entityId` (path): UUID\n- `If-Match` (header, optional): transaction ID of last read — optimistic concurrency; if the entity was modified since, returns `412 Precondition Failed`\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n\nRequest body: updated entity JSON/XML payload.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n{\n  \"transactionId\": \"733e7180-c055-11ef-a357-ae468cd3ed16\",\n  \"entityIds\": [\"cdcff600-bab1-11ee-a357-ae468cd3ed16\"]\n}\n```\n\n**PUT /api/entity/{format}/{entityId}/{transition}** — Update a single entity with a named transition\n\n- `format` (path): `JSON` or `XML`\n- `entityId` (path): UUID\n- `transition` (path): string — transition name defined in the model's workflow\n- `If-Match` (header, optional): transaction ID\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n\nResponse: `200 OK`, same shape as loopback update.\n\n**PUT /api/entity/{format}** — Update a collection (mixed entities)\n\n- `format` (path): `JSON` (only supported format today; single-item PUT endpoints still accept XML)\n- `transactionWindow` (query, optional): int32, default `100`, max `1000` — max items accepted in one batch; batches over the window are rejected with `400 BAD_REQUEST`\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n\n**IMPORTANT — `payload` is a JSON-encoded string, not an object.**\n\nThe `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).\n\nCorrect: `\"payload\": \"{\\\"category\\\":\\\"physics\\\"}\"`\nWrong:   `\"payload\": {\"category\":\"physics\"}`   (will be rejected with `errors.BAD_REQUEST`)\n\nRequest body: JSON array of update items:\n\n```json\n[\n  {\n    \"id\": \"8824c480-c166-11ee-9e63-ae468cd3ed16\",\n    \"payload\": \"{\\\"category\\\":\\\"physics\\\",\\\"year\\\":\\\"2024\\\"}\",\n    \"transition\": \"UPDATE\"\n  }\n]\n```\n\nIf any entity in the collection is not found, the entire operation fails and no entities are updated (all-or-nothing).\n\nResponse: `200 OK`, `application/json`, `EntityTransactionResponse` array (one element — the whole collection runs in a single transaction):\n\n```json\n[\n  {\n    \"transactionId\": \"733e7180-c055-11ef-a357-ae468cd3ed16\",\n    \"entityIds\": [\"8824c480-c166-11ee-9e63-ae468cd3ed16\"]\n  }\n]\n```\n\n**DELETE /api/entity/{entityId}** — Delete a single entity by UUID\n\n- `entityId` (path): UUID\n\nResponse: `200 OK`, `application/json`:\n\n```json\n{\n  \"id\": \"a2242880-8d30-11ef-9e63-ae468cd3ed16\",\n  \"modelKey\": {\n    \"name\": \"nobel-prize\",\n    \"version\": 4\n  },\n  \"transactionId\": \"9fe62d00-a727-11ef-9e63-ae468cd3ed16\"\n}\n```\n\n**DELETE /api/entity/{entityName}/{modelVersion}** — Delete all entities for a model\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[{\n  \"deleteResult\": {\n    \"idToError\": {},\n    \"numberOfEntitites\": 42,\n    \"numberOfEntititesRemoved\": 42\n  },\n  \"entityModelClassId\": \"31134900-d9cb-11ee-b913-ae468cd3ed16\"\n}]\n```\n\n**GET /api/entity/{entityName}/{modelVersion}** — List all entities for a model (paginated)\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n- `pageSize` (query, optional): int32, default `20`\n- `pageNumber` (query, optional): int32, default `0`\n\nResponse: `200 OK`, `application/json`, array of entity envelopes (same shape as single-entity GET).\n\n**GET /api/entity/{entityId}/changes** — Get entity change history metadata\n\n- `entityId` (path): UUID\n- `pointInTime` (query, optional): RFC 3339 — view history as it existed at this time\n\nResponse: `200 OK`, `application/json`, array of change entries:\n\n```json\n[\n  {\n    \"changeType\": \"CREATED\",\n    \"timeOfChange\": \"2025-08-01T10:00:00Z\",\n    \"user\": \"admin\",\n    \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\"\n  },\n  {\n    \"changeType\": \"UPDATED\",\n    \"timeOfChange\": \"2025-08-02T09:00:00Z\",\n    \"user\": \"admin\",\n    \"transactionId\": \"733e7180-c055-11ef-a357-ae468cd3ed16\"\n  }\n]\n```\n\n- `changeType`: `CREATED`, `UPDATED`, or `DELETED`\n- `transactionId`: present only when `hasEntity` is true (i.e., entity payload exists at that version)\n\n**GET /api/entity/{entityId}/transitions** — List available transitions for an entity\n\n- `entityId` (path): UUID\n- `pointInTime` (query, optional): RFC 3339\n- `transactionId` (query, optional): UUID — derive point-in-time from transaction submit time\n\n`pointInTime` and `transactionId` are mutually exclusive; supplying both returns `400 BAD_REQUEST`. When neither is provided, the current time is used.\n\nResponse: `200 OK`, `application/json`, array of available transition names (as returned by the workflow engine).\n\n**GET /api/platform-api/entity/fetch/transitions** — List available transitions (platform-api format)\n\n- `entityClass` (query, required): string in `Name.Version` format, e.g., `Offer.1`\n- `entityId` (query, required): UUID string\n\nResponse: `200 OK`, `application/json`, array of available transition names.\n\n**GET /api/entity/stats** — Entity count statistics across all models\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[\n  { \"modelName\": \"nobel-prize\", \"modelVersion\": 1, \"count\": 42 },\n  { \"modelName\": \"family-member\", \"modelVersion\": 3, \"count\": 7 }\n]\n```\n\n**GET /api/entity/stats/states** — Entity count by state across all models\n\n- `states` (query, optional): comma-separated list of state names to filter by; maximum 1000 entries\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[\n  { \"modelName\": \"nobel-prize\", \"modelVersion\": 1, \"state\": \"NEW\", \"count\": 10 },\n  { \"modelName\": \"nobel-prize\", \"modelVersion\": 1, \"state\": \"APPROVED\", \"count\": 32 }\n]\n```\n\n**GET /api/entity/stats/{entityName}/{modelVersion}** — Entity count for a specific model\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n\nResponse: `200 OK`, `application/json`, single `ModelStatsDto`.\n\n**GET /api/entity/stats/states/{entityName}/{modelVersion}** — Entity count by state for a specific model\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n- `states` (query, optional): list of state names to filter by; maximum 1000 entries\n\nResponse: `200 OK`, `application/json`, array of `ModelStateStatsDto`.\n\n## ENTITY ENVELOPE\n\nAll entity read operations return entities in the standard envelope:\n\n```json\n{\n  \"type\": \"ENTITY\",\n  \"data\": { ... },\n  \"meta\": {\n    \"id\": \"74807f00-ed0d-11ee-a357-ae468cd3ed16\",\n    \"modelKey\": { \"name\": \"nobel-prize\", \"version\": 1 },\n    \"state\": \"NEW\",\n    \"creationDate\": \"2025-08-01T10:00:00.000000000Z\",\n    \"lastUpdateTime\": \"2025-08-01T10:00:00.000000000Z\",\n    \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n    \"transitionForLatestSave\": \"UPDATE\"\n  }\n}\n```\n\n- `type` — always `\"ENTITY\"`\n- `data` — the entity's JSON payload (decoded with `json.Number` for numeric precision)\n- `meta.id` — UUID string\n- `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}`).\n- `meta.state` — current workflow state string\n- `meta.creationDate` — RFC 3339 with nanoseconds\n- `meta.lastUpdateTime` — RFC 3339 with nanoseconds\n- `meta.transactionId` — present when a transaction ID exists\n- `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.\n\n## OPTIMISTIC CONCURRENCY\n\nThe `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.\n\nTo use: read the entity (`GET /entity/{id}`), note `meta.transactionId`, include it in `If-Match` on the subsequent update.\n\n## ERRORS\n\n- `errors.ENTITY_NOT_FOUND` — `404` — entity UUID does not exist\n- `errors.MODEL_NOT_FOUND` — `404` — model referenced during create does not exist\n- `errors.MODEL_NOT_LOCKED` — `409` — model exists but is not in `LOCKED` state; entities cannot be created until the model is locked\n- `errors.VALIDATION_FAILED` — `400` — payload fails schema validation against the model\n- `errors.CONFLICT` — `409` — transaction conflict (retryable)\n- `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\n- `errors.TRANSITION_NOT_FOUND` — `404` — named transition does not exist in the workflow\n- `errors.BAD_REQUEST` — `400` — malformed request, invalid UUID, conflicting query parameters, states filter exceeds 1000 entries\n\n## EXAMPLES\n\n**Create a single entity:**\n\n```\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"category\":\"physics\",\"year\":\"2024\"}' \\\n  \"http://localhost:8080/api/entity/JSON/nobel-prize/1\"\n```\n\n**Read an entity:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16\"\n```\n\n**Read an entity at a point in time:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16?pointInTime=2025-08-01T10:00:00Z\"\n```\n\n**Update an entity with loopback transition:**\n\n```\ncurl -s -X PUT \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"If-Match: cb91fa80-d4a8-11ee-a357-ae468cd3ed16\" \\\n  -d '{\"category\":\"chemistry\",\"year\":\"2024\"}' \\\n  \"http://localhost:8080/api/entity/JSON/74807f00-ed0d-11ee-a357-ae468cd3ed16\"\n```\n\n**Update an entity with a named transition:**\n\n```\ncurl -s -X PUT \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"category\":\"chemistry\",\"year\":\"2024\"}' \\\n  \"http://localhost:8080/api/entity/JSON/74807f00-ed0d-11ee-a357-ae468cd3ed16/APPROVE\"\n```\n\n**Delete a single entity:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16\"\n```\n\n**Delete all entities for a model:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/nobel-prize/1\"\n```\n\n**List all entities for a model (page 0, size 20):**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/nobel-prize/1?pageSize=20&pageNumber=0\"\n```\n\n**Get entity change history:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16/changes\"\n```\n\n**Get available transitions:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16/transitions\"\n```\n\n**Create a multi-model collection:**\n\n```\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '[{\"model\":{\"name\":\"nobel-prize\",\"version\":1},\"payload\":\"{\\\"category\\\":\\\"physics\\\",\\\"year\\\":\\\"2024\\\"}\"}]' \\\n  \"http://localhost:8080/api/entity/JSON\"\n```\n\n**Get statistics by state for a model:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/stats/states/nobel-prize/1\"\n```\n\n## SEE ALSO\n\n- models\n- search\n- workflows\n- errors.ENTITY_NOT_FOUND\n- errors.MODEL_NOT_FOUND\n- errors.MODEL_NOT_LOCKED\n- errors.VALIDATION_FAILED\n- errors.CONFLICT\n- errors.TRANSITION_NOT_FOUND\n- openapi\n",
  "sections": [
    {
      "name": "NAME",
      "body": "crud — entity create, read, update, delete, and transition REST API."
    },
    {
      "name": "SYNOPSIS",
      "body": "```\nPOST   /api/entity/{format}/{entityName}/{modelVersion}\nPOST   /api/entity/{format}\nGET    /api/entity/{entityId}\nPUT    /api/entity/{format}/{entityId}\nPUT    /api/entity/{format}/{entityId}/{transition}\nPUT    /api/entity/{format}\nDELETE /api/entity/{entityId}\nDELETE /api/entity/{entityName}/{modelVersion}\nGET    /api/entity/{entityName}/{modelVersion}\nGET    /api/entity/{entityId}/changes\nGET    /api/entity/{entityId}/transitions\nGET    /api/entity/stats\nGET    /api/entity/stats/states\nGET    /api/entity/stats/{entityName}/{modelVersion}\nGET    /api/entity/stats/states/{entityName}/{modelVersion}\nGET    /api/platform-api/entity/fetch/transitions\n```\n\nContext path prefix is `CYODA_CONTEXT_PATH` (default `/api`). All endpoints require `Authorization: Bearer <token>` except when `CYODA_IAM_MODE=mock`."
    },
    {
      "name": "DESCRIPTION",
      "body": "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.\n\nBody size limit on all write endpoints: 10 MiB."
    },
    {
      "name": "ENDPOINTS",
      "body": "**POST /api/entity/{format}/{entityName}/{modelVersion}** — Create a single entity\n\n- `format` (path): `JSON` or `XML`\n- `entityName` (path): string — model name\n- `modelVersion` (path): int32\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n\nIf 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.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[{\n  \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n  \"entityIds\": [\"74807f00-ed0d-11ee-a357-ae468cd3ed16\"]\n}]\n```\n\n**POST /api/entity/{format}** — Create a collection (mixed models)\n\n- `format` (path): `JSON` or `XML`\n\n**IMPORTANT — `payload` is a JSON-encoded string, not an object.**\n\nThe `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.\n\nCorrect: `\"payload\": \"{\\\"category\\\":\\\"physics\\\"}\"`\nWrong:   `\"payload\": {\"category\":\"physics\"}`   (will be rejected with `errors.BAD_REQUEST`)\n\nRequest body: JSON array of `CreatePayload` objects:\n\n```json\n[\n  {\n    \"model\": { \"name\": \"nobel-prize\", \"version\": 1 },\n    \"payload\": \"{\\\"category\\\":\\\"physics\\\",\\\"year\\\":\\\"2024\\\"}\"\n  }\n]\n```\n\nEach item may reference a different model. All items are created in a single transaction.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[{\n  \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n  \"entityIds\": [\n    \"74807f00-ed0d-11ee-a357-ae468cd3ed16\",\n    \"72428380-0704-11ef-a357-ae468cd3ed16\"\n  ]\n}]\n```\n\n**GET /api/entity/{entityId}** — Read a single entity by UUID\n\n- `entityId` (path): UUID string\n- `pointInTime` (query, optional): RFC 3339 date-time — load entity state at this instant\n- `transactionId` (query, optional): UUID — load entity state as of the end of this transaction\n\n`pointInTime` and `transactionId` are mutually exclusive; supplying both returns `400 BAD_REQUEST`.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n{\n  \"type\": \"ENTITY\",\n  \"data\": { \"category\": \"physics\", \"year\": \"2024\" },\n  \"meta\": {\n    \"id\": \"74807f00-ed0d-11ee-a357-ae468cd3ed16\",\n    \"state\": \"NEW\",\n    \"creationDate\": \"2025-08-01T10:00:00Z\",\n    \"lastUpdateTime\": \"2025-08-01T10:00:00Z\",\n    \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n    \"transitionForLatestSave\": \"loopback\"\n  }\n}\n```\n\n**PUT /api/entity/{format}/{entityId}** — Update a single entity (loopback transition)\n\n- `format` (path): `JSON` or `XML`\n- `entityId` (path): UUID\n- `If-Match` (header, optional): transaction ID of last read — optimistic concurrency; if the entity was modified since, returns `412 Precondition Failed`\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n\nRequest body: updated entity JSON/XML payload.\n\nResponse: `200 OK`, `application/json`:\n\n```json\n{\n  \"transactionId\": \"733e7180-c055-11ef-a357-ae468cd3ed16\",\n  \"entityIds\": [\"cdcff600-bab1-11ee-a357-ae468cd3ed16\"]\n}\n```\n\n**PUT /api/entity/{format}/{entityId}/{transition}** — Update a single entity with a named transition\n\n- `format` (path): `JSON` or `XML`\n- `entityId` (path): UUID\n- `transition` (path): string — transition name defined in the model's workflow\n- `If-Match` (header, optional): transaction ID\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n\nResponse: `200 OK`, same shape as loopback update.\n\n**PUT /api/entity/{format}** — Update a collection (mixed entities)\n\n- `format` (path): `JSON` (only supported format today; single-item PUT endpoints still accept XML)\n- `transactionWindow` (query, optional): int32, default `100`, max `1000` — max items accepted in one batch; batches over the window are rejected with `400 BAD_REQUEST`\n- `transactionTimeoutMillis` (query, optional): int64, default `10000`\n- `waitForConsistencyAfter` (query, optional): boolean, default `false`\n\n**IMPORTANT — `payload` is a JSON-encoded string, not an object.**\n\nThe `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).\n\nCorrect: `\"payload\": \"{\\\"category\\\":\\\"physics\\\"}\"`\nWrong:   `\"payload\": {\"category\":\"physics\"}`   (will be rejected with `errors.BAD_REQUEST`)\n\nRequest body: JSON array of update items:\n\n```json\n[\n  {\n    \"id\": \"8824c480-c166-11ee-9e63-ae468cd3ed16\",\n    \"payload\": \"{\\\"category\\\":\\\"physics\\\",\\\"year\\\":\\\"2024\\\"}\",\n    \"transition\": \"UPDATE\"\n  }\n]\n```\n\nIf any entity in the collection is not found, the entire operation fails and no entities are updated (all-or-nothing).\n\nResponse: `200 OK`, `application/json`, `EntityTransactionResponse` array (one element — the whole collection runs in a single transaction):\n\n```json\n[\n  {\n    \"transactionId\": \"733e7180-c055-11ef-a357-ae468cd3ed16\",\n    \"entityIds\": [\"8824c480-c166-11ee-9e63-ae468cd3ed16\"]\n  }\n]\n```\n\n**DELETE /api/entity/{entityId}** — Delete a single entity by UUID\n\n- `entityId` (path): UUID\n\nResponse: `200 OK`, `application/json`:\n\n```json\n{\n  \"id\": \"a2242880-8d30-11ef-9e63-ae468cd3ed16\",\n  \"modelKey\": {\n    \"name\": \"nobel-prize\",\n    \"version\": 4\n  },\n  \"transactionId\": \"9fe62d00-a727-11ef-9e63-ae468cd3ed16\"\n}\n```\n\n**DELETE /api/entity/{entityName}/{modelVersion}** — Delete all entities for a model\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[{\n  \"deleteResult\": {\n    \"idToError\": {},\n    \"numberOfEntitites\": 42,\n    \"numberOfEntititesRemoved\": 42\n  },\n  \"entityModelClassId\": \"31134900-d9cb-11ee-b913-ae468cd3ed16\"\n}]\n```\n\n**GET /api/entity/{entityName}/{modelVersion}** — List all entities for a model (paginated)\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n- `pageSize` (query, optional): int32, default `20`\n- `pageNumber` (query, optional): int32, default `0`\n\nResponse: `200 OK`, `application/json`, array of entity envelopes (same shape as single-entity GET).\n\n**GET /api/entity/{entityId}/changes** — Get entity change history metadata\n\n- `entityId` (path): UUID\n- `pointInTime` (query, optional): RFC 3339 — view history as it existed at this time\n\nResponse: `200 OK`, `application/json`, array of change entries:\n\n```json\n[\n  {\n    \"changeType\": \"CREATED\",\n    \"timeOfChange\": \"2025-08-01T10:00:00Z\",\n    \"user\": \"admin\",\n    \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\"\n  },\n  {\n    \"changeType\": \"UPDATED\",\n    \"timeOfChange\": \"2025-08-02T09:00:00Z\",\n    \"user\": \"admin\",\n    \"transactionId\": \"733e7180-c055-11ef-a357-ae468cd3ed16\"\n  }\n]\n```\n\n- `changeType`: `CREATED`, `UPDATED`, or `DELETED`\n- `transactionId`: present only when `hasEntity` is true (i.e., entity payload exists at that version)\n\n**GET /api/entity/{entityId}/transitions** — List available transitions for an entity\n\n- `entityId` (path): UUID\n- `pointInTime` (query, optional): RFC 3339\n- `transactionId` (query, optional): UUID — derive point-in-time from transaction submit time\n\n`pointInTime` and `transactionId` are mutually exclusive; supplying both returns `400 BAD_REQUEST`. When neither is provided, the current time is used.\n\nResponse: `200 OK`, `application/json`, array of available transition names (as returned by the workflow engine).\n\n**GET /api/platform-api/entity/fetch/transitions** — List available transitions (platform-api format)\n\n- `entityClass` (query, required): string in `Name.Version` format, e.g., `Offer.1`\n- `entityId` (query, required): UUID string\n\nResponse: `200 OK`, `application/json`, array of available transition names.\n\n**GET /api/entity/stats** — Entity count statistics across all models\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[\n  { \"modelName\": \"nobel-prize\", \"modelVersion\": 1, \"count\": 42 },\n  { \"modelName\": \"family-member\", \"modelVersion\": 3, \"count\": 7 }\n]\n```\n\n**GET /api/entity/stats/states** — Entity count by state across all models\n\n- `states` (query, optional): comma-separated list of state names to filter by; maximum 1000 entries\n\nResponse: `200 OK`, `application/json`:\n\n```json\n[\n  { \"modelName\": \"nobel-prize\", \"modelVersion\": 1, \"state\": \"NEW\", \"count\": 10 },\n  { \"modelName\": \"nobel-prize\", \"modelVersion\": 1, \"state\": \"APPROVED\", \"count\": 32 }\n]\n```\n\n**GET /api/entity/stats/{entityName}/{modelVersion}** — Entity count for a specific model\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n\nResponse: `200 OK`, `application/json`, single `ModelStatsDto`.\n\n**GET /api/entity/stats/states/{entityName}/{modelVersion}** — Entity count by state for a specific model\n\n- `entityName` (path): string\n- `modelVersion` (path): int32\n- `states` (query, optional): list of state names to filter by; maximum 1000 entries\n\nResponse: `200 OK`, `application/json`, array of `ModelStateStatsDto`."
    },
    {
      "name": "ENTITY ENVELOPE",
      "body": "All entity read operations return entities in the standard envelope:\n\n```json\n{\n  \"type\": \"ENTITY\",\n  \"data\": { ... },\n  \"meta\": {\n    \"id\": \"74807f00-ed0d-11ee-a357-ae468cd3ed16\",\n    \"modelKey\": { \"name\": \"nobel-prize\", \"version\": 1 },\n    \"state\": \"NEW\",\n    \"creationDate\": \"2025-08-01T10:00:00.000000000Z\",\n    \"lastUpdateTime\": \"2025-08-01T10:00:00.000000000Z\",\n    \"transactionId\": \"cb91fa80-d4a8-11ee-a357-ae468cd3ed16\",\n    \"transitionForLatestSave\": \"UPDATE\"\n  }\n}\n```\n\n- `type` — always `\"ENTITY\"`\n- `data` — the entity's JSON payload (decoded with `json.Number` for numeric precision)\n- `meta.id` — UUID string\n- `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}`).\n- `meta.state` — current workflow state string\n- `meta.creationDate` — RFC 3339 with nanoseconds\n- `meta.lastUpdateTime` — RFC 3339 with nanoseconds\n- `meta.transactionId` — present when a transaction ID exists\n- `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."
    },
    {
      "name": "OPTIMISTIC CONCURRENCY",
      "body": "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.\n\nTo use: read the entity (`GET /entity/{id}`), note `meta.transactionId`, include it in `If-Match` on the subsequent update."
    },
    {
      "name": "ERRORS",
      "body": "- `errors.ENTITY_NOT_FOUND` — `404` — entity UUID does not exist\n- `errors.MODEL_NOT_FOUND` — `404` — model referenced during create does not exist\n- `errors.MODEL_NOT_LOCKED` — `409` — model exists but is not in `LOCKED` state; entities cannot be created until the model is locked\n- `errors.VALIDATION_FAILED` — `400` — payload fails schema validation against the model\n- `errors.CONFLICT` — `409` — transaction conflict (retryable)\n- `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\n- `errors.TRANSITION_NOT_FOUND` — `404` — named transition does not exist in the workflow\n- `errors.BAD_REQUEST` — `400` — malformed request, invalid UUID, conflicting query parameters, states filter exceeds 1000 entries"
    },
    {
      "name": "EXAMPLES",
      "body": "**Create a single entity:**\n\n```\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"category\":\"physics\",\"year\":\"2024\"}' \\\n  \"http://localhost:8080/api/entity/JSON/nobel-prize/1\"\n```\n\n**Read an entity:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16\"\n```\n\n**Read an entity at a point in time:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16?pointInTime=2025-08-01T10:00:00Z\"\n```\n\n**Update an entity with loopback transition:**\n\n```\ncurl -s -X PUT \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"If-Match: cb91fa80-d4a8-11ee-a357-ae468cd3ed16\" \\\n  -d '{\"category\":\"chemistry\",\"year\":\"2024\"}' \\\n  \"http://localhost:8080/api/entity/JSON/74807f00-ed0d-11ee-a357-ae468cd3ed16\"\n```\n\n**Update an entity with a named transition:**\n\n```\ncurl -s -X PUT \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"category\":\"chemistry\",\"year\":\"2024\"}' \\\n  \"http://localhost:8080/api/entity/JSON/74807f00-ed0d-11ee-a357-ae468cd3ed16/APPROVE\"\n```\n\n**Delete a single entity:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16\"\n```\n\n**Delete all entities for a model:**\n\n```\ncurl -s -X DELETE \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/nobel-prize/1\"\n```\n\n**List all entities for a model (page 0, size 20):**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/nobel-prize/1?pageSize=20&pageNumber=0\"\n```\n\n**Get entity change history:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16/changes\"\n```\n\n**Get available transitions:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/74807f00-ed0d-11ee-a357-ae468cd3ed16/transitions\"\n```\n\n**Create a multi-model collection:**\n\n```\ncurl -s -X POST \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '[{\"model\":{\"name\":\"nobel-prize\",\"version\":1},\"payload\":\"{\\\"category\\\":\\\"physics\\\",\\\"year\\\":\\\"2024\\\"}\"}]' \\\n  \"http://localhost:8080/api/entity/JSON\"\n```\n\n**Get statistics by state for a model:**\n\n```\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n  \"http://localhost:8080/api/entity/stats/states/nobel-prize/1\"\n```"
    },
    {
      "name": "SEE ALSO",
      "body": "- models\n- search\n- workflows\n- errors.ENTITY_NOT_FOUND\n- errors.MODEL_NOT_FOUND\n- errors.MODEL_NOT_LOCKED\n- errors.VALIDATION_FAILED\n- errors.CONFLICT\n- errors.TRANSITION_NOT_FOUND\n- openapi"
    }
  ],
  "see_also": [
    "models",
    "search",
    "workflows",
    "errors.ENTITY_NOT_FOUND",
    "errors.MODEL_NOT_FOUND",
    "errors.MODEL_NOT_LOCKED",
    "errors.VALIDATION_FAILED",
    "errors.CONFLICT",
    "errors.IDEMPOTENCY_CONFLICT",
    "errors.TRANSITION_NOT_FOUND",
    "openapi"
  ],
  "stability": "stable",
  "actions": []
}
