openapi: 3.1.0 info: title: NeNe Records API version: 0.1.0 description: > NeNe Records public API contract — entity_types, field_defs, entities, tags, entity_tags, entity_relations, text_fields, int_fields, enum_fields, bool_fields, and datetime_fields CRUD (MVP + Phase 2 registry + Phase 3 tags/relations). servers: - url: http://localhost:8080 description: Local development server tags: - name: Auth description: Authentication endpoints. - name: System description: Runtime health endpoints. - name: EntityTypes description: Entity type schema definitions. - name: Entities description: Entity instances bound to an entity type. - name: FieldDefs description: Registered field definitions per entity type. - name: TextFields description: Typed text field values on entities. - name: IntFields description: Typed integer field values on entities. - name: EnumFields description: Typed enum field values on entities. - name: BoolFields description: Typed boolean field values on entities. - name: DateTimeFields description: Typed datetime field values on entities. - name: Tags description: Tag definitions for entity classification. - name: EntityTags description: Tags attached to entity instances. - name: EntityRelations description: Typed relation links between entity instances. - name: Analytics description: HTTP access log aggregation and reporting. - name: PublicConsumer description: Aggregated read-only views for public consumer pages and MCP. - name: Settings description: Site-wide settings registry with revision history. - name: NavigationItems description: Ordered navigation link management for the public site header. - name: Webhooks description: Webhook endpoints for entity create/update/delete event notifications. - name: Users description: User account management (admin only) and self-service password change. paths: /api/v1/auth/login: post: tags: - Auth summary: Admin login description: Authenticates with email/password and returns a bearer token. operationId: login requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' example: email: admin@example.com password: secret responses: '200': description: Token issued content: application/json: schema: $ref: '#/components/schemas/LoginResponse' example: token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... expires_at: '2026-05-25T00:00:00Z' email: admin@example.com role: admin '401': $ref: '#/components/responses/InvalidCredentials' /health: get: tags: - System summary: Health endpoint description: Operational health check (NENE2 runtime). operationId: getHealth responses: '200': description: Healthy response (no checks configured). content: application/json: schema: $ref: '#/components/schemas/HealthResponse' examples: ok: summary: Healthy response (no checks) value: status: ok service: NENE2 '500': $ref: '#/components/responses/InternalServerError' /api/v1/entity-types: get: tags: - EntityTypes summary: List entity types description: Returns a paginated list of entity types. operationId: listEntityTypes parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' responses: '200': description: Paginated entity type list. content: application/json: schema: $ref: '#/components/schemas/EntityTypeListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - EntityTypes summary: Create entity type description: Creates a new entity type with a unique slug. operationId: createEntityType requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateEntityTypeRequest' examples: ok: value: name: Article slug: article responses: '201': description: Entity type created. content: application/json: schema: $ref: '#/components/schemas/EntityTypeResponse' examples: ok: value: id: 1 name: Article slug: article '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entity-types/{id}: get: tags: - EntityTypes summary: Get entity type by id operationId: getEntityTypeById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Entity type found. content: application/json: schema: $ref: '#/components/schemas/EntityTypeResponse' examples: ok: value: id: 1 name: Article slug: article '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - EntityTypes summary: Update entity type operationId: updateEntityType parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateEntityTypeRequest' examples: ok: value: name: Blog Post slug: blog-post responses: '200': description: Entity type updated. content: application/json: schema: $ref: '#/components/schemas/EntityTypeResponse' examples: ok: value: id: 1 name: Blog Post slug: blog-post '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - EntityTypes summary: Delete entity type description: Permanently deletes an entity type (hard delete). operationId: deleteEntityType parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Entity type deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities: get: tags: - Entities summary: List entities description: Returns non-deleted entities, optionally filtered by entity type, status, tags, relation fields, and full-text search. operationId: listEntities parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' - $ref: '#/components/parameters/EntityTypeIdQuery' - name: status in: query required: false schema: $ref: '#/components/schemas/EntityStatus' description: Filter by publish status. - $ref: '#/components/parameters/TagsQuery' - $ref: '#/components/parameters/TagQuery' - $ref: '#/components/parameters/RelationFilterQuery' - name: q in: query required: false schema: type: string minLength: 1 description: Full-text search across slug and text field values (partial match). - name: sort in: query required: false schema: type: string enum: [id, published_at, title] default: id description: Sort key. `title` joins the `title` text field value. - name: order in: query required: false schema: type: string enum: [asc, desc] default: desc description: Sort direction. responses: '200': description: Paginated entity list. content: application/json: schema: $ref: '#/components/schemas/EntityListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 total: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - Entities summary: Create entity operationId: createEntity requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateEntityRequest' examples: ok: value: entity_type_id: 1 responses: '201': description: Entity created. content: application/json: schema: $ref: '#/components/schemas/EntityResponse' examples: ok: value: id: 1 entity_type_id: 1 status: draft published_at: null scheduled_at: null is_deleted: false deleted_at: null meta_title: null meta_description: null '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/export: get: tags: - Entities summary: Export entities description: Export non-deleted entities as CSV or JSON. Returns up to 10,000 records including all text field values. operationId: exportEntities parameters: - $ref: '#/components/parameters/EntityTypeIdQuery' - name: format in: query required: false schema: type: string enum: [json, csv] default: json description: Response format. Use `csv` to download a spreadsheet-compatible file. - name: status in: query required: false schema: $ref: '#/components/schemas/EntityStatus' description: Filter by publish status. - name: q in: query required: false schema: type: string minLength: 1 description: Full-text search across slug and text field values. responses: '200': description: Exported entity data. content: application/json: schema: type: object required: [items, total] properties: items: type: array items: type: object total: type: integer text/csv: schema: type: string format: binary '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{id}: get: tags: - Entities summary: Get entity by id operationId: getEntityById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Entity found. content: application/json: schema: $ref: '#/components/schemas/EntityResponse' examples: ok: value: id: 1 entity_type_id: 1 status: draft published_at: null scheduled_at: null is_deleted: false deleted_at: null meta_title: null meta_description: null '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - Entities summary: Update entity operationId: updateEntity parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateEntityRequest' examples: ok: value: entity_type_id: 2 responses: '200': description: Entity updated. content: application/json: schema: $ref: '#/components/schemas/EntityResponse' examples: ok: value: id: 1 entity_type_id: 2 status: published published_at: '2026-05-24T10:00:00+00:00' scheduled_at: null is_deleted: false deleted_at: null meta_title: null meta_description: null '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Entities summary: Soft delete entity operationId: deleteEntity parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Entity soft-deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{id}/revisions: get: tags: - Entities summary: List entity revisions description: Returns paginated revision history for an entity record. operationId: listEntityRevisions parameters: - $ref: '#/components/parameters/IdPath' - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' responses: '200': description: Paginated revision list. content: application/json: schema: $ref: '#/components/schemas/EntityRevisionListResponse' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{id}/schedule: post: tags: - Entities summary: Schedule entity for future publish description: Sets the entity status to `scheduled` and stores the future publish datetime. operationId: scheduleEntity parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ScheduleEntityRequest' examples: schedule: value: scheduled_at: '2026-06-01T09:00:00+09:00' responses: '200': description: Entity scheduled. content: application/json: schema: $ref: '#/components/schemas/ScheduleEntityResponse' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Entities summary: Cancel scheduled publish description: Cancels the scheduled publish and reverts the entity status to `draft`. operationId: unscheduleEntity parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Schedule cancelled. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/process-scheduled: post: tags: - Entities summary: Process scheduled publishes description: Publishes all entities whose `scheduled_at` has passed. Intended for cron job invocation. operationId: processScheduledPublish responses: '200': description: Result of the batch publish. content: application/json: schema: $ref: '#/components/schemas/ProcessScheduledPublishResponse' examples: ok: value: published_count: 2 published_ids: [10, 11] '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{entityId}/tags: get: tags: - EntityTags summary: List tags attached to entity operationId: listEntityTags parameters: - $ref: '#/components/parameters/EntityIdPath' responses: '200': description: Tags attached to the entity. content: application/json: schema: $ref: '#/components/schemas/EntityTagListResponse' examples: ok: value: items: [] '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' post: tags: - EntityTags summary: Attach tag to entity operationId: attachEntityTag parameters: - $ref: '#/components/parameters/EntityIdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AttachEntityTagRequest' examples: ok: value: tag_id: 1 responses: '201': description: Tag attached. content: application/json: schema: $ref: '#/components/schemas/TagResponse' examples: ok: value: id: 1 slug: featured name: Featured '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{entityId}/tags/{tagId}: delete: tags: - EntityTags summary: Detach tag from entity operationId: detachEntityTag parameters: - $ref: '#/components/parameters/EntityIdPath' - $ref: '#/components/parameters/TagIdPath' responses: '204': description: Tag detached. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{entityId}/relations: get: tags: - EntityRelations summary: List relations attached to entity operationId: listEntityRelations parameters: - $ref: '#/components/parameters/EntityIdPath' - name: field_key in: query required: true schema: type: string minLength: 1 responses: '200': description: Relations for the given field key. content: application/json: schema: $ref: '#/components/schemas/EntityRelationListResponse' examples: ok: value: items: [] '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - EntityRelations summary: Attach relation target to entity operationId: attachEntityRelation parameters: - $ref: '#/components/parameters/EntityIdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AttachEntityRelationRequest' examples: ok: value: field_key: author target_entity_id: 2 responses: '201': description: Relation attached. content: application/json: schema: $ref: '#/components/schemas/EntityRelationItemResponse' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{entityId}/relations/{targetEntityId}: delete: tags: - EntityRelations summary: Detach relation target from entity operationId: detachEntityRelation parameters: - $ref: '#/components/parameters/EntityIdPath' - name: targetEntityId in: path required: true schema: type: integer format: int64 minimum: 1 - name: field_key in: query required: true schema: type: string minLength: 1 responses: '204': description: Relation detached. '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/field-defs: get: tags: - FieldDefs summary: List field definitions description: Returns non-deleted field definitions, optionally filtered by entity type. operationId: listFieldDefs parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' - $ref: '#/components/parameters/EntityTypeIdQuery' responses: '200': description: Paginated field definition list. content: application/json: schema: $ref: '#/components/schemas/FieldDefListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - FieldDefs summary: Create field definition description: Registers a field key and data type for an entity type. operationId: createFieldDef requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateFieldDefRequest' examples: ok: value: entity_type_id: 1 field_key: title data_type: text responses: '201': description: Field definition created. content: application/json: schema: $ref: '#/components/schemas/FieldDefResponse' examples: ok: value: id: 1 entity_type_id: 1 field_key: title data_type: text '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/FieldDefConflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/field-defs/{id}: get: tags: - FieldDefs summary: Get field definition by id operationId: getFieldDefById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Field definition found. content: application/json: schema: $ref: '#/components/schemas/FieldDefResponse' examples: ok: value: id: 1 entity_type_id: 1 field_key: title data_type: text '404': $ref: '#/components/responses/FieldDefNotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - FieldDefs summary: Update field definition operationId: updateFieldDef parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateFieldDefRequest' examples: ok: value: entity_type_id: 1 field_key: headline data_type: text responses: '200': description: Field definition updated. content: application/json: schema: $ref: '#/components/schemas/FieldDefResponse' examples: ok: value: id: 1 entity_type_id: 1 field_key: headline data_type: text '404': $ref: '#/components/responses/FieldDefNotFound' '409': $ref: '#/components/responses/FieldDefConflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - FieldDefs summary: Soft delete field definition operationId: deleteFieldDef parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Field definition soft-deleted. '404': $ref: '#/components/responses/FieldDefNotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/text-fields: get: tags: - TextFields summary: List text fields description: Returns non-deleted text fields. operationId: listTextFields parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' - $ref: '#/components/parameters/EntityIdQuery' - $ref: '#/components/parameters/EntityTypeIdQuery' - name: locale in: query required: false description: Filter by locale code (e.g. "ja", "en"). When omitted, rows of all locales are returned. schema: type: string maxLength: 10 responses: '200': description: Paginated text field list. content: application/json: schema: $ref: '#/components/schemas/TextFieldListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - TextFields summary: Create text field operationId: createTextField requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateTextFieldRequest' examples: ok: value: entity_id: 1 field_key: title value: Hello world withLocale: value: entity_id: 1 field_key: title value: こんにちは locale: ja responses: '201': description: Text field created. content: application/json: schema: $ref: '#/components/schemas/TextFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: title value: Hello world '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' /api/v1/text-fields/{id}: get: tags: - TextFields summary: Get text field by id operationId: getTextFieldById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Text field found. content: application/json: schema: $ref: '#/components/schemas/TextFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: title value: Hello world '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - TextFields summary: Update text field operationId: updateTextField parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateTextFieldRequest' examples: ok: value: field_key: title value: Updated title responses: '200': description: Text field updated. content: application/json: schema: $ref: '#/components/schemas/TextFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: title value: Updated title '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - TextFields summary: Soft delete text field operationId: deleteTextField parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Text field soft-deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/int-fields: get: tags: - IntFields summary: List int fields description: Returns non-deleted int fields. operationId: listIntFields parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' - $ref: '#/components/parameters/EntityIdQuery' responses: '200': description: Paginated int field list. content: application/json: schema: $ref: '#/components/schemas/IntFieldListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - IntFields summary: Create int field operationId: createIntField requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateIntFieldRequest' examples: ok: value: entity_id: 1 field_key: count value: 42 responses: '201': description: Int field created. content: application/json: schema: $ref: '#/components/schemas/IntFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: count value: 42 '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' /api/v1/int-fields/{id}: get: tags: - IntFields summary: Get int field by id operationId: getIntFieldById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Int field found. content: application/json: schema: $ref: '#/components/schemas/IntFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: count value: 42 '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - IntFields summary: Update int field operationId: updateIntField parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateIntFieldRequest' examples: ok: value: field_key: count value: 99 responses: '200': description: Int field updated. content: application/json: schema: $ref: '#/components/schemas/IntFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: count value: 99 '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - IntFields summary: Soft delete int field operationId: deleteIntField parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Int field soft-deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/enum-fields: get: tags: - EnumFields summary: List enum fields description: Returns non-deleted enum fields. operationId: listEnumFields parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' - $ref: '#/components/parameters/EntityIdQuery' responses: '200': description: Paginated enum field list. content: application/json: schema: $ref: '#/components/schemas/EnumFieldListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - EnumFields summary: Create enum field operationId: createEnumField requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateEnumFieldRequest' examples: ok: value: entity_id: 1 field_key: status value: active responses: '201': description: Enum field created. content: application/json: schema: $ref: '#/components/schemas/EnumFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: status value: active '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' /api/v1/enum-fields/{id}: get: tags: - EnumFields summary: Get enum field by id operationId: getEnumFieldById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Enum field found. content: application/json: schema: $ref: '#/components/schemas/EnumFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: status value: active '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - EnumFields summary: Update enum field operationId: updateEnumField parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateEnumFieldRequest' examples: ok: value: field_key: status value: inactive responses: '200': description: Enum field updated. content: application/json: schema: $ref: '#/components/schemas/EnumFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: status value: inactive '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - EnumFields summary: Soft delete enum field operationId: deleteEnumField parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Enum field soft-deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/bool-fields: get: tags: - BoolFields summary: List bool fields description: Returns non-deleted bool fields. operationId: listBoolFields parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' - $ref: '#/components/parameters/EntityIdQuery' responses: '200': description: Paginated bool field list. content: application/json: schema: $ref: '#/components/schemas/BoolFieldListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - BoolFields summary: Create bool field operationId: createBoolField requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateBoolFieldRequest' examples: ok: value: entity_id: 1 field_key: enabled value: true responses: '201': description: Bool field created. content: application/json: schema: $ref: '#/components/schemas/BoolFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: enabled value: true '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' /api/v1/bool-fields/{id}: get: tags: - BoolFields summary: Get bool field by id operationId: getBoolFieldById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Bool field found. content: application/json: schema: $ref: '#/components/schemas/BoolFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: enabled value: true '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - BoolFields summary: Update bool field operationId: updateBoolField parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateBoolFieldRequest' examples: ok: value: field_key: enabled value: false responses: '200': description: Bool field updated. content: application/json: schema: $ref: '#/components/schemas/BoolFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: enabled value: false '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - BoolFields summary: Soft delete bool field operationId: deleteBoolField parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Bool field soft-deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/datetime-fields: get: tags: - DateTimeFields summary: List datetime fields description: Returns non-deleted datetime fields. operationId: listDateTimeFields parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' - $ref: '#/components/parameters/EntityIdQuery' responses: '200': description: Paginated datetime field list. content: application/json: schema: $ref: '#/components/schemas/DateTimeFieldListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - DateTimeFields summary: Create datetime field operationId: createDateTimeField requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateDateTimeFieldRequest' examples: ok: value: entity_id: 1 field_key: published_at value: 2026-05-24T12:00:00+00:00 responses: '201': description: Datetime field created. content: application/json: schema: $ref: '#/components/schemas/DateTimeFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: published_at value: 2026-05-24T12:00:00+00:00 '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' /api/v1/datetime-fields/{id}: get: tags: - DateTimeFields summary: Get datetime field by id operationId: getDateTimeFieldById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Datetime field found. content: application/json: schema: $ref: '#/components/schemas/DateTimeFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: published_at value: 2026-05-24T12:00:00+00:00 '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - DateTimeFields summary: Update datetime field operationId: updateDateTimeField parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateDateTimeFieldRequest' examples: ok: value: field_key: published_at value: 2026-05-25T12:00:00+00:00 responses: '200': description: Datetime field updated. content: application/json: schema: $ref: '#/components/schemas/DateTimeFieldResponse' examples: ok: value: id: 1 entity_id: 1 field_key: published_at value: 2026-05-25T12:00:00+00:00 '404': $ref: '#/components/responses/NotFound' '422': description: Validation failed, unregistered field key, or field type mismatch. content: application/problem+json: schema: oneOf: - $ref: '#/components/schemas/ValidationProblemDetails' - $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - DateTimeFields summary: Soft delete datetime field operationId: deleteDateTimeField parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Datetime field soft-deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/tags: get: tags: - Tags summary: List tags description: Returns a paginated list of tags. operationId: listTags parameters: - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' responses: '200': description: Paginated tag list. content: application/json: schema: $ref: '#/components/schemas/TagListResponse' examples: ok: summary: Empty list value: items: [] limit: 20 offset: 0 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' post: tags: - Tags summary: Create tag description: Creates a new tag with a unique slug. operationId: createTag requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateTagRequest' examples: ok: value: name: Featured slug: featured responses: '201': description: Tag created. content: application/json: schema: $ref: '#/components/schemas/TagResponse' examples: ok: value: id: 1 slug: featured name: Featured '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/tags/{id}: get: tags: - Tags summary: Get tag by id operationId: getTagById parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: Tag found. content: application/json: schema: $ref: '#/components/schemas/TagResponse' examples: ok: value: id: 1 slug: featured name: Featured '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' put: tags: - Tags summary: Update tag operationId: updateTag parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateTagRequest' examples: ok: value: name: Featured Item slug: featured-item responses: '200': description: Tag updated. content: application/json: schema: $ref: '#/components/schemas/TagResponse' examples: ok: value: id: 1 slug: featured-item name: Featured Item '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Tags summary: Delete tag description: Permanently deletes a tag (hard delete). operationId: deleteTag parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: Tag deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/settings: get: tags: - Settings summary: List settings description: Returns all registered settings with effective values for admin use. operationId: listSettings responses: '200': description: Setting list. content: application/json: schema: $ref: '#/components/schemas/SettingListResponse' '500': $ref: '#/components/responses/InternalServerError' /api/v1/settings/{key}: put: tags: - Settings summary: Update setting value description: Updates a setting value and appends a revision record atomically. operationId: updateSettingByKey parameters: - $ref: '#/components/parameters/SettingKeyPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateSettingRequest' responses: '200': description: Updated setting value. content: application/json: schema: $ref: '#/components/schemas/SettingValueResponse' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/settings/{key}/revisions: get: tags: - Settings summary: List setting revisions description: Returns paginated revision history for a setting key. operationId: listSettingRevisions parameters: - $ref: '#/components/parameters/SettingKeyPath' - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/OffsetQuery' responses: '200': description: Paginated revision list. content: application/json: schema: $ref: '#/components/schemas/SettingRevisionListResponse' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/public/settings: get: tags: - Settings summary: List public settings description: Returns effective values for settings marked as public. operationId: listPublicSettings responses: '200': description: Public setting list. headers: Cache-Control: schema: type: string example: public, max-age=300, stale-while-revalidate=3600 ETag: schema: type: string content: application/json: schema: $ref: '#/components/schemas/PublicSettingListResponse' '304': description: Not Modified — ETag matched, use cached response. headers: ETag: schema: type: string Cache-Control: schema: type: string '500': $ref: '#/components/responses/InternalServerError' /api/v1/public/entity-types/{slug}/records/{entitySlug}: get: tags: - PublicConsumer summary: Get aggregated public record view description: > Returns a single JSON payload with entity type list, entity, field definitions, typed field values, and relation data — aligned with the consumer detail page TanStack Query cache keys. operationId: getPublicRecordView parameters: - name: slug in: path required: true description: Entity type slug. schema: type: string minLength: 1 - name: entitySlug in: path required: true description: Entity slug. schema: type: string minLength: 1 responses: '200': description: Aggregated public record view bootstrap payload. headers: Cache-Control: schema: type: string example: public, max-age=60, stale-while-revalidate=300 ETag: schema: type: string content: application/json: schema: $ref: '#/components/schemas/PublicRecordViewResponse' '304': description: Not Modified — ETag matched, use cached response. headers: ETag: schema: type: string Cache-Control: schema: type: string '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/webhooks: get: tags: - Webhooks summary: List webhooks description: Returns all registered webhooks ordered by id. operationId: listWebhooks responses: '200': description: Webhook list. content: application/json: schema: $ref: '#/components/schemas/WebhookListResponse' post: tags: - Webhooks summary: Create webhook description: Registers a new webhook endpoint. operationId: createWebhook requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateWebhookRequest' responses: '201': description: Webhook created. headers: Location: schema: type: string example: /api/v1/webhooks/1 content: application/json: schema: $ref: '#/components/schemas/WebhookResponse' '422': $ref: '#/components/responses/ValidationFailed' /api/v1/webhooks/{id}: parameters: - name: id in: path required: true schema: type: integer format: int64 minimum: 1 get: tags: - Webhooks summary: Get webhook by ID operationId: getWebhookById responses: '200': description: Webhook found. content: application/json: schema: $ref: '#/components/schemas/WebhookResponse' '404': $ref: '#/components/responses/NotFound' put: tags: - Webhooks summary: Update webhook operationId: updateWebhook requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateWebhookRequest' responses: '200': description: Webhook updated. content: application/json: schema: $ref: '#/components/schemas/WebhookResponse' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' delete: tags: - Webhooks summary: Delete webhook operationId: deleteWebhook responses: '204': description: Webhook deleted. '404': $ref: '#/components/responses/NotFound' /api/v1/entities/{id}/comments: get: tags: - Comments summary: List approved comments for an entity description: Returns approved comments for the specified entity, ordered by created_at ascending. operationId: listComments parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Comment list. content: application/json: schema: $ref: '#/components/schemas/CommentListResponse' '500': $ref: '#/components/responses/InternalServerError' post: tags: - Comments summary: Post a comment description: Submits a new comment. The comment is stored with is_approved=false and awaits moderation. operationId: postComment parameters: - name: id in: path required: true schema: type: integer requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PostCommentRequest' responses: '201': description: Comment submitted. content: application/json: schema: $ref: '#/components/schemas/CommentResponse' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/admin/comments: get: tags: - Comments summary: List all comments (admin) description: Returns all comments (approved and pending), ordered by created_at descending. Requires ManageSettings capability. operationId: listAllComments security: - BearerAuth: [] responses: '200': description: Comment list. content: application/json: schema: $ref: '#/components/schemas/AdminCommentListResponse' '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '500': $ref: '#/components/responses/InternalServerError' /api/v1/admin/comments/{id}/approve: patch: tags: - Comments summary: Approve a comment description: Sets is_approved=true for the specified comment. operationId: approveComment security: - BearerAuth: [] parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Comment approved. content: application/json: schema: $ref: '#/components/schemas/AdminCommentResponse' '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/admin/comments/{id}: delete: tags: - Comments summary: Delete a comment description: Permanently removes the specified comment. operationId: deleteComment security: - BearerAuth: [] parameters: - name: id in: path required: true schema: type: integer responses: '204': description: Comment deleted. '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/media: get: tags: - Media summary: List media items description: Returns all uploaded media items ordered by created_at descending. operationId: listMedia responses: '200': description: Media list. content: application/json: schema: $ref: '#/components/schemas/MediaListResponse' '500': $ref: '#/components/responses/InternalServerError' post: tags: - Media summary: Upload media file description: "Accepts a multipart/form-data upload and returns the stored media URL. Allowed types — images (JPEG, PNG, GIF, WebP, SVG), documents (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX), video (MP4, WebM), audio (MP3, WAV, OGG), archives (ZIP), text (TXT, CSV). Maximum size — 10 MiB." operationId: uploadMedia requestBody: required: true content: multipart/form-data: schema: type: object required: - file properties: file: type: string format: binary responses: '201': description: Media uploaded. content: application/json: schema: $ref: '#/components/schemas/MediaResponse' examples: ok: value: id: 1 url: /media/2026/05/abc123.jpg original_name: photo.jpg mime_type: image/jpeg size: 204800 '413': $ref: '#/components/responses/PayloadTooLarge' /api/v1/media/{id}: delete: tags: - Media summary: Delete media item description: Deletes a media item and its physical file from storage. operationId: deleteMedia parameters: - name: id in: path required: true schema: type: integer format: int64 responses: '204': description: Media deleted. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/navigation-items: get: tags: - NavigationItems summary: List navigation items description: Returns all navigation items ordered by display_order, then id. operationId: listNavigationItems responses: '200': description: Navigation item list. content: application/json: schema: $ref: '#/components/schemas/NavigationItemListResponse' '500': $ref: '#/components/responses/InternalServerError' post: tags: - NavigationItems summary: Create navigation item description: Creates a new navigation item. operationId: createNavigationItem requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateNavigationItemRequest' responses: '201': description: Created navigation item. content: application/json: schema: $ref: '#/components/schemas/NavigationItemResponse' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/navigation-items/{id}: put: tags: - NavigationItems summary: Update navigation item description: Updates an existing navigation item by ID. operationId: updateNavigationItem parameters: - name: id in: path required: true description: Navigation item ID. schema: type: integer minimum: 1 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateNavigationItemRequest' responses: '200': description: Updated navigation item. content: application/json: schema: $ref: '#/components/schemas/NavigationItemResponse' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - NavigationItems summary: Delete navigation item description: Permanently deletes a navigation item by ID. operationId: deleteNavigationItem parameters: - name: id in: path required: true description: Navigation item ID. schema: type: integer minimum: 1 responses: '204': description: Deleted successfully. '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/public/navigation-items: get: tags: - PublicConsumer summary: List public navigation items description: Returns all navigation items for the public site header. No authentication required. operationId: listPublicNavigationItems responses: '200': description: Navigation item list. headers: Cache-Control: schema: type: string example: public, max-age=300, stale-while-revalidate=3600 ETag: schema: type: string content: application/json: schema: $ref: '#/components/schemas/NavigationItemListResponse' '304': description: Not Modified — ETag matched, use cached response. headers: ETag: schema: type: string Cache-Control: schema: type: string '500': $ref: '#/components/responses/InternalServerError' /api/v1/entities/{id}/preview-token: post: tags: - Entity summary: Generate a draft preview token description: Generates (or rotates) a 24-hour preview token for the entity. Any previously issued token for the same entity is revoked. operationId: generatePreviewToken security: - bearerAuth: [] parameters: - name: id in: path required: true schema: type: integer responses: '201': description: Preview token created. content: application/json: schema: $ref: '#/components/schemas/GeneratePreviewTokenResponse' '401': $ref: '#/components/responses/InvalidCredentials' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Entity summary: Revoke draft preview token description: Deletes all preview tokens for the entity. Returns 204 even if no token existed. operationId: revokePreviewToken security: - bearerAuth: [] parameters: - name: id in: path required: true schema: type: integer responses: '204': description: Token revoked. '401': $ref: '#/components/responses/InvalidCredentials' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/public/preview/{token}: get: tags: - PublicConsumer summary: Get entity record by preview token description: Returns the full public record view for any entity (including drafts) identified by a valid, non-expired preview token. No authentication required. operationId: getPreviewRecordByToken parameters: - name: token in: path required: true schema: type: string responses: '200': description: Preview record bootstrap data. content: application/json: schema: $ref: '#/components/schemas/PublicRecordViewResponse' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /api/v1/dashboard: get: tags: - Dashboard summary: Get dashboard summary description: Returns an aggregated summary for the admin dashboard — recent published entities, today/month access counts, and published/draft counts per entity type. operationId: getDashboardSummary responses: '200': description: Dashboard summary. content: application/json: schema: $ref: '#/components/schemas/DashboardSummaryResponse' examples: ok: value: recent_published: - id: 10 entity_type_id: 1 entity_type_name: Article entity_type_slug: article slug: hello-world published_at: '2026-05-25T10:00:00+00:00' today_access_count: 42 this_month_access_count: 1234 entity_type_summary: - entity_type_id: 1 entity_type_name: Article entity_type_slug: article published_count: 5 draft_count: 2 '500': $ref: '#/components/responses/InternalServerError' /api/v1/analytics/access-stats: get: tags: - Analytics summary: Access stats by date description: Returns daily HTTP access counts and average duration for the inclusive date range. operationId: getAccessStatsByDate parameters: - name: from in: query required: true description: Start date (inclusive), YYYY-MM-DD. schema: type: string format: date - name: to in: query required: true description: End date (inclusive), YYYY-MM-DD. Range must not exceed 366 days. schema: type: string format: date responses: '200': description: Daily access statistics. content: application/json: schema: $ref: '#/components/schemas/AccessStatsByDateResponse' examples: ok: value: from: '2026-05-01' to: '2026-05-03' items: - date: '2026-05-01' request_count: 12 avg_duration_ms: 8.5 - date: '2026-05-02' request_count: 7 avg_duration_ms: 11.2 '422': $ref: '#/components/responses/ValidationFailed' '500': $ref: '#/components/responses/InternalServerError' /api/v1/users: get: tags: - Users summary: List users description: Returns all user accounts. Admin only. operationId: listUsers security: - bearerAuth: [] responses: '200': description: List of users. content: application/json: schema: $ref: '#/components/schemas/UserListResponse' '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' post: tags: - Users summary: Create user description: Creates a new user account with a password set immediately. Admin only. operationId: createUser security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateUserRequest' responses: '201': description: User created. headers: Location: schema: type: string description: URL of the new user. content: application/json: schema: $ref: '#/components/schemas/UserResponse' '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' /api/v1/users/{id}: get: tags: - Users summary: Get user description: Returns a single user by ID. Admin only. operationId: getUserById security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdPath' responses: '200': description: User found. content: application/json: schema: $ref: '#/components/schemas/UserResponse' '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '404': $ref: '#/components/responses/NotFound' patch: tags: - Users summary: Update user role description: Changes the role of a user. Admin only. operationId: updateUserRole security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateUserRoleRequest' responses: '200': description: Role updated. content: application/json: schema: $ref: '#/components/schemas/UserResponse' '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' delete: tags: - Users summary: Delete user description: Deletes a user account. Admin only. Cannot delete own account or the last admin. operationId: deleteUser security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdPath' responses: '204': description: User deleted. '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '404': $ref: '#/components/responses/NotFound' /api/v1/users/{id}/password: patch: tags: - Users summary: Admin password reset description: Sets a new password for any user. Admin only. operationId: adminResetPassword security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdPath' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AdminResetPasswordRequest' responses: '204': description: Password updated. '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '403': description: Forbidden. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationFailed' /api/v1/users/me/password: put: tags: - Users summary: Change own password description: Changes the authenticated user's own password. Requires current password verification. operationId: changeOwnPassword security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ChangeOwnPasswordRequest' responses: '204': description: Password changed. '401': description: Unauthorized. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' '422': $ref: '#/components/responses/ValidationFailed' /api/v1/users/invite: post: tags: - Users summary: Invite user description: Creates an invited user and sends an email with an activation link. Admin only. operationId: inviteUser security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/InviteUserRequest' responses: '201': description: Invite sent. headers: Location: schema: type: string content: application/json: schema: $ref: '#/components/schemas/UserResponse' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' /api/v1/auth/accept-invite: post: tags: - Users summary: Accept invite description: Accepts an invitation token and sets the user's password, activating the account. operationId: acceptInvite requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AcceptInviteRequest' responses: '204': description: Account activated. '422': $ref: '#/components/responses/ValidationFailed' /api/v1/auth/password-reset: post: tags: - Auth summary: Request password reset description: > Sends a password reset email. Always returns 204 regardless of whether the email exists (prevents email enumeration). operationId: requestPasswordReset requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PasswordResetRequest' responses: '204': description: Reset email sent (or silently ignored if email not found). '422': $ref: '#/components/responses/ValidationFailed' /api/v1/auth/password-reset/confirm: post: tags: - Auth summary: Confirm password reset description: Validates the reset token and sets a new password. operationId: confirmPasswordReset requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PasswordResetConfirmRequest' responses: '204': description: Password updated. '422': $ref: '#/components/responses/ValidationFailed' components: parameters: IdPath: name: id in: path required: true schema: type: integer format: int64 minimum: 1 example: 1 LimitQuery: name: limit in: query required: false schema: type: integer minimum: 1 maximum: 100 default: 20 example: 20 OffsetQuery: name: offset in: query required: false schema: type: integer minimum: 0 default: 0 example: 0 EntityTypeIdQuery: name: entity_type_id in: query required: false schema: type: integer format: int64 minimum: 1 example: 1 EntityIdQuery: name: entity_id in: query required: false schema: type: integer format: int64 minimum: 1 example: 1 TagsQuery: name: tags in: query required: false description: Comma-separated tag slugs; entities matching any slug are returned. schema: type: string example: featured,draft TagQuery: name: tag in: query required: false description: Alias for a single tag slug or comma-separated slugs (merged with `tags`). schema: type: string example: featured RelationFilterQuery: name: relation.{field_key} in: query required: false description: | Filter entities linked via a relation field. Use one query parameter per field key, for example `relation.author=5`. Multiple relation filters are combined with AND. PHP query parsing may expose these keys as `relation_{field_key}` in the parameter array. schema: type: integer format: int64 minimum: 1 example: 5 EntityIdPath: name: entityId in: path required: true schema: type: integer format: int64 minimum: 1 example: 1 TagIdPath: name: tagId in: path required: true schema: type: integer format: int64 minimum: 1 example: 1 SettingKeyPath: name: key in: path required: true schema: type: string pattern: '^[a-z][a-z0-9_]*$' example: site_name responses: InvalidCredentials: description: Email or password is incorrect. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: invalidCredentials: value: type: https://nene-records.dev/problems/invalid-credentials title: Invalid credentials status: 401 detail: Email or password is incorrect. NotFound: description: Resource not found. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: notFound: value: type: https://nene-records.dev/problems/not-found title: Not Found status: 404 detail: The requested resource was not found. instance: /api/v1/entity-types/99 Conflict: description: Request conflicts with current state. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: conflict: value: type: https://nene-records.dev/problems/conflict title: Conflict status: 409 detail: The slug is already in use. instance: /api/v1/entity-types FieldDefNotFound: description: Field definition not found. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: fieldDefNotFound: value: type: https://nene-records.dev/problems/field-def-not-found title: Field Definition Not Found status: 404 detail: The requested field definition was not found. instance: /api/v1/field-defs/99 FieldDefConflict: description: Duplicate field key for entity type. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: fieldDefConflict: value: type: https://nene-records.dev/problems/field-def-conflict title: Conflict status: 409 detail: A field definition with this key already exists for the entity type. instance: /api/v1/field-defs FieldKeyNotRegistered: description: Field key is not registered for the entity type. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: fieldKeyNotRegistered: value: type: https://nene-records.dev/problems/field-key-not-registered title: Field Key Not Registered status: 422 detail: The field key is not registered for this entity type. instance: /api/v1/text-fields FieldTypeMismatch: description: Field key is registered with a different data type. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: fieldTypeMismatch: value: type: https://nene-records.dev/problems/field-type-mismatch title: Field Type Mismatch status: 422 detail: The field key is registered with a different data type. instance: /api/v1/text-fields PayloadTooLarge: description: Uploaded file exceeds size limit. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' ValidationFailed: description: Validation failed. content: application/problem+json: schema: $ref: '#/components/schemas/ValidationProblemDetails' examples: validationFailed: value: type: https://nene-records.dev/problems/validation-failed title: Validation Failed status: 422 detail: One or more fields failed validation. instance: /api/v1/entity-types errors: - field: slug message: Slug is required. code: required InternalServerError: description: Internal server error. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' examples: internal: value: type: https://nene-records.dev/problems/internal-server-error title: Internal Server Error status: 500 detail: An unexpected error occurred. instance: /api/v1/entity-types schemas: UserResponse: type: object required: - id - email - role - status properties: id: type: integer email: type: string format: email role: type: string enum: - admin - editor status: type: string enum: - active - invited created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true additionalProperties: false UserListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/UserResponse' additionalProperties: false CreateUserRequest: type: object required: - email - password - role properties: email: type: string format: email password: type: string minLength: 8 role: type: string enum: - admin - editor additionalProperties: false UpdateUserRoleRequest: type: object required: - role properties: role: type: string enum: - admin - editor additionalProperties: false AdminResetPasswordRequest: type: object required: - new_password properties: new_password: type: string minLength: 8 additionalProperties: false ChangeOwnPasswordRequest: type: object required: - current_password - new_password properties: current_password: type: string new_password: type: string minLength: 8 additionalProperties: false InviteUserRequest: type: object required: - email - role properties: email: type: string format: email role: type: string enum: - admin - editor app_base_url: type: string description: Optional base URL for the invite link (defaults to request origin). additionalProperties: false AcceptInviteRequest: type: object required: - token - password properties: token: type: string password: type: string minLength: 8 additionalProperties: false PasswordResetRequest: type: object required: - email properties: email: type: string format: email app_base_url: type: string description: Optional base URL for the reset link (defaults to request origin). additionalProperties: false PasswordResetConfirmRequest: type: object required: - token - new_password properties: token: type: string new_password: type: string minLength: 8 additionalProperties: false HealthResponse: type: object required: - status - service properties: status: type: string enum: - ok - degraded service: type: string ProblemDetails: type: object required: - type - title - status - instance properties: type: type: string format: uri title: type: string status: type: integer detail: type: string instance: type: string ValidationProblemDetails: allOf: - $ref: '#/components/schemas/ProblemDetails' - type: object required: - errors properties: errors: type: array items: $ref: '#/components/schemas/ValidationError' ValidationError: type: object required: - field - message - code properties: field: type: string message: type: string code: type: string EntityTypeResponse: type: object required: - id - name - slug - is_pinned properties: id: type: integer format: int64 name: type: string slug: type: string is_pinned: type: boolean permalink_pattern: type: string nullable: true description: 'URL pattern for public records. Tokens: {type} {slug} {id} {year} {month} {day}. Null = default "/{type}/{id}".' example: '/{type}/{slug}' previous_permalink_pattern: type: string nullable: true description: 'Previous URL pattern, automatically saved when permalink_pattern changes. Used to redirect old URLs.' CreateEntityTypeRequest: type: object required: - name - slug properties: name: type: string minLength: 1 slug: type: string pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' is_pinned: type: boolean additionalProperties: false UpdateEntityTypeRequest: type: object required: - name - slug properties: name: type: string minLength: 1 slug: type: string pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' is_pinned: type: boolean labels: type: object additionalProperties: type: string permalink_pattern: type: string nullable: true description: 'URL pattern. Must contain {slug} or {id}. Must start with /.' additionalProperties: false EntityTypeListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/EntityTypeResponse' limit: type: integer offset: type: integer EntityStatus: type: string enum: - draft - published - archived - scheduled MediaResponse: type: object required: - id - url - original_name - mime_type - size - created_at properties: id: type: integer format: int64 url: type: string minLength: 1 original_name: type: string minLength: 1 mime_type: type: string minLength: 1 size: type: integer minimum: 0 created_at: type: string format: date-time MediaListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/MediaResponse' EntityResponse: type: object required: - id - entity_type_id - slug - status - published_at - scheduled_at - is_deleted - deleted_at - meta_title - meta_description - created_at - updated_at properties: id: type: integer format: int64 entity_type_id: type: integer format: int64 slug: type: - string - 'null' minLength: 1 status: $ref: '#/components/schemas/EntityStatus' published_at: type: - string - 'null' format: date-time scheduled_at: type: - string - 'null' format: date-time is_deleted: type: boolean deleted_at: type: - string - 'null' format: date-time meta_title: type: - string - 'null' maxLength: 255 meta_description: type: - string - 'null' created_at: type: - string - 'null' format: date-time updated_at: type: - string - 'null' format: date-time CreateEntityRequest: type: object required: - entity_type_id properties: entity_type_id: type: integer format: int64 minimum: 1 slug: type: string minLength: 1 status: $ref: '#/components/schemas/EntityStatus' additionalProperties: false UpdateEntityRequest: type: object required: - entity_type_id properties: entity_type_id: type: integer format: int64 minimum: 1 slug: type: string minLength: 1 status: $ref: '#/components/schemas/EntityStatus' published_at: type: - string - 'null' format: date-time scheduled_at: type: - string - 'null' format: date-time meta_title: type: - string - 'null' maxLength: 255 meta_description: type: - string - 'null' additionalProperties: false ScheduleEntityRequest: type: object required: - scheduled_at properties: scheduled_at: type: string format: date-time description: Future ISO 8601 datetime when the entity will be auto-published. additionalProperties: false ScheduleEntityResponse: type: object required: - id - status - scheduled_at properties: id: type: integer format: int64 status: $ref: '#/components/schemas/EntityStatus' scheduled_at: type: string format: date-time ProcessScheduledPublishResponse: type: object required: - published_count - published_ids properties: published_count: type: integer minimum: 0 published_ids: type: array items: type: integer format: int64 EntityListResponse: type: object required: - items - limit - offset - total properties: items: type: array items: $ref: '#/components/schemas/EntityResponse' limit: type: integer offset: type: integer total: type: integer minimum: 0 TagResponse: type: object required: - id - slug - name properties: id: type: integer format: int64 slug: type: string name: type: string CreateTagRequest: type: object required: - name - slug properties: name: type: string minLength: 1 slug: type: string pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' additionalProperties: false UpdateTagRequest: $ref: '#/components/schemas/CreateTagRequest' TagListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/TagResponse' limit: type: integer offset: type: integer EntityTagListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/TagResponse' AttachEntityTagRequest: type: object required: - tag_id properties: tag_id: type: integer format: int64 minimum: 1 additionalProperties: false FieldDefResponse: type: object required: - id - entity_type_id - field_key - data_type properties: id: type: integer format: int64 entity_type_id: type: integer format: int64 field_key: type: string data_type: type: string enum: - text - markdown - int - enum - bool - datetime - image - file - relation target_entity_type_id: type: integer format: int64 minimum: 1 cardinality: type: string enum: - one - many CreateFieldDefRequest: type: object required: - entity_type_id - field_key - data_type properties: entity_type_id: type: integer format: int64 minimum: 1 field_key: type: string minLength: 1 data_type: type: string enum: - text - markdown - int - enum - bool - datetime - image - file - relation target_entity_type_id: type: integer format: int64 minimum: 1 cardinality: type: string enum: - one - many additionalProperties: false UpdateFieldDefRequest: $ref: '#/components/schemas/CreateFieldDefRequest' FieldDefListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/FieldDefResponse' limit: type: integer offset: type: integer additionalProperties: false EntityRelationListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/EntityRelationItemResponse' EntityRelationItemResponse: type: object required: - field_key - target_entity_id properties: field_key: type: string target_entity_id: type: integer format: int64 minimum: 1 additionalProperties: false AttachEntityRelationRequest: type: object required: - field_key - target_entity_id properties: field_key: type: string minLength: 1 target_entity_id: type: integer format: int64 minimum: 1 additionalProperties: false TextFieldResponse: type: object required: - id - entity_id - field_key - value - locale properties: id: type: integer format: int64 entity_id: type: integer format: int64 field_key: type: string value: type: string locale: type: string nullable: true description: BCP-47 locale code (e.g. "ja", "en-US"). Null indicates the default/global value. CreateTextFieldRequest: type: object required: - entity_id - field_key - value properties: entity_id: type: integer format: int64 minimum: 1 field_key: type: string minLength: 1 value: type: string locale: type: string nullable: true maxLength: 10 description: BCP-47 locale code. Omit for the default/global value. additionalProperties: false UpdateTextFieldRequest: type: object required: - field_key - value properties: field_key: type: string minLength: 1 value: type: string locale: type: string nullable: true maxLength: 10 description: BCP-47 locale code. Omit to keep as the default/global value. additionalProperties: false TextFieldListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/TextFieldResponse' limit: type: integer offset: type: integer IntFieldResponse: type: object required: - id - entity_id - field_key - value properties: id: type: integer format: int64 entity_id: type: integer format: int64 field_key: type: string value: type: integer format: int64 CreateIntFieldRequest: type: object required: - entity_id - field_key - value properties: entity_id: type: integer format: int64 minimum: 1 field_key: type: string minLength: 1 value: type: integer format: int64 additionalProperties: false UpdateIntFieldRequest: type: object required: - field_key - value properties: field_key: type: string minLength: 1 value: type: integer format: int64 additionalProperties: false IntFieldListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/IntFieldResponse' limit: type: integer offset: type: integer EnumFieldResponse: type: object required: - id - entity_id - field_key - value properties: id: type: integer format: int64 entity_id: type: integer format: int64 field_key: type: string value: type: string CreateEnumFieldRequest: type: object required: - entity_id - field_key - value properties: entity_id: type: integer format: int64 minimum: 1 field_key: type: string minLength: 1 value: type: string additionalProperties: false UpdateEnumFieldRequest: type: object required: - field_key - value properties: field_key: type: string minLength: 1 value: type: string additionalProperties: false EnumFieldListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/EnumFieldResponse' limit: type: integer offset: type: integer BoolFieldResponse: type: object required: - id - entity_id - field_key - value properties: id: type: integer format: int64 entity_id: type: integer format: int64 field_key: type: string value: type: boolean CreateBoolFieldRequest: type: object required: - entity_id - field_key - value properties: entity_id: type: integer format: int64 minimum: 1 field_key: type: string minLength: 1 value: type: boolean additionalProperties: false UpdateBoolFieldRequest: type: object required: - field_key - value properties: field_key: type: string minLength: 1 value: type: boolean additionalProperties: false BoolFieldListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/BoolFieldResponse' limit: type: integer offset: type: integer DateTimeFieldResponse: type: object required: - id - entity_id - field_key - value properties: id: type: integer format: int64 entity_id: type: integer format: int64 field_key: type: string value: type: string format: date-time CreateDateTimeFieldRequest: type: object required: - entity_id - field_key - value properties: entity_id: type: integer format: int64 minimum: 1 field_key: type: string minLength: 1 value: type: string format: date-time additionalProperties: false UpdateDateTimeFieldRequest: type: object required: - field_key - value properties: field_key: type: string minLength: 1 value: type: string format: date-time additionalProperties: false DateTimeFieldListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/DateTimeFieldResponse' limit: type: integer offset: type: integer AccessStatsByDateResponse: type: object required: - from - to - items properties: from: type: string format: date to: type: string format: date items: type: array items: $ref: '#/components/schemas/AccessStatsDayItem' additionalProperties: false AccessStatsDayItem: type: object required: - date - request_count - avg_duration_ms properties: date: type: string format: date request_count: type: integer minimum: 0 avg_duration_ms: type: number minimum: 0 additionalProperties: false SettingListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/SettingAdminItemResponse' additionalProperties: false SettingAdminItemResponse: type: object required: - setting_key - label - data_type - is_public - value properties: setting_key: type: string pattern: '^[a-z][a-z0-9_]*$' label: type: string data_type: type: string enum: [text, markdown, bool, url] default_value: type: string nullable: true is_public: type: boolean value: type: string updated_at: type: string format: date-time nullable: true additionalProperties: false PublicSettingListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/PublicSettingItemResponse' additionalProperties: false PublicSettingItemResponse: type: object required: - setting_key - value properties: setting_key: type: string pattern: '^[a-z][a-z0-9_]*$' value: type: string additionalProperties: false UpdateSettingRequest: type: object required: - value properties: value: type: string additionalProperties: false SettingValueResponse: type: object required: - setting_key - value - updated_at properties: setting_key: type: string pattern: '^[a-z][a-z0-9_]*$' value: type: string updated_at: type: string format: date-time additionalProperties: false EntityRevisionListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/EntityRevisionResponse' limit: type: integer offset: type: integer additionalProperties: false EntityRevisionResponse: type: object required: - id - entity_id - action - status - created_at properties: id: type: integer minimum: 1 entity_id: type: integer minimum: 1 action: type: string enum: [created, updated, deleted, restored] status: type: string enum: [draft, published, archived] previous_status: type: string nullable: true enum: [draft, published, archived] slug: type: string nullable: true previous_slug: type: string nullable: true actor_user_id: type: integer nullable: true created_at: type: string format: date-time additionalProperties: false SettingRevisionListResponse: type: object required: - items - limit - offset properties: items: type: array items: $ref: '#/components/schemas/SettingRevisionResponse' limit: type: integer offset: type: integer additionalProperties: false SettingRevisionResponse: type: object required: - id - setting_key - action - created_at properties: id: type: integer minimum: 1 setting_key: type: string pattern: '^[a-z][a-z0-9_]*$' value: type: string nullable: true previous_value: type: string nullable: true action: type: string enum: [created, updated, deleted, restored] actor_user_id: type: integer nullable: true created_at: type: string format: date-time additionalProperties: false PublicRecordViewResponse: type: object required: - entityTypeSlug - entityTypeId - entityId - entityTypes - entity - fieldDefs - textFields - intFields - enumFields - boolFields - dateTimeFields - entityRelations - relationTextFieldsByEntityTypeId - publicSettings properties: entityTypeSlug: type: string minLength: 1 entityTypeId: type: integer minimum: 1 entityId: type: integer minimum: 1 entityTypes: $ref: '#/components/schemas/EntityTypeListResponse' entity: $ref: '#/components/schemas/EntityResponse' fieldDefs: $ref: '#/components/schemas/FieldDefListResponse' textFields: $ref: '#/components/schemas/TextFieldListResponse' intFields: $ref: '#/components/schemas/IntFieldListResponse' enumFields: $ref: '#/components/schemas/EnumFieldListResponse' boolFields: $ref: '#/components/schemas/BoolFieldListResponse' dateTimeFields: $ref: '#/components/schemas/DateTimeFieldListResponse' entityRelations: type: array items: $ref: '#/components/schemas/PublicRecordRelationQuery' relationTextFieldsByEntityTypeId: type: object additionalProperties: $ref: '#/components/schemas/TextFieldListResponse' publicSettings: $ref: '#/components/schemas/PublicSettingListResponse' additionalProperties: false PublicRecordRelationQuery: type: object required: - fieldKey - items properties: fieldKey: type: string minLength: 1 items: type: array items: $ref: '#/components/schemas/EntityRelationItemResponse' additionalProperties: false LoginRequest: type: object required: - email - password properties: email: type: string format: email minLength: 1 password: type: string minLength: 1 additionalProperties: false LoginResponse: type: object required: - token - expires_at - email - role properties: token: type: string minLength: 1 expires_at: type: string format: date-time email: type: string format: email role: type: string minLength: 1 additionalProperties: false NavigationItemResponse: type: object required: - id - label - url - display_order - created_at - updated_at properties: id: type: integer minimum: 1 label: type: string minLength: 1 url: type: string minLength: 1 display_order: type: integer minimum: 0 created_at: type: string format: date-time updated_at: type: string format: date-time additionalProperties: false NavigationItemListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/NavigationItemResponse' additionalProperties: false CreateNavigationItemRequest: type: object required: - label - url properties: label: type: string minLength: 1 url: type: string minLength: 1 display_order: type: integer minimum: 0 default: 0 additionalProperties: false UpdateNavigationItemRequest: type: object required: - label - url properties: label: type: string minLength: 1 url: type: string minLength: 1 display_order: type: integer minimum: 0 default: 0 additionalProperties: false WebhookResponse: type: object required: - id - url - events - is_active - created_at - updated_at properties: id: type: integer format: int64 url: type: string events: type: array items: type: string enum: - entity.created - entity.updated - entity.deleted entity_type_id: type: integer format: int64 nullable: true secret: type: string nullable: true is_active: type: boolean created_at: type: string format: date-time updated_at: type: string format: date-time additionalProperties: false WebhookListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/WebhookResponse' additionalProperties: false CreateWebhookRequest: type: object required: - url - events properties: url: type: string format: uri minLength: 1 events: type: array minItems: 1 items: type: string enum: - entity.created - entity.updated - entity.deleted entity_type_id: type: integer format: int64 minimum: 1 nullable: true secret: type: string nullable: true is_active: type: boolean default: true additionalProperties: false UpdateWebhookRequest: type: object required: - url - events properties: url: type: string format: uri minLength: 1 events: type: array minItems: 1 items: type: string enum: - entity.created - entity.updated - entity.deleted entity_type_id: type: integer format: int64 minimum: 1 nullable: true secret: type: string nullable: true is_active: type: boolean additionalProperties: false DashboardSummaryResponse: type: object required: - recent_published - today_access_count - this_month_access_count - entity_type_summary properties: recent_published: type: array items: $ref: '#/components/schemas/DashboardRecentEntity' today_access_count: type: integer minimum: 0 this_month_access_count: type: integer minimum: 0 entity_type_summary: type: array items: $ref: '#/components/schemas/DashboardEntityTypeSummary' additionalProperties: false DashboardRecentEntity: type: object required: - id - entity_type_id - entity_type_name - entity_type_slug - slug - published_at properties: id: type: integer format: int64 minimum: 1 entity_type_id: type: integer format: int64 minimum: 1 entity_type_name: type: string entity_type_slug: type: string slug: type: string nullable: true published_at: type: string format: date-time nullable: true additionalProperties: false DashboardEntityTypeSummary: type: object required: - entity_type_id - entity_type_name - entity_type_slug - published_count - draft_count properties: entity_type_id: type: integer format: int64 minimum: 1 entity_type_name: type: string entity_type_slug: type: string published_count: type: integer minimum: 0 draft_count: type: integer minimum: 0 additionalProperties: false PostCommentRequest: type: object required: - author_name - author_email - body properties: author_name: type: string maxLength: 100 author_email: type: string format: email maxLength: 255 body: type: string additionalProperties: false CommentResponse: type: object required: - id - entity_id - author_name - body - is_approved - created_at properties: id: type: integer entity_id: type: integer author_name: type: string body: type: string is_approved: type: boolean created_at: type: string format: date-time additionalProperties: false CommentListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/CommentResponse' additionalProperties: false AdminCommentResponse: type: object required: - id - entity_id - author_name - author_email - body - is_approved - created_at properties: id: type: integer entity_id: type: integer author_name: type: string author_email: type: string body: type: string is_approved: type: boolean created_at: type: string format: date-time additionalProperties: false AdminCommentListResponse: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/AdminCommentResponse' additionalProperties: false GeneratePreviewTokenResponse: type: object required: - token - expires_at - preview_url properties: token: type: string description: Opaque 64-character hex token. expires_at: type: string format: date-time description: ISO 8601 expiry timestamp (24 hours from generation). preview_url: type: string description: Relative API path to fetch the preview record. additionalProperties: false