Add RSwag coverage for /chat and /transactions API endpoints (#210)

* Add RSwag coverage for chat API

* Linter

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>

* Add transaction rswag

* FIX linter

---------

Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: sokie <sokysrm@gmail.com>
This commit is contained in:
Juan José Mata
2025-12-17 14:14:17 +01:00
committed by GitHub
parent 5f8a295479
commit 9d54719007
12 changed files with 2274 additions and 203 deletions

View File

@@ -1,228 +1,59 @@
# Chat API Documentation
The Chat API allows external applications to interact with Sure's AI chat functionality.
The Chat API allows external applications to interact with Sure's AI chat functionality. The OpenAPI description is generated directly from executable request specs, ensuring it always reflects the behaviour of the running Rails application.
## Authentication
## Generated OpenAPI specification
All chat endpoints require authentication via OAuth2 or API keys. The chat endpoints also require the user to have AI features enabled (`ai_enabled: true`).
- The source of truth for the documentation lives in [`spec/requests/api/v1/chats_spec.rb`](../../spec/requests/api/v1/chats_spec.rb). These specs authenticate against the Rails stack, exercise every chat endpoint, and capture real response shapes.
- Regenerate the OpenAPI document with:
## Endpoints
```sh
RAILS_ENV=test bundle exec rake rswag:specs:swaggerize
```
### List Chats
```
GET /api/v1/chats
```
The task compiles the request specs and writes the result to [`docs/api/openapi.yaml`](openapi.yaml).
**Required Scope:** `read`
- Run just the documentation specs with:
**Response:**
```json
{
"chats": [
{
"id": "uuid",
"title": "Chat title",
"last_message_at": "2024-01-01T00:00:00Z",
"message_count": 5,
"error": null,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total_count": 50,
"total_pages": 3
}
}
```
```sh
bundle exec rspec spec/requests/api/v1/chats_spec.rb
```
### Get Chat
```
GET /api/v1/chats/:id
```
## Authentication requirements
**Required Scope:** `read`
All chat endpoints require an OAuth2 access token or API key that grants the appropriate scope. The authenticated user must also have AI features enabled (`ai_enabled: true`).
**Response:**
```json
{
"id": "uuid",
"title": "Chat title",
"error": null,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"messages": [
{
"id": "uuid",
"type": "user_message",
"role": "user",
"content": "Hello AI",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
},
{
"id": "uuid",
"type": "assistant_message",
"role": "assistant",
"content": "Hello! How can I help you?",
"model": "gpt-4",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"tool_calls": []
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total_count": 2,
"total_pages": 1
}
}
```
## Available endpoints
### Create Chat
```
POST /api/v1/chats
```
| Endpoint | Scope | Description |
| --- | --- | --- |
| `GET /api/v1/chats` | `read` | List chats for the authenticated user with pagination metadata. |
| `GET /api/v1/chats/{id}` | `read` | Retrieve a chat, including ordered messages and optional pagination. |
| `POST /api/v1/chats` | `write` | Create a chat and optionally seed it with an initial user message. |
| `PATCH /api/v1/chats/{id}` | `write` | Update a chat title. |
| `DELETE /api/v1/chats/{id}` | `write` | Permanently delete a chat. |
| `POST /api/v1/chats/{chat_id}/messages` | `write` | Append a user message to a chat. |
| `POST /api/v1/chats/{chat_id}/messages/retry` | `write` | Retry the last assistant response in a chat. |
**Required Scope:** `write`
Refer to the generated [`openapi.yaml`](openapi.yaml) for request/response schemas, reusable components (pagination, errors, messages, tool calls), and security definitions.
**Request Body:**
```json
{
"title": "Optional chat title",
"message": "Initial message to AI",
"model": "gpt-4" // optional, defaults to gpt-4
}
```
## AI response behaviour
**Response:** Same as Get Chat endpoint
- Chat creation and message submission queue AI processing jobs asynchronously; the API responds immediately with the user message payload.
- Poll `GET /api/v1/chats/{id}` to detect new assistant messages (`type: "assistant_message"`).
- Supported models today: `gpt-4` (default), `gpt-4-turbo`, and `gpt-3.5-turbo`.
- Assistant responses may include structured tool calls (`tool_calls`) that reference financial data fetches and their results.
### Update Chat
```
PATCH /api/v1/chats/:id
```
## Error responses
**Required Scope:** `write`
**Request Body:**
```json
{
"title": "New chat title"
}
```
**Response:** Same as Get Chat endpoint
### Delete Chat
```
DELETE /api/v1/chats/:id
```
**Required Scope:** `write`
**Response:** 204 No Content
### Create Message
```
POST /api/v1/chats/:chat_id/messages
```
**Required Scope:** `write`
**Request Body:**
```json
{
"content": "User message",
"model": "gpt-4" // optional, defaults to gpt-4
}
```
**Response:**
```json
{
"id": "uuid",
"chat_id": "uuid",
"type": "user_message",
"role": "user",
"content": "User message",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"ai_response_status": "pending",
"ai_response_message": "AI response is being generated"
}
```
### Retry Last Message
```
POST /api/v1/chats/:chat_id/messages/retry
```
**Required Scope:** `write`
Retries the last assistant message in the chat.
**Response:**
```json
{
"message": "Retry initiated",
"message_id": "uuid"
}
```
## AI Response Handling
AI responses are processed asynchronously. When you create a message or chat with an initial message, the API returns immediately with the user message. The AI response is generated in the background.
### Checking for AI Responses
Currently, you need to poll the chat endpoint to check for new AI responses. Look for new messages with `type: "assistant_message"`.
### Available AI Models
- `gpt-4` (default)
- `gpt-4-turbo`
- `gpt-3.5-turbo`
### Tool Calls
The AI assistant can make tool calls to access user financial data. These appear in the `tool_calls` array of assistant messages:
```json
{
"tool_calls": [
{
"id": "uuid",
"function_name": "get_accounts",
"function_arguments": {},
"function_result": { ... },
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
## Error Handling
All endpoints return standard error responses:
Errors conform to the shared `ErrorResponse` schema in the OpenAPI document:
```json
{
"error": "error_code",
"message": "Human readable error message",
"details": ["Additional error details"] // optional
"details": ["Optional array of extra context"]
}
```
Common error codes:
- `unauthorized` - Invalid or missing authentication
- `forbidden` - Insufficient permissions or AI not enabled
- `not_found` - Resource not found
- `unprocessable_entity` - Invalid request data
- `rate_limit_exceeded` - Too many requests
## Rate Limits
Chat API endpoints are subject to the standard API rate limits based on your API key tier.
Common error codes include `unauthorized`, `forbidden`, `feature_disabled`, `not_found`, `unprocessable_entity`, and `rate_limit_exceeded`.

888
docs/api/openapi.yaml Normal file
View File

@@ -0,0 +1,888 @@
---
openapi: 3.0.3
info:
title: Sure API
version: v1
description: OpenAPI documentation generated from executable request specs.
servers:
- url: https://api.sure.app
description: Production
- url: http://localhost:3000
description: Local development
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Pagination:
type: object
required:
- page
- per_page
- total_count
- total_pages
properties:
page:
type: integer
minimum: 1
per_page:
type: integer
minimum: 1
total_count:
type: integer
minimum: 0
total_pages:
type: integer
minimum: 0
ErrorResponse:
type: object
required:
- error
properties:
error:
type: string
message:
type: string
nullable: true
details:
oneOf:
- type: array
items:
type: string
- type: object
nullable: true
ToolCall:
type: object
required:
- id
- function_name
- function_arguments
- created_at
properties:
id:
type: string
format: uuid
function_name:
type: string
function_arguments:
type: object
additionalProperties: true
function_result:
type: object
additionalProperties: true
nullable: true
created_at:
type: string
format: date-time
Message:
type: object
required:
- id
- type
- role
- content
- created_at
- updated_at
properties:
id:
type: string
format: uuid
type:
type: string
enum:
- user_message
- assistant_message
role:
type: string
enum:
- user
- assistant
content:
type: string
model:
type: string
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
tool_calls:
type: array
items:
"$ref": "#/components/schemas/ToolCall"
nullable: true
MessageResponse:
allOf:
- "$ref": "#/components/schemas/Message"
- type: object
required:
- chat_id
properties:
chat_id:
type: string
format: uuid
ai_response_status:
type: string
enum:
- pending
- complete
- failed
nullable: true
ai_response_message:
type: string
nullable: true
ChatResource:
type: object
required:
- id
- title
- created_at
- updated_at
properties:
id:
type: string
format: uuid
title:
type: string
error:
type: string
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
ChatSummary:
allOf:
- "$ref": "#/components/schemas/ChatResource"
- type: object
required:
- message_count
properties:
message_count:
type: integer
minimum: 0
last_message_at:
type: string
format: date-time
nullable: true
ChatDetail:
allOf:
- "$ref": "#/components/schemas/ChatResource"
- type: object
required:
- messages
properties:
messages:
type: array
items:
"$ref": "#/components/schemas/Message"
pagination:
"$ref": "#/components/schemas/Pagination"
nullable: true
ChatCollection:
type: object
required:
- chats
- pagination
properties:
chats:
type: array
items:
"$ref": "#/components/schemas/ChatSummary"
pagination:
"$ref": "#/components/schemas/Pagination"
RetryResponse:
type: object
required:
- message
- message_id
properties:
message:
type: string
message_id:
type: string
format: uuid
Account:
type: object
required:
- id
- name
- account_type
properties:
id:
type: string
format: uuid
name:
type: string
account_type:
type: string
Category:
type: object
required:
- id
- name
- classification
- color
- icon
properties:
id:
type: string
format: uuid
name:
type: string
classification:
type: string
color:
type: string
icon:
type: string
Merchant:
type: object
required:
- id
- name
properties:
id:
type: string
format: uuid
name:
type: string
Tag:
type: object
required:
- id
- name
- color
properties:
id:
type: string
format: uuid
name:
type: string
color:
type: string
Transfer:
type: object
required:
- id
- amount
- currency
properties:
id:
type: string
format: uuid
amount:
type: string
currency:
type: string
other_account:
"$ref": "#/components/schemas/Account"
nullable: true
Transaction:
type: object
required:
- id
- date
- amount
- currency
- name
- classification
- account
- tags
- created_at
- updated_at
properties:
id:
type: string
format: uuid
date:
type: string
format: date
amount:
type: string
currency:
type: string
name:
type: string
notes:
type: string
nullable: true
classification:
type: string
account:
"$ref": "#/components/schemas/Account"
category:
"$ref": "#/components/schemas/Category"
nullable: true
merchant:
"$ref": "#/components/schemas/Merchant"
nullable: true
tags:
type: array
items:
"$ref": "#/components/schemas/Tag"
transfer:
"$ref": "#/components/schemas/Transfer"
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
TransactionCollection:
type: object
required:
- transactions
- pagination
properties:
transactions:
type: array
items:
"$ref": "#/components/schemas/Transaction"
pagination:
"$ref": "#/components/schemas/Pagination"
DeleteResponse:
type: object
required:
- message
properties:
message:
type: string
paths:
"/api/v1/chats":
get:
summary: List chats
tags:
- Chats
security:
- bearerAuth: []
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
responses:
'200':
description: chats listed
content:
application/json:
schema:
"$ref": "#/components/schemas/ChatCollection"
'403':
description: AI features disabled
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
post:
summary: Create chat
tags:
- Chats
security:
- bearerAuth: []
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
responses:
'201':
description: chat created
content:
application/json:
schema:
"$ref": "#/components/schemas/ChatDetail"
'422':
description: validation error
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
title:
type: string
example: Monthly budget review
message:
type: string
description: Initial message in the chat
model:
type: string
description: Optional OpenAI model identifier
required:
- title
- message
required: true
"/api/v1/chats/{id}":
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: id
in: path
required: true
description: Chat ID
schema:
type: string
get:
summary: Retrieve a chat
tags:
- Chats
security:
- bearerAuth: []
responses:
'200':
description: chat retrieved
content:
application/json:
schema:
"$ref": "#/components/schemas/ChatDetail"
'404':
description: chat not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
patch:
summary: Update a chat
tags:
- Chats
security:
- bearerAuth: []
parameters: []
responses:
'200':
description: chat updated
content:
application/json:
schema:
"$ref": "#/components/schemas/ChatDetail"
'404':
description: chat not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
'422':
description: validation error
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
title:
type: string
example: Updated chat title
required: true
delete:
summary: Delete a chat
tags:
- Chats
security:
- bearerAuth: []
responses:
'204':
description: chat deleted
'404':
description: chat not found
"/api/v1/chats/{chat_id}/messages":
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
- name: chat_id
in: path
required: true
description: Chat ID
schema:
type: string
post:
summary: Create a message
tags:
- Chat Messages
security:
- bearerAuth: []
parameters: []
responses:
'201':
description: message created
content:
application/json:
schema:
"$ref": "#/components/schemas/MessageResponse"
'404':
description: chat not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
'422':
description: validation error
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
content:
type: string
model:
type: string
required:
- content
required: true
"/api/v1/chats/{chat_id}/messages/retry":
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
- name: chat_id
in: path
required: true
description: Chat ID
schema:
type: string
post:
summary: Retry the last assistant response
tags:
- Chat Messages
security:
- bearerAuth: []
responses:
'202':
description: retry started
content:
application/json:
schema:
"$ref": "#/components/schemas/RetryResponse"
'404':
description: chat not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
'422':
description: no assistant message available
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
"/api/v1/transactions":
get:
summary: List transactions
tags:
- Transactions
security:
- bearerAuth: []
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: page
in: query
required: false
description: 'Page number (default: 1)'
schema:
type: integer
- name: per_page
in: query
required: false
description: 'Items per page (default: 25, max: 100)'
schema:
type: integer
- name: account_id
in: query
required: false
description: Filter by account ID
schema:
type: string
- name: category_id
in: query
required: false
description: Filter by category ID
schema:
type: string
- name: merchant_id
in: query
required: false
description: Filter by merchant ID
schema:
type: string
- name: start_date
in: query
format: date
required: false
description: Filter transactions from this date
schema:
type: string
- name: end_date
in: query
format: date
required: false
description: Filter transactions until this date
schema:
type: string
- name: min_amount
in: query
required: false
description: Filter by minimum amount
schema:
type: number
- name: max_amount
in: query
required: false
description: Filter by maximum amount
schema:
type: number
- name: type
in: query
enum:
- income
- expense
required: false
description: "Filter by transaction type:\n * `income` \n * `expense` \n "
schema:
type: string
- name: search
in: query
required: false
description: Search by name, notes, or merchant name
schema:
type: string
responses:
'200':
description: transactions filtered by date range
content:
application/json:
schema:
"$ref": "#/components/schemas/TransactionCollection"
post:
summary: Create transaction
tags:
- Transactions
security:
- bearerAuth: []
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
responses:
'201':
description: transaction created
content:
application/json:
schema:
"$ref": "#/components/schemas/Transaction"
'422':
description: validation error - missing required fields
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
transaction:
type: object
properties:
account_id:
type: string
format: uuid
description: Account ID (required)
date:
type: string
format: date
description: Transaction date
amount:
type: number
description: Transaction amount
name:
type: string
description: Transaction name/description
notes:
type: string
description: Additional notes
currency:
type: string
description: Currency code (defaults to family currency)
category_id:
type: string
format: uuid
description: Category ID
merchant_id:
type: string
format: uuid
description: Merchant ID
nature:
type: string
enum:
- income
- expense
- inflow
- outflow
description: Transaction nature (determines sign)
tag_ids:
type: array
items:
type: string
format: uuid
description: Array of tag IDs
required:
- account_id
- date
- amount
- name
required:
- transaction
required: true
"/api/v1/transactions/{id}":
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token
- name: id
in: path
required: true
description: Transaction ID
schema:
type: string
get:
summary: Retrieve a transaction
tags:
- Transactions
security:
- bearerAuth: []
responses:
'200':
description: transaction retrieved
content:
application/json:
schema:
"$ref": "#/components/schemas/Transaction"
'404':
description: transaction not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
patch:
summary: Update a transaction
tags:
- Transactions
security:
- bearerAuth: []
parameters: []
responses:
'200':
description: transaction updated
content:
application/json:
schema:
"$ref": "#/components/schemas/Transaction"
'404':
description: transaction not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
transaction:
type: object
properties:
date:
type: string
format: date
amount:
type: number
name:
type: string
notes:
type: string
category_id:
type: string
format: uuid
merchant_id:
type: string
format: uuid
nature:
type: string
enum:
- income
- expense
- inflow
- outflow
tag_ids:
type: array
items:
type: string
format: uuid
required: true
delete:
summary: Delete a transaction
tags:
- Transactions
security:
- bearerAuth: []
responses:
'200':
description: transaction deleted
content:
application/json:
schema:
"$ref": "#/components/schemas/DeleteResponse"
'404':
description: transaction not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"

159
docs/api/transactions.md Normal file
View File

@@ -0,0 +1,159 @@
# Transactions API Documentation
The Transactions API allows external applications to manage financial transactions within Sure. The OpenAPI description is generated directly from executable request specs, ensuring it always reflects the behaviour of the running Rails application.
## Generated OpenAPI specification
- The source of truth for the documentation lives in [`spec/requests/api/v1/transactions_spec.rb`](../../spec/requests/api/v1/transactions_spec.rb). These specs authenticate against the Rails stack, exercise every transaction endpoint, and capture real response shapes.
- Regenerate the OpenAPI document with:
```sh
SWAGGER_DRY_RUN=0 bundle exec rspec spec/requests --format Rswag::Specs::SwaggerFormatter
```
The task compiles the request specs and writes the result to [`docs/api/openapi.yaml`](openapi.yaml).
- Run just the documentation specs with:
```sh
bundle exec rspec spec/requests/api/v1/transactions_spec.rb
```
## Authentication requirements
All transaction endpoints require an OAuth2 access token or API key that grants the appropriate scope (`read` or `read_write`).
## Available endpoints
| Endpoint | Scope | Description |
| --- | --- | --- |
| `GET /api/v1/transactions` | `read` | List transactions with filtering and pagination. |
| `GET /api/v1/transactions/{id}` | `read` | Retrieve a single transaction with full details. |
| `POST /api/v1/transactions` | `write` | Create a new transaction. |
| `PATCH /api/v1/transactions/{id}` | `write` | Update an existing transaction. |
| `DELETE /api/v1/transactions/{id}` | `write` | Permanently delete a transaction. |
Refer to the generated [`openapi.yaml`](openapi.yaml) for request/response schemas, reusable components (pagination, errors, accounts, categories, merchants, tags), and security definitions.
## Filtering options
The `GET /api/v1/transactions` endpoint supports the following query parameters for filtering:
| Parameter | Type | Description |
| --- | --- | --- |
| `page` | integer | Page number (default: 1) |
| `per_page` | integer | Items per page (default: 25, max: 100) |
| `account_id` | uuid | Filter by a single account ID |
| `account_ids[]` | uuid[] | Filter by multiple account IDs |
| `category_id` | uuid | Filter by a single category ID |
| `category_ids[]` | uuid[] | Filter by multiple category IDs |
| `merchant_id` | uuid | Filter by a single merchant ID |
| `merchant_ids[]` | uuid[] | Filter by multiple merchant IDs |
| `tag_ids[]` | uuid[] | Filter by tag IDs |
| `start_date` | date | Filter transactions from this date (inclusive) |
| `end_date` | date | Filter transactions until this date (inclusive) |
| `min_amount` | number | Filter by minimum amount |
| `max_amount` | number | Filter by maximum amount |
| `type` | string | Filter by transaction type: `income` or `expense` |
| `search` | string | Search by name, notes, or merchant name |
## Transaction object
A transaction response includes:
```json
{
"id": "uuid",
"date": "2024-01-15",
"amount": "$75.50",
"currency": "USD",
"name": "Grocery shopping",
"notes": "Weekly groceries",
"classification": "expense",
"account": {
"id": "uuid",
"name": "Checking Account",
"account_type": "depository"
},
"category": {
"id": "uuid",
"name": "Groceries",
"classification": "expense",
"color": "#4CAF50",
"icon": "shopping-cart"
},
"merchant": {
"id": "uuid",
"name": "Whole Foods"
},
"tags": [
{
"id": "uuid",
"name": "Essential",
"color": "#2196F3"
}
],
"transfer": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
```
## Creating transactions
When creating a transaction, the `nature` field determines how the amount is stored:
| Nature | Behaviour |
| --- | --- |
| `income` / `inflow` | Amount is stored as negative (credit) |
| `expense` / `outflow` | Amount is stored as positive (debit) |
Example request body:
```json
{
"transaction": {
"account_id": "uuid",
"date": "2024-01-15",
"amount": 75.50,
"name": "Grocery shopping",
"nature": "expense",
"category_id": "uuid",
"merchant_id": "uuid",
"tag_ids": ["uuid", "uuid"]
}
}
```
## Transfer transactions
If a transaction is part of a transfer between accounts, the `transfer` field will be populated with details about the linked transaction:
```json
{
"transfer": {
"id": "uuid",
"amount": "$500.00",
"currency": "USD",
"other_account": {
"id": "uuid",
"name": "Savings Account",
"account_type": "depository"
}
}
}
```
## Error responses
Errors conform to the shared `ErrorResponse` schema in the OpenAPI document:
```json
{
"error": "error_code",
"message": "Human readable error message",
"errors": ["Optional array of validation errors"]
}
```
Common error codes include `unauthorized`, `not_found`, `validation_failed`, and `internal_server_error`.