LinkO'Star

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:

HTTPMeaningWhat to do
400Validation failureRead the detail, fix the input
401Missing/expired credentialRefresh and retry
403Authenticated but unauthorisedCheck role / tenant scope
404Resource not foundVerify the ID
409Conflict (e.g. already exists)Branch on the detail message
423Locked (e.g. blocked hub)Contact ops
429Rate limitedBack off and retry
500Server 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 data evolve per endpoint. Additions are minor releases; removals and renames are major (announced in Changelog).
  • Ignore unknown keys — never throw.