Response Envelopes
Every LinkOStar endpoint responds with a consistent envelope. Centralise the unwrap logic in one helper and every call goes through it.
1. Single resource — { data }
HTTP 200 OK
{
"data": {
"hubUuid": "...",
"tenantUuid": "...",
"createdAt": "2026-06-01T12:00:00Z",
...
}
}A GET for a non-existent ID returns 404 with an error envelope (below).
Examples added in V10+/V13:
# GET /app/supplier-links/{id}/access-token
{
"data": {
"accessToken": "<supplier OAuth access_token>",
"expiresAt": "2026-06-20T10:23:45Z"
}
}
# GET /app/bundle-catalog/{bundleUuid}
{
"data": {
"bundleUuid": "...",
"supplierUuid": "...",
"name": "...",
"versionLabel": "1.0.0",
"requiresSupplierLink": true,
"currentUserLinked": false,
"consentPreview": "This supplier collects the hub's data on its own servers.",
"supplierAuthorizeHint":"POST /app/supplier-link/authorize with {\"supplierUuid\":\"...\"}"
}
}2. List + pagination — { data, pagination }
HTTP 200 OK
{
"data": [
{ "hubUuid": "...", ... },
{ "hubUuid": "...", ... }
],
"pagination": {
"page": 1,
"size": 20,
"totalElements": 137,
"totalPages": 7
}
}Query params are page (1-based) and size. BFFs most often consume totalElements for count tiles.
3. Empty body (204 No Content)
Telemetry intake (POST /v1/hubs/{uuid}/telemetry), claim-code revoke, and some other mutations return 204. The body is empty — clients must check status (or content-length) before calling .json().
4. Error envelope (RFC 7807 problem+json)
Spring Boot serialises errors as ProblemDetail.
HTTP 4xx / 5xx
Content-Type: application/problem+json
{
"type": "https://api.linkostar.com/errors/validation-failed",
"title": "Validation Failed",
"status": 400,
"detail": "claimCode is required",
"instance": "/v1/hubs/claim",
"traceId": "abc123...",
"timestamp": "2026-06-04T12:34:56Z"
}Common statuses:
| HTTP | Meaning | What to do |
|---|---|---|
| 400 | Validation failure | Read the detail, fix the input |
| 401 | Missing/expired credential | Refresh and retry |
| 403 | Authenticated but unauthorised | Check role / tenant scope |
| 404 | Resource not found | Verify the ID |
| 409 | Conflict (e.g. already exists) | Branch on the detail message |
| 423 | Locked (e.g. blocked hub) | Contact ops |
| 429 | Rate limited | Back off and retry |
| 500 | Server error (possibly transient) | Capture traceId, retry, escalate if it persists |
5. TypeScript unwrap helper
interface ApiEnvelope<T> {
data?: T;
pagination?: { page: number; size: number; totalElements: number; totalPages: number };
}
async function apiList<T>(path: string): Promise<{ items: T[]; total: number }> {
const res = await fetch(`${BASE}${path}`, { headers: { 'X-API-Key': KEY } });
if (!res.ok) throw new ApiError(await res.json());
const env = await res.json() as ApiEnvelope<T[]>;
return {
items: env.data ?? [],
total: env.pagination?.totalElements ?? (env.data?.length ?? 0),
};
}6. Java unwrap helper
record Envelope<T>(T data, Pagination pagination) {}
record Pagination(int page, int size, long totalElements, int totalPages) {}
public <T> long countViaList(String path, ParameterizedTypeReference<Envelope<List<T>>> tref) {
Envelope<List<T>> env = restClient
.get().uri(path + "?page=1&size=1")
.retrieve()
.body(tref);
return env.pagination() != null ? env.pagination().totalElements() : 0L;
}7. Compatibility promise
- The envelope shape itself is not subject to breaking changes — existing keys stay, only new keys are added.
- Entity keys inside
dataevolve per endpoint. Additions are minor releases; removals and renames are major (announced in Changelog). - Ignore unknown keys — never throw.