Skip to main content
Version: v2

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

RuleDetailExample
Always include /v2/Every v2 endpoint path starts with /v2//v2/payments, /v2/refunds
No versionless aliasesDo not create unversioned routes that forward to v2/payments → ❌
Version in base path onlyThe version appears once at the start, not repeated in sub-paths/v2/payments/{id}/cancel, not /v2/payments/{id}/v2/cancel
Multiple versions can coexistv1 (/v1/...) and v2 (/v2/...) endpoints run side-by-side in the same service/v1/payments and /v2/payments both active
Future versionsWhen 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

ConventionRuleExample
ResourcesPlural nouns in kebab-case/v2/payments, /v2/refunds, /v2/sessions
ActionsNon-CRUD operations use a verb sub-path/v2/payments/{paymentId}/cancel, /v2/payments/{paymentId}/capture
Query parameterscamelCase?merchantTransactionId=abc
Path parameterscamelCase{paymentId}, {sessionId}
Composite pathsFor scoped resources, nest under parent/v2/token/payments (token-scoped payment creation)

HTTP Methods & Status Codes

MethodSemanticsSuccess CodeWhen to Use
POSTCreate a resource202 AcceptedAsync creation (payment, refund, session)
GETRetrieve a resource200 OK (terminal) / 202 Accepted (in-progress)Read-only retrieval
PATCHPartial update / action202 AcceptedCancel, capture (async operations)

Error Status Codes

CodeMeaningWhen to Use
400Bad RequestSchema validation failures (malformed JSON, missing required fields, constraint violations)
401UnauthorizedMissing or invalid OAuth2 token
403ForbiddenValid token but insufficient scope, or X-Merchant-Id not linked to token clientId
404Not FoundResource does not exist or belongs to a different merchant
406Not AcceptableRequest cannot be fulfilled due to business rule (e.g., invalid state transition)
422Unprocessable EntityBusiness rule violation or vendor processing error after request was accepted
500Internal Server ErrorUnexpected server-side failure

Required Headers

Every v2 request must include:

HeaderTypeRequiredDescription
AuthorizationstringYesOAuth2 bearer token
X-Merchant-IduuidYesMerchant identifier — must be linked to the OAuth clientId
X-Upstream-EnvstringNon-prod onlyEnvironment target: dev, stage, test
X-SourcestringNoSource system identifier (max 50 chars)
Content-TypestringYes (POST/PATCH)application/json

Request Body Conventions

Field Types

ConventionRuleExample
Monetary amountsInteger in cents (smallest currency unit)"amount": 15000 = $150.00
UUIDsstring with format: uuid"id": "550e8400-e29b-41d4-a716-446655440000"
DatesISO 8601 YYYY-MM-DD"dateOfBirth": "1990-05-15"
TimestampsISO 8601 with UTC Z suffix"paymentDateUtc": "2024-01-15T14:30:00Z"
EnumsUPPER_SNAKE_CASE strings"status": "COMPLETED", "type": "CARD"
Nullable fieldsTyped as [type, "null"] with default: null"description": null
MetadataKey-value map, max 20 entries, key 1-40 chars, value 1-100 chars"metadata": {"orderId": "12345"}

Validation Standards

ConstraintImplementation
Required fieldsrequired array in OpenAPI schema + @Valid / @NotNull annotations
String lengthminLength / maxLength in schema
Numeric rangeminimum / maximum in schema (e.g., amount: min=1, max=100000000)
Patternpattern regex in schema (e.g., email, SSN last 4, ZIP)
Enum valuesenum array in schema — reject unknown values
Array boundsminItems / maxItems (e.g., paymentAllocations: min=1, max=2)
Cross-fieldService 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

FieldRole
merchantTransactionIdClient-supplied idempotency key for POST /v2/payments
Retry limitSame merchantTransactionId may be submitted up to 5 times (1 initial + 4 retries)
Behavior on duplicateReturns the existing payment resource with current status
After 5 attemptsNew 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

RuleDetail
URL-basedVersion is always in the URL path: /v2/payments
CoexistenceMultiple API versions (v1, v2) run simultaneously in the same service and gateway
Backward compatibility within a versionAdditive changes (new optional fields, new endpoints) do not require a version bump
Breaking changesNew required fields, removed fields, changed semantics, changed status codes → new version (e.g., v3)
Deprecation noticeMinimum 6-month notice before a version is retired
RoutingThe API gateway routes requests to the correct version handler based on the URL prefix
Schema separationEach version has its own OpenAPI spec and schema directory (openapi/v2/, future openapi/v3/)
Commons libraryShared 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 TypeBreaking?Action Required
Add optional field to requestNoDocument in changelog
Add field to responseNoDocument in changelog
Add new endpointNoDocument in changelog
Remove field from request/responseYesNew API version
Make optional field requiredYesNew API version
Change field typeYesNew API version
Change HTTP status code for existing scenarioYesNew API version
Change enum value semanticsYesNew API version