API Design Conventions
Overview
All v2 endpoints follow consistent design patterns. This document is the authoritative reference for engineers adding or modifying endpoints.
URL Structure & Version Prefix
All v2 endpoints must include the /v2/ prefix in the URL path. This is a hard requirement — there is no versionless path.
/v2/{resource}
/v2/{resource}/{resourceId}
/v2/{resource}/{resourceId}/{action}
Version Prefix Rules
| Rule | Detail | Example |
|---|---|---|
Always include /v2/ | Every v2 endpoint path starts with /v2/ | /v2/payments, /v2/refunds |
| No versionless aliases | Do not create unversioned routes that forward to v2 | ❌ /payments → ❌ |
| Version in base path only | The version appears once at the start, not repeated in sub-paths | /v2/payments/{id}/cancel, not /v2/payments/{id}/v2/cancel |
| Multiple versions can coexist | v1 (/v1/...) and v2 (/v2/...) endpoints run side-by-side in the same service | /v1/payments and /v2/payments both active |
| Future versions | When v3 is introduced, it will follow the same pattern: /v3/{resource} | New version = new prefix, no aliasing |
Controller Implementation
The /v2/ prefix is enforced via @RequestMapping at the controller class level. Never set the version in individual method mappings:
// Correct — version set once at class level
@RestController
@RequestMapping("/v2/payments") // ← all methods inherit /v2/payments/*
@Validated
@Slf4j
public class PaymentControllerV2 {
@PostMapping // → POST /v2/payments
public Mono<ResponseEntity<Resource<PaymentResponse>>> createPaymentIntent(
@RequestHeader("X-Merchant-Id") UUID merchantId,
@RequestHeader(value = "X-Customer-Id", required = false) String customerId,
@RequestBody @Valid PaymentRequest paymentRequest,
ServerHttpRequest httpRequest) { ... }
@GetMapping("/{paymentId}") // → GET /v2/payments/{paymentId}
public Mono<ResponseEntity<Resource<PaymentResponse>>> getPayment(
@RequestHeader("X-Merchant-Id") UUID merchantId,
@PathVariable UUID paymentId) { ... }
@PatchMapping("/{paymentId}") // → PATCH /v2/payments/{paymentId}
public Mono<ResponseEntity<Resource<PaymentResponse>>> cancelPayment(
@RequestHeader("X-Merchant-Id") UUID merchantId,
@PathVariable UUID paymentId,
@RequestBody @Valid CancelRequest cancelRequest) { ... }
}
// Correct — refund controller
@RestController
@RequestMapping("/v2/refunds")
public class RefundControllerV2 { ... }
// Incorrect — version in method mapping
@RestController
@RequestMapping("/payments")
public class PaymentControllerV2 {
@PostMapping("/v2") // ❌ — version must be at class level
public Mono<...> createPayment(...) { ... }
}
Resource & Path Conventions
| Convention | Rule | Example |
|---|---|---|
| Resources | Plural nouns in kebab-case | /v2/payments, /v2/refunds, /v2/sessions |
| Actions | Non-CRUD operations use a verb sub-path | /v2/payments/{paymentId}/cancel, /v2/payments/{paymentId}/capture |
| Query parameters | camelCase | ?merchantTransactionId=abc |
| Path parameters | camelCase | {paymentId}, {sessionId} |
| Composite paths | For scoped resources, nest under parent | /v2/token/payments (token-scoped payment creation) |
HTTP Methods & Status Codes
| Method | Semantics | Success Code | When to Use |
|---|---|---|---|
POST | Create a resource | 202 Accepted | Async creation (payment, refund, session) |
GET | Retrieve a resource | 200 OK (terminal) / 202 Accepted (in-progress) | Read-only retrieval |
PATCH | Partial update / action | 202 Accepted | Cancel, capture (async operations) |
Error Status Codes
| Code | Meaning | When to Use |
|---|---|---|
400 | Bad Request | Schema validation failures (malformed JSON, missing required fields, constraint violations) |
401 | Unauthorized | Missing or invalid OAuth2 token |
403 | Forbidden | Valid token but insufficient scope, or X-Merchant-Id not linked to token clientId |
404 | Not Found | Resource does not exist or belongs to a different merchant |
406 | Not Acceptable | Request cannot be fulfilled due to business rule (e.g., invalid state transition) |
422 | Unprocessable Entity | Business rule violation or vendor processing error after request was accepted |
500 | Internal Server Error | Unexpected server-side failure |
Required Headers
Every v2 request must include:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | OAuth2 bearer token |
X-Merchant-Id | uuid | Yes | Merchant identifier — must be linked to the OAuth clientId |
X-Upstream-Env | string | Non-prod only | Environment target: dev, stage, test |
X-Source | string | No | Source system identifier (max 50 chars) |
Content-Type | string | Yes (POST/PATCH) | application/json |
Request Body Conventions
Field Types
| Convention | Rule | Example |
|---|---|---|
| Monetary amounts | Integer in cents (smallest currency unit) | "amount": 15000 = $150.00 |
| UUIDs | string with format: uuid | "id": "550e8400-e29b-41d4-a716-446655440000" |
| Dates | ISO 8601 YYYY-MM-DD | "dateOfBirth": "1990-05-15" |
| Timestamps | ISO 8601 with UTC Z suffix | "paymentDateUtc": "2024-01-15T14:30:00Z" |
| Enums | UPPER_SNAKE_CASE strings | "status": "COMPLETED", "type": "CARD" |
| Nullable fields | Typed as [type, "null"] with default: null | "description": null |
| Metadata | Key-value map, max 20 entries, key 1-40 chars, value 1-100 chars | "metadata": {"orderId": "12345"} |
Validation Standards
| Constraint | Implementation |
|---|---|
| Required fields | required array in OpenAPI schema + @Valid / @NotNull annotations |
| String length | minLength / maxLength in schema |
| Numeric range | minimum / maximum in schema (e.g., amount: min=1, max=100000000) |
| Pattern | pattern regex in schema (e.g., email, SSN last 4, ZIP) |
| Enum values | enum array in schema — reject unknown values |
| Array bounds | minItems / maxItems (e.g., paymentAllocations: min=1, max=2) |
| Cross-field | Service layer validation (e.g., SUM(allocations.amount) == payment.amount) |
Response Envelope
Success Response (200 / 202)
{
"url": "https://api-stg.uhg.com/api/financial/commerce/nonprodcheckout/v2/payments/550e8400-e29b-41d4-a716-446655440000",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"merchantTransactionId": "order-12345",
"amount": 15000,
"currencyCode": "USD",
"status": "INITIATED",
"authorizeCard": false,
"partialAuthorization": false,
"customer": { ... },
"paymentAllocations": [ ... ],
"paymentDateUtc": "2024-01-15T14:30:00Z",
"metadata": null
}
}
Error Response (4xx / 5xx)
{
"title": "INVALID_REQUEST",
"status": 400,
"detail": "Invalid request format. Please check JSON structure and field types"
}
Unprocessable Entity with Allocation Detail (422)
{
"title": "PAYMENT_ERROR",
"status": 422,
"detail": "Payment processing failed for all records. Check individual records for error details",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "FAILED",
"paymentAllocations": [
{
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"status": "FAILED",
"error": {
"code": "card_declined",
"message": "Your card was declined."
}
}
]
}
}
Idempotency Convention
| Field | Role |
|---|---|
merchantTransactionId | Client-supplied idempotency key for POST /v2/payments |
| Retry limit | Same merchantTransactionId may be submitted up to 5 times (1 initial + 4 retries) |
| Behavior on duplicate | Returns the existing payment resource with current status |
| After 5 attempts | New merchantTransactionId required; 422 returned otherwise |
Versioning Strategy
URL-Based Versioning
The platform uses URL-based versioning with the version as the first segment of the path:
/v1/payments ← v1 (legacy, being deprecated)
/v2/payments ← v2 (current)
/v3/payments ← v3 (future — same pattern)
Version Lifecycle Rules
| Rule | Detail |
|---|---|
| URL-based | Version is always in the URL path: /v2/payments |
| Coexistence | Multiple API versions (v1, v2) run simultaneously in the same service and gateway |
| Backward compatibility within a version | Additive changes (new optional fields, new endpoints) do not require a version bump |
| Breaking changes | New required fields, removed fields, changed semantics, changed status codes → new version (e.g., v3) |
| Deprecation notice | Minimum 6-month notice before a version is retired |
| Routing | The API gateway routes requests to the correct version handler based on the URL prefix |
| Schema separation | Each version has its own OpenAPI spec and schema directory (openapi/v2/, future openapi/v3/) |
| Commons library | Shared DTOs in wallet-event-commons are versioned by package (events.v2.dto) — new versions add new packages, never modify existing ones |
What Constitutes a Breaking Change
| Change Type | Breaking? | Action Required |
|---|---|---|
| Add optional field to request | No | Document in changelog |
| Add field to response | No | Document in changelog |
| Add new endpoint | No | Document in changelog |
| Remove field from request/response | Yes | New API version |
| Make optional field required | Yes | New API version |
| Change field type | Yes | New API version |
| Change HTTP status code for existing scenario | Yes | New API version |
| Change enum value semantics | Yes | New API version |