!!! info "Document Metadata" - Title: Payflow REST API Interface & Schema Specification - Author: Payflow Engineering (shashakchandel@gmail.com) - Status: Approved / Living Specification - Created Date: 2026-08-01 - Last Updated: 2026-09-30 - Authoritative Location: API_SPECIFICATION.md - Related Documents: System Architecture | Security Architecture | Architecture Decisions (ADRs) | Phased Roadmap | Engineering Conventions
This document details the REST API endpoints, request/response models, input validation rules, and error handling behaviors for the Payflow API service.
- API Base Prefix: All endpoints are versioned and prefixed with
/api/v1. - Content-Type: All request and response bodies use
application/json. - Monetary Currency & Precision: All monetary values are strictly denominated in Indian Rupees (INR, symbol: ₹) and encoded as exact base-10 decimals with up to 4 decimal places (e.g.
100.0000= ₹100.00). - Pagination: Default page size is 10, with a hard maximum of 100 per page (
@Min(1) @Max(100)). - Authentication: Mutation and secure history endpoints require a cryptographically signed JWT token passed via the
Authorization: Bearer <token>header. - Idempotency: All mutation write operations require a unique identifier passed in the
Idempotency-Keyheader.
Payflow API auto-generates live, interactive OpenAPI 3.0 documentation using Springdoc OpenAPI 3.1.1:
- Swagger UI (Interactive Playground): http://localhost:8080/swagger-ui.html
- OpenAPI 3.0 JSON Specification: http://localhost:8080/v3/api-docs
When an API error occurs (validation error, resource not found, conflict, etc.), the service returns a standardized error payload in compliance with RFC 9457 (which obsoletes RFC 7807 for Problem Details for HTTP APIs):
{
"type": "https://api.payflow.com/errors/invalid-request",
"title": "Invalid Request Content",
"status": 400,
"detail": "Validation failed for request parameters.",
"instance": "/api/v1/users",
"timestamp": "2026-08-01T16:59:46Z",
"errors": {
"phoneNumber": "Phone number must be exactly 10 digits",
"balance": "Balance must be non-negative"
}
}Authenticates a registered user by UPI ID and issues a cryptographically signed JWT access token.
- HTTP Method:
POST - Path:
/api/v1/auth/login - Authentication: None (Public Endpoint)
- Request Body DTO (
LoginRequest):upiId: String, required (@NotBlank), valid UPI format (@Pattern).
=== "cURL Example"
bash curl -X POST https://api.payflow.com/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{ "upiId": "alice@payflow" }'
=== "Request Payload"
json { "upiId": "alice@payflow" }
=== "Response (200 OK)"
json { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "tokenType": "Bearer", "expiresIn": 3600, "upiId": "alice@payflow", "referenceId": "550e8400-e29b-41d4-a716-446655440000" }
=== "Error Responses"
- 401 Unauthorized: Invalid UPI ID or user account does not exist.
- 422 Unprocessable Entity: UPI ID format validation failure.
Registers a new client profile with an initial balance.
- HTTP Method:
POST - Path:
/api/v1/users - Authentication: None (Public Registration)
- Request Body DTO (
CreateUserRequest):name: String, required (@NotBlank), max 100 chars (@Size(max = 100)).upiId: String, required (@NotBlank), max 100 chars (@Size(max = 100)), valid UPI format (@Pattern(regexp = "^[a-zA-Z0-9.\\-_]{2,64}@[a-zA-Z]{2,32}$")).phoneNumber: String, required (@NotBlank), exactly 10 digits (@Pattern(regexp = "^\\d{10}$")).balance: BigDecimal, required (@NotNull), non-negative (@DecimalMin("0.0")), denominated in Indian Rupees (INR, ₹).
=== "cURL Example"
bash curl -X POST https://api.payflow.com/api/v1/users \ -H "Content-Type: application/json" \ -d '{ "name": "Aarav Sharma", "upiId": "aarav@upi", "phoneNumber": "9876543210", "balance": 1000.00 }'
=== "Request Payload"
json { "name": "Aarav Sharma", "upiId": "aarav@upi", "phoneNumber": "9876543210", "balance": 1000.00 }
=== "Response (201 Created)"
Headers: Location: /api/v1/users/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d
json { "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "name": "Aarav Sharma", "upiId": "aarav@upi", "phoneNumber": "9876543210", "balance": 1000.0000, "createdAt": "2026-08-01T16:00:00Z", "updatedAt": "2026-08-01T16:00:00Z" }
=== "Error Responses"
- 409 Conflict: User with the requested UPI ID already exists (DuplicateUpiIdException).
- 422 Unprocessable Entity: Input validation failure, or external UPI verification rejected the UPI ID (InvalidUpiException, type: https://api.payflow.com/errors/invalid-upi-id).
Retrieves a paginated list of registered users. Requires administrative privileges (ROLE_ADMIN).
- HTTP Method:
GET - Path:
/api/v1/users - Authentication:
Authorization: Bearer <token>(RequiresROLE_ADMIN) - Query Parameters:
page: Integer, optional. Page index (0-based,@Min(0)). Default:0.size: Integer, optional. Page size (@Min(1) @Max(100)). Default:10.sortBy: String, optional. Column name to sort. Default:userId.
=== "cURL Example"
bash curl -X GET "https://api.payflow.com/api/v1/users?page=0&size=10&sortBy=userId" \ -H "Authorization: Bearer <admin-token>"
=== "Response (200 OK)"
json { "content": [ { "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "name": "Aarav Sharma", "upiId": "aarav@upi", "phoneNumber": "9876543210", "balance": 1000.0000, "createdAt": "2026-08-01T16:00:00Z", "updatedAt": "2026-08-01T16:00:00Z" } ], "page": 0, "size": 10, "totalElements": 1, "totalPages": 1, "first": true, "last": true }
=== "Error Responses"
- 401 Unauthorized: Missing, expired, or invalid JWT token.
- 403 Forbidden: Caller lacks administrative role privileges (ForbiddenOperationException, type: https://api.payflow.com/errors/forbidden-operation).
- 422 Unprocessable Entity: Query parameter validation failure (e.g. size < 1 or size > 100).
Fetches a single user record by their unique UUID reference ID.
- HTTP Method:
GET - Path:
/api/v1/users/{id} - Authentication:
Authorization: Bearer <token>(User can only inspect their own profile)
=== "cURL Example"
bash curl -X GET https://api.payflow.com/api/v1/users/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d \ -H "Authorization: Bearer <token>"
=== "Response (200 OK)"
json { "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "name": "Aarav Sharma", "upiId": "aarav@upi", "phoneNumber": "9876543210", "balance": 1000.0000, "createdAt": "2026-08-01T16:00:00Z", "updatedAt": "2026-08-01T16:00:00Z" }
=== "Error Responses"
- 401 Unauthorized: Missing or invalid JWT bearer token.
- 403 Forbidden: Authenticated user cannot view another user's profile.
- 404 Not Found: User reference ID not found.
Fetches a single user record by their unique UPI ID.
- HTTP Method:
GET - Path:
/api/v1/users/upi/{upiId} - Authentication:
Authorization: Bearer <token>(User can only inspect their own profile)
=== "cURL Example"
bash curl -X GET https://api.payflow.com/api/v1/users/upi/priya@upi \ -H "Authorization: Bearer <token>"
=== "Response (200 OK)"
json { "userId": 2, "name": "Priya Patel", "upiId": "priya@upi", "phoneNumber": "9876543211", "balance": 50.0000, "version": 0, "createdAt": "2026-08-01T16:00:00Z", "updatedAt": "2026-08-01T16:00:00Z" }
=== "Error Responses"
- 401 Unauthorized: Missing or invalid JWT bearer token.
- 404 Not Found: User UPI ID not found.
Retrieves paginated double-entry balance ledger audit entries for a user by UUID reference ID.
- HTTP Method:
GET - Path:
/api/v1/users/{id}/ledger - Query Parameters:
page: Integer, optional (default0), min0.size: Integer, optional (default10), min1, max100.
- Authentication:
Authorization: Bearer <token>(User can only view their own ledger)
=== "cURL Example"
bash curl -X GET "https://api.payflow.com/api/v1/users/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d/ledger?page=0&size=10" \ -H "Authorization: Bearer <token>"
=== "Response (200 OK)"
json { "content": [ { "ledgerId": 101, "userReferenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "transactionReferenceId": "f9e8d7c6-b5a4-3f2e-1d0c-9b8a7f6e5d4c", "entryType": "DEBIT", "amount": 100.0000, "balanceBefore": 500.0000, "balanceAfter": 400.0000, "createdAt": "2026-08-09T14:00:00Z" } ], "page": 0, "size": 10, "totalElements": 1, "totalPages": 1, "first": true, "last": true }
=== "Error Responses"
- 401 Unauthorized: Missing or invalid JWT bearer token.
- 403 Forbidden: Authenticated user not authorized to inspect this ledger.
- 404 Not Found: User not found.
Retrieves a list of users whose balance exceeds the specified minimum threshold. Requires administrative privileges (ROLE_ADMIN).
- HTTP Method:
GET - Path:
/api/v1/users/balance/{amount} - Authentication:
Authorization: Bearer <token>(RequiresROLE_ADMIN) - Path Variables:
amount: BigDecimal, required. Minimum balance threshold denominated in INR (₹).
=== "cURL Example"
bash curl -X GET https://api.payflow.com/api/v1/users/balance/500.00 \ -H "Authorization: Bearer <admin-token>"
=== "Response (200 OK)"
json [ { "referenceId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d", "name": "Aarav Sharma", "upiId": "aarav@payflow", "phoneNumber": "9876543210", "balance": 1000.0000, "createdAt": "2026-08-01T16:00:00Z", "updatedAt": "2026-08-01T16:00:00Z" } ]
=== "Error Responses"
- 400 Bad Request: Parameter type mismatch (non-numeric amount).
- 401 Unauthorized: Missing or invalid JWT token.
- 403 Forbidden: Caller lacks administrative role privileges.
Executes a peer-to-peer fund transfer request with guaranteed exactly-once idempotency and sender verification.
- HTTP Method:
POST - Path:
/api/v1/transactions - Authentication:
Authorization: Bearer <token>(Authenticated JWT principal must matchsenderUpiId) - Headers:
Idempotency-Key: String / UUID, required. Must be 1-255 characters matching^[A-Za-z0-9_.:-]+$. Prevents duplicate debits and replays cached responses on retry.
- Request Body DTO (
TransferMoneyRequest):senderUpiId: String, required (@NotBlank), max 100 chars (@Size(max = 100)), valid UPI format (@Pattern(...)).receiverUpiId: String, required (@NotBlank), max 100 chars (@Size(max = 100)), valid UPI format (@Pattern(...)).amount: BigDecimal, required (@NotNull), minimum ₹0.01 (@DecimalMin("0.01")), maximum ₹10,00,000 (@DecimalMax("1000000.00")), denominated in Indian Rupees (INR, ₹).note: String, optional, max 255 characters (@Size(max = 255)).
=== "cURL Example"
bash curl -X POST https://api.payflow.com/api/v1/transactions \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" \ -d '{ "senderUpiId": "aarav@upi", "receiverUpiId": "priya@upi", "amount": 150.00, "note": "Dinner bill split" }'
=== "HTTP Request" ```http POST /api/v1/transactions HTTP/1.1 Host: api.payflow.com Authorization: Bearer Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Content-Type: application/json
{
"senderUpiId": "aarav@upi",
"receiverUpiId": "priya@upi",
"amount": 150.00,
"note": "Dinner bill split"
}
```
=== "Response (201 Created)"
Headers: Location: /api/v1/transactions/550e8400-e29b-41d4-a716-446655440000
json { "transactionId": 1, "referenceId": "550e8400-e29b-41d4-a716-446655440000", "senderUpiId": "aarav@upi", "receiverUpiId": "priya@upi", "amount": 150.0000, "status": "COMPLETED", "type": "TRANSFER", "note": "Dinner bill split", "createdAt": "2026-08-01T16:05:00Z" }
=== "Error Responses"
- 400 Bad Request: Missing Idempotency-Key header, or key reused with a mismatched payload.
- 401 Unauthorized: Missing or invalid JWT bearer token.
- 403 Forbidden: Authenticated user is not authorized to transfer from requested sender UPI.
- 409 Conflict: A request with the same Idempotency-Key is currently in-flight.
- 422 Unprocessable Entity: Validation constraint failure or insufficient sender balance.
- 429 Too Many Requests: Per-user rate limit exceeded (maximum 10 requests per second; response includes Retry-After: 1 header).
- 503 Service Unavailable: Downstream dependency circuit breaker is open (upiValidation) or external provider failure.
Fetches details of a single transaction by its unique UUID reference ID.
- HTTP Method:
GET - Path:
/api/v1/transactions/{id} - Authentication:
Authorization: Bearer <token>(Only sender or receiver participants can view)
=== "cURL Example"
bash curl -X GET https://api.payflow.com/api/v1/transactions/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer <token>"
=== "Response (200 OK)"
json { "transactionId": 1, "referenceId": "550e8400-e29b-41d4-a716-446655440000", "senderUpiId": "aarav@upi", "receiverUpiId": "priya@upi", "amount": 150.0000, "status": "COMPLETED", "type": "TRANSFER", "note": "Dinner bill split", "createdAt": "2026-08-01T16:05:00Z" }
=== "Error Responses"
- 401 Unauthorized: Missing or invalid JWT bearer token.
- 403 Forbidden: Authenticated principal was neither the sender nor the receiver.
- 404 Not Found: Transaction reference ID does not exist.
Retrieves a paginated list of all transactions where the specified UPI ID is either the sender or receiver.
- HTTP Method:
GET - Path:
/api/v1/transactions/user/{upiId} - Authentication:
Authorization: Bearer <token> - Path Variables:
upiId: String, required. Registered UPI handle (e.g.aarav@upi). Must match authenticated principal or caller must be a participant.
- Query Parameters:
page: Integer, optional. Page index (0-based,@Min(0)). Default:0.size: Integer, optional. Page size (@Min(1) @Max(100)). Default:10.sortBy: String, optional. Column name to sort. Default:createdAt.
=== "cURL Example"
bash curl -X GET "https://api.payflow.com/api/v1/transactions/user/aarav@upi?page=0&size=10" \ -H "Authorization: Bearer <token>"
=== "Response (200 OK)"
json { "content": [ { "transactionId": 1, "referenceId": "550e8400-e29b-41d4-a716-446655440000", "senderUpiId": "aarav@upi", "receiverUpiId": "priya@upi", "amount": 150.0000, "status": "COMPLETED", "type": "TRANSFER", "note": "Dinner bill split", "createdAt": "2026-08-01T16:05:00Z" } ], "page": 0, "size": 10, "totalElements": 1, "totalPages": 1, "first": true, "last": true }
=== "Error Responses"
- 401 Unauthorized: Missing or invalid JWT bearer token.
- 403 Forbidden: Authenticated principal is not authorized to view transactions for this UPI ID.
Analyzes transaction metadata and recipient UPI handle using Spring AI structured prompt engineering to classify the expenditure into personal finance categories and generate actionable budget insights.
- HTTP Method:
POST - Path:
/api/v1/transactions/{id}/insights - Authentication: Principal-Bound (
Bearer JWT— caller must be transaction sender or receiver) - Feature Toggle: Controlled by
payflow.ai.enabled: true(returns503 Service UnavailablewithFeature DisabledProblemDetail when disabled) - Path Parameters:
id: UUID (Transaction Reference ID)
- Response Model (
SpendInsightResponse):transactionReferenceId: UUIDcategory: String (FOOD_AND_DINING,SHOPPING,TRANSPORTATION,UTILITIES,ENTERTAINMENT,HEALTHCARE,INVESTMENTS,TRANSFER)amount: BigDecimal (Transaction amount)summary: String (Contextual transaction summary)budgetingTip: String (Actionable budgeting advice)confidenceScore: Double (0.0 to 1.0)source: String (AI_MODELorRULE_BASED_FALLBACK)
=== "cURL Example"
bash curl -X POST https://api.payflow.com/api/v1/transactions/550e8400-e29b-41d4-a716-446655440000/insights \ -H "Authorization: Bearer <token>"
=== "Response (200 OK)"
json { "transactionReferenceId": "550e8400-e29b-41d4-a716-446655440000", "category": "FOOD_AND_DINING", "amount": 450.00, "summary": "Dining or grocery expenditure with swiggy@upi.", "budgetingTip": "Set a dedicated dining out ceiling to optimize discretionary spending.", "confidenceScore": 0.94, "source": "AI_MODEL" }
=== "Fault Tolerance"
- Downstream LLM timeouts, rate limits, or outages trip Resilience4j aiCircuitBreaker, automatically executing ruleBasedFallback with deterministic keyword heuristic categorization (source: "RULE_BASED_FALLBACK"), guaranteeing zero API downtime for end users.
=== "Error Responses"
- 403 Forbidden: Authenticated user is neither the sender nor receiver of this transaction.
- 404 Not Found: Transaction reference ID does not exist.
- 503 Service Unavailable: Gen-AI Spend Insights feature is disabled (payflow.ai.enabled: false).
Payflow API exposes standard Spring Boot Actuator endpoints for container health probes, Resilience4j status, and Prometheus metrics collection:
| Endpoint | Method | Auth Required | Description |
|---|---|---|---|
/actuator/health |
GET |
No | Basic liveness status ({"status": "UP"}). Detailed status (DB, CircuitBreakers, RateLimiters) shown when authorized. |
/actuator/health/liveness |
GET |
No | Kubernetes liveness probe confirming process health. |
/actuator/health/readiness |
GET |
No | Kubernetes readiness probe verifying database and circuit breaker health. |
/actuator/info |
GET |
No | Application build and version information. |
/actuator/prometheus |
GET |
No | Prometheus format scrape output including payflow_transfers_*, hikaricp_connections, resilience4j_circuitbreaker_*, and resilience4j_ratelimiter_*. |
/actuator/metrics |
GET |
No | JSON catalog of available Micrometer metric names. |
All error responses adhere to the standard RFC 7807 and RFC 9457 application/problem+json format:
{
"type": "https://api.payflow.com/errors/insufficient-balance",
"title": "Insufficient Balance",
"status": 422,
"detail": "Insufficient balance. Available: 100.0000, Required: 250.0000",
"instance": "/api/v1/transactions",
"timestamp": "2026-08-01T16:00:00Z",
"requestId": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d"
}Response includes header: Retry-After: 1
{
"type": "https://api.payflow.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Too many requests. You have exceeded your rate limit of 10 requests per second. Please retry after 1 seconds.",
"instance": "/api/v1/transactions",
"timestamp": "2026-09-05T12:00:00Z",
"requestId": "b7c9d1e2-3456-789a-bcde-f0123456789a"
}{
"type": "https://api.payflow.com/errors/service-unavailable",
"title": "Service Unavailable",
"status": 503,
"detail": "Circuit breaker 'upiValidation' is OPEN and does not permit further calls",
"instance": "/api/v1/transactions",
"timestamp": "2026-09-05T12:00:01Z",
"requestId": "c8d0e2f3-4567-89ab-cdef-0123456789ab"
}| Code | Status | Trigger Condition |
|---|---|---|
| 200 | OK |
Standard successful read or lookup. |
| 201 | Created |
Successfully registered a user or created a transaction. |
| 400 | Bad Request |
Illegal business arguments (e.g. self-transfer attempt), parameter type mismatch (MethodArgumentTypeMismatchException), or malformed/oversized Idempotency-Key header. |
| 401 | Unauthorized |
Missing, expired, or invalid JWT authentication token. |
| 403 | Forbidden |
Authenticated principal is not authorized for the requested resource (sender impersonation, cross-user ledger/transaction access, or non-admin access to bulk user directory / balance queries). |
| 404 | Not Found |
User or transaction lookup returned no matching records (UserNotFoundException). |
| 409 | Conflict |
Resource conflict (e.g. duplicate UPI ID registration, in-flight idempotency conflict, or constraint violation). |
| 422 | Unprocessable Entity |
Jakarta validation constraint violation, handler method parameter validation failure (HandlerMethodValidationException), or insufficient account balance (InsufficientBalanceException). |
| 429 | Too Many Requests |
Dynamic per-user rate limit exceeded (RFC 6585 with Retry-After: 1 header). |
| 500 | Internal Error |
Unexpected server error (sanitized, stack traces suppressed). |
| 503 | Service Unavailable |
Downstream service circuit breaker is OPEN (CallNotPermittedException). |