API Standards¶
Authority: ADR-015: API Design Standards. Per-service request/response shapes live under API Contracts.
Versioning¶
Every external REST API is mounted under /api/v1. /internal/** endpoints (synchronous,
service-to-service, tokenless-at-the-network-perimeter calls such as campaign-service's
eligibility check) are explicitly not versioned this way and are never gateway-routed.
The response envelope¶
Every external response is wrapped in ApiResult<T> (com.telco.platform.common.api.ApiResult,
from starter-api - never hand-rolled per service):
{
"success": true,
"data": { "...": "..." },
"error": null,
"meta": {
"traceId": "...",
"correlationId": "...",
"timestamp": "...",
"service": "customer-service",
"path": "/api/v1/customers/123"
}
}
On failure, data is null and error carries a stable code, a human-readable message, the
same traceId, and a server-side logId that correlates to the full detail in the logs - never
a stack trace or raw exception message. Domain models are never returned directly; every response
is a DTO.
Pagination¶
Two supported models, chosen per endpoint based on the data shape:
| Model | Type | Use for |
|---|---|---|
| Offset-based | PageResult<T> |
Default; small to medium, typically admin-facing datasets |
| Cursor-based | CursorPage<T> |
Large or streaming reads where offset pagination would degrade (e.g. usage history) |
Cursors are opaque, stable, and backed by deterministic ordering - never a raw offset or a client-visible database identifier.
Errors¶
Every thrown exception is one of the platform's typed PlatformException subtypes
(ResourceNotFoundException, ValidationException, ConflictException, BusinessRuleException,
AccessDeniedException, UnauthenticatedException, DependencyFailureException), each mapping
to a fixed HTTP status via the shared GlobalExceptionHandler. Services extend the platform
ErrorCode enum with their own domain-specific codes rather than inventing a new mechanism.
Idempotency¶
Endpoints that must not double-execute on retry (payments, in particular) require an
Idempotency-Key header. This is currently implemented per service; a shared
starter-api/starter-inbox mechanism is a tracked platform gap - see
Platform & Reuse-Before-Build.
Where the real contracts live¶
This page is the shape of the contract, not the content. For what each service actually exposes - every route, request/response body, and status code - see API Contracts and the per-service pages under it.