Base URL: http://localhost:8081
- Swagger UI:
http://localhost:8081/swagger-ui.html - OpenAPI JSON:
http://localhost:8081/v3/api-docs
All endpoints return AppResponse<T>:
{
"code": 0,
"data": { "...": "..." },
"message": "OK"
}Error envelope:
{
"code": 40001,
"data": null,
"message": "Validation error details"
}The app can attach correlation headers:
X-Trace-Id: <32-char-hex-trace-id>
X-Request-Id: <request-id>Use these values to correlate API errors with logs and traces.
| Code | Meaning |
|---|---|
| 0 | Success |
| 40001 | Bad request |
| 40101 | Unauthorized |
| 40102 | Invalid grant |
| 40103 | Invalid token |
| 40301 | Forbidden |
| 40401 | Not found |
| 40901 | Conflict |
| 42901 | Too many requests |
| 50000 | Internal server error |
DPoP support:
- Auth endpoints accept optional header
DPoP: <proof-jwt>and pass it to Keycloak. - Protected endpoints accept:
Authorization: Bearer <access_token>Authorization: DPoP <access_token>+DPoP: <proof-jwt>
- If introspection payload 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": "<client-secret>"
}Success response (200):
{
"code": 0,
"data": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 300
},
"message": "OK"
}Typical errors:
400+40001validation issues401+40101Keycloak auth failure429+42901rate limit exceeded
Request body:
{
"refreshToken": "eyJ...",
"clientId": "spring-app",
"clientSecret": "<client-secret>"
}Success response (200): same shape as login.
Typical errors:
400+40001validation issues401+40102invalid/expired refresh token
Request body:
{
"refreshToken": "eyJ...",
"clientId": "spring-app",
"clientSecret": "<client-secret>"
}Success response (200):
{
"code": 0,
"data": null,
"message": "OK"
}Typical errors:
400+40001validation issues401+40103invalid token / upstream auth error
Accepted auth schemes for all protected endpoints:
Authorization: Bearer <access_token>or
Authorization: DPoP <access_token>
DPoP: <proof-jwt>Create asynchronous client creation request.
Required role: CLIENT_CREATE
Request body:
{
"firstName": "John",
"lastName": "Doe",
"phone": "+37061234567"
}Validation:
firstName: required, 1..50lastName: required, 1..50phone: required,+[0-9]{7,15}
Success response (202):
{
"code": 0,
"data": {
"requestId": "2e6a42a8-8bb7-4f7d-b4d6-71eb31ec8a13",
"status": "PENDING"
},
"message": "OK"
}Then poll GET /api/requests/{requestId} until terminal status.
Get async request status and final payload (if processed).
Required role: CLIENT_CREATE
Path param:
id: UUID request id
Success response (200, processing):
{
"code": 0,
"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
},
"message": "OK"
}Success response (200, completed):
{
"code": 0,
"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,
"data": {
"id": 1,
"firstName": "John",
"lastName": "Doe",
"phone": "+37061234567"
},
"message": "OK"
}
},
"message": "OK"
}Typical errors:
400+40001invalid UUID format404+40401request not found
Get client by id.
Required role: CLIENT_GET
Success response (200):
{
"code": 0,
"data": {
"id": 1,
"firstName": "John",
"lastName": "Doe",
"phone": "+37061234567"
},
"message": "OK"
}Search by first/last name (case-insensitive).
Required role: CLIENT_SEARCH
Rules:
qminimum length is validated by backend- result size is capped by
app.clients.search.max-results
Success response (200):
{
"code": 0,
"data": [
{
"id": 1,
"firstName": "Alice",
"lastName": "Brown",
"phone": "+37070000001"
}
],
"message": "OK"
}Typical errors:
400+40001query too short
Update account balance with pessimistic lock.
Required role: UPDATE_BALANCE
Request body:
{
"clientId": 1,
"amount": 100.50
}Update account balance with optimistic lock retries.
Required role: UPDATE_BALANCE
Request body:
{
"clientId": 1,
"amount": -50.00
}Typical errors:
404+40401account not found409+40901optimistic lock conflict after retries
Get account by client id.
Required role: CLIENT_GET
Success response shape for account endpoints:
{
"code": 0,
"data": {
"accountId": 1,
"clientId": 1,
"balance": 100.50
},
"message": "OK"
}The API supports language negotiation via Accept-Language.
Supported locales:
enru
Example:
GET /api/clients/999999 HTTP/1.1
Authorization: Bearer <token>
Accept-Language: ru{
"code": 40401,
"data": null,
"message": "Клиент с id=999999 не найден"
}| Endpoint | Access |
|---|---|
GET /actuator/health |
Public |
GET /actuator/info |
Public |
GET /actuator/metrics/** |
Public |
GET /actuator/prometheus |
Public |
Configured rules in application.properties:
| Rule | Endpoint pattern | Limit | Key strategy |
|---|---|---|---|
login |
/api/auth/login (POST) |
5 req / 60 sec | IP |
clients |
/api/clients/** |
20 req / 60 sec | CLIENT_ID (for configured client ids) |
When throttled, API returns 429 with AppResponse.code=42901.