Base URL: http://localhost:8081
Swagger UI: http://localhost:8081/swagger-ui/index.html
OpenAPI JSON: http://localhost:8081/v3/api-docs
All endpoints return a standardized response:
{
"code": 0,
"message": "Success",
"data": { ... }
}Error response:
{
"code": 400,
"message": "Error description",
"data": null
}Every response may include correlation header:
X-Trace-Id: <trace-id>Use this value to correlate client-side errors with server logs/traces (including 401, 403, and 429 responses).
| Code | Meaning |
|---|---|
| 0 | Success |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
DPoP support:
- Auth endpoints accept optional header
DPoP: <proof-jwt>and forward it to Keycloak. - Protected endpoints support both:
Authorization: Bearer <access_token>Authorization: DPoP <access_token>+DPoP: <proof-jwt>
- If token introspection contains
cnf.jkt, DPoP proof is mandatory.
Authenticate with username/password and client credentials.
Optional Header:
DPoP: <proof-jwt>Request Body:
{
"username": "user",
"password": "password",
"clientId": "spring-app",
"clientSecret": "your-client-secret"
}Success Response (200):
{
"code": 0,
"message": "Success",
"data": {
"access_token": "eyJhbG...",
"refresh_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 300
}
}Error Responses:
400— Validation error (missing fields)401— Invalid credentials429— Rate limit exceeded (5 requests/minute)
Refresh an expired access token.
Optional Header:
DPoP: <proof-jwt>Request Body:
{
"refreshToken": "eyJhbG...",
"clientId": "spring-app",
"clientSecret": "your-client-secret"
}Success Response (200):
{
"code": 0,
"message": "Success",
"data": {
"access_token": "eyJhbG...",
"refresh_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 300
}
}Error Responses:
400— Validation error401— Invalid or expired refresh token
Revoke a refresh token.
Optional Header:
DPoP: <proof-jwt>Request Body:
{
"refreshToken": "eyJhbG...",
"clientId": "spring-app",
"clientSecret": "your-client-secret"
}Success Response (200):
{
"code": 0,
"message": "Logout successful",
"data": null
}ℹ️ Keycloak 26 always returns 200 for logout, even with invalid tokens.
All client endpoints require a valid access token in the Authorization header:
Authorization: Bearer <access_token>
Or DPoP:
Authorization: DPoP <access_token>
DPoP: <proof-jwt>
Accept a new asynchronous client creation request.
Required Role: CLIENT_CREATE
Request Body:
{
"firstName": "John",
"lastName": "Doe",
"phone": "+37061234567"
}Validation Rules:
| Field | Rules |
|---|---|
| firstName | Required, 1–50 characters |
| lastName | Required, 1–50 characters |
| phone | Required, must match pattern +[0-9]{7,15} |
Success Response (202):
{
"code": 0,
"message": "OK",
"data": {
"requestId": "2e6a42a8-8bb7-4f7d-b4d6-71eb31ec8a13",
"status": "PENDING"
}
}After that, poll GET /api/requests/{requestId} until the request reaches COMPLETED or FAILED.
Error Responses:
400— Validation error401— Missing or invalid token403— Insufficient role429— Rate limit exceeded (20 requests/minute)
Get asynchronous request status and final response payload when processing is complete.
Required Role: CLIENT_CREATE
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | UUID | Request identifier |
Success Response (200), still processing:
{
"code": 0,
"message": "OK",
"data": {
"requestId": "2e6a42a8-8bb7-4f7d-b4d6-71eb31ec8a13",
"type": "CLIENT_CREATE",
"status": "PROCESSING",
"createdAt": "2026-03-19T12:00:00Z",
"statusChangedAt": "2026-03-19T12:00:01Z",
"response": null
}
}Success Response (200), processed:
{
"code": 0,
"message": "OK",
"data": {
"requestId": "2e6a42a8-8bb7-4f7d-b4d6-71eb31ec8a13",
"type": "CLIENT_CREATE",
"status": "COMPLETED",
"createdAt": "2026-03-19T12:00:00Z",
"statusChangedAt": "2026-03-19T12:00:02Z",
"response": {
"code": 0,
"message": "OK",
"data": {
"id": 1,
"firstName": "John",
"lastName": "Doe",
"phone": "+37061234567"
}
}
}
}Error Responses:
400— Invalid UUID format401— Missing or invalid token403— Insufficient role404— Request not found
Get a client by ID.
Required Role: CLIENT_GET
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | Long | Client identifier |
Success Response (200):
{
"code": 0,
"message": "Success",
"data": {
"id": 1,
"firstName": "John",
"lastName": "Doe",
"phone": "+37061234567"
}
}Error Responses:
400— Invalid ID format401— Missing or invalid token403— Insufficient role404— Client not found
Search clients by first name or last name (case-insensitive).
Required Role: CLIENT_SEARCH
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| q | String | Search term, minimum 3 characters |
Returned rows are limited by configuration property app.clients.search.max-results.
Success Response (200):
{
"code": 0,
"message": "OK",
"data": [
{
"id": 1,
"firstName": "Alice",
"lastName": "Brown",
"phone": "+37070000001"
}
]
}Error Responses:
400— Query shorter than 3 characters401— Missing or invalid token403— Insufficient role
Update account balance using pessimistic locking.
Required Role: UPDATE_BALANCE
Request Body:
{
"clientId": 1,
"amount": 100.50
}Success Response (200):
{
"code": 0,
"message": "Success",
"data": {
"accountId": 1,
"clientId": 1,
"balance": 150.50
}
}Error Responses:
400— Validation error401— Missing or invalid token403— Insufficient role404— Account not found
Update account balance using optimistic locking with retries.
Required Role: UPDATE_BALANCE
Request Body:
{
"clientId": 1,
"amount": -50.00
}Success Response (200):
{
"code": 0,
"message": "Success",
"data": {
"accountId": 1,
"clientId": 1,
"balance": 100.50
}
}Error Responses:
400— Validation error401— Missing or invalid token403— Insufficient role404— Account not found409— Optimistic lock conflict
Get account by client id.
Required Role: CLIENT_GET
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
| clientId | Long | Client identifier |
Success Response (200):
{
"code": 0,
"message": "Success",
"data": {
"accountId": 1,
"clientId": 1,
"balance": 100.50
}
}Error Responses:
400— Invalid ID format401— Missing or invalid token403— Insufficient role404— Account not found
The API supports multiple languages via the Accept-Language header.
Supported Languages:
| Code | Language |
|---|---|
en |
English |
ru |
Russian |
Example:
GET /api/clients/999999 HTTP/1.1
Authorization: Bearer <token>
Accept-Language: ruResponse:
{
"code": 404,
"message": "Клиент с id=999999 не найден",
"data": null
}| Endpoint | Description |
|---|---|
GET /actuator/health |
Application health check |
GET /actuator/prometheus |
Prometheus metrics |
| Endpoint | Limit |
|---|---|
/api/auth/login |
5 requests/minute |
/api/clients.* |
20 requests/minute |
When rate limit is exceeded, the API returns HTTP 429 Too Many Requests.
Typical response headers for a throttled request include X-Trace-Id, which can be used for diagnostics:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-Trace-Id: 2f0a3e58a2d7f97c3f6d9d6cc2b1aa93