> For the complete documentation index, see [llms.txt](https://developers.share.inc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.share.inc/errors/error-contract.md).

# Error contract

Every public-API response uses the same JSON envelope, regardless of whether the call succeeds or fails. Branching on the envelope rather than on the HTTP status is the supported integration pattern — Share guarantees the envelope shape, but reserves the right to refine status codes as the API evolves.

> **Status:** binding for every endpoint under `/v1/`. The Splynx- native master webhook (`/v1/integrations/splynx/webhooks`) is not exposed in the public reference and is excluded from this contract.

***

## 1. Response envelope

All responses share the same top-level shape:

```ts
interface ApiResponseDto<T> {
  success: boolean; // true on 2xx, false on every error
  data?: T; // present on success
  error?: string; // human-readable summary on failure
  rejectionReasons?: RejectionReason[]; // structured per-field reasons (errors)
  errorCode?: string; // endpoint-specific business code (errors, optional)
  correlationId?: string; // trace ID — also returned in `x-correlation-id` header
  meta?: {
    // pagination metadata (list endpoints)
    page?: number;
    limit?: number;
    total?: number;
    totalPages?: number;
  };
}

interface RejectionReason {
  field: string; // dot-path of the offending field, or "_" if not field-scoped
  code: string; // machine-readable code from the taxonomy in `error-codes.md`
  message: string; // human-readable explanation, safe to surface to operators
}
```

The success-side helpers `ApiResponseDto.ok(data, meta?)` and the error helper `ApiResponseDto.error(error, rejectionReasons?)` are how the backend builds the envelope. As an integrator you do not need to import anything — just consume the JSON shape above.

The HTTP status mirrors the `error` category (4xx for client errors, 5xx for server errors), but **trust the body**: in rare timeout cases the gateway may surface a 4xx envelope on a 200 status, or vice-versa.

***

## 2. Worked examples

### 2.1 Success

```http
HTTP/1.1 200 OK
Content-Type: application/json
x-correlation-id: c3f1d9c4-...-...

{
  "success": true,
  "data": {
    "id": "b3a1d9c4-...-...",
    "firstName": "Jane",
    "lastName": "Mwangi"
  }
}
```

List responses wrap the array in `data` and include `meta`:

```http
HTTP/1.1 200 OK
Content-Type: application/json
x-correlation-id: c3f1d9c4-...-...

{
  "success": true,
  "data": [ /* ... */ ],
  "meta": { "page": 1, "limit": 20, "total": 137, "totalPages": 7 }
}
```

### 2.2 Validation failure (multi-field)

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json
x-correlation-id: c3f1d9c4-...-...

{
  "success": false,
  "error": "Validation failed",
  "rejectionReasons": [
    { "field": "email",            "code": "INVALID_FORMAT", "message": "Must be a valid email address" },
    { "field": "phone",            "code": "REQUIRED",       "message": "Phone number is required" },
    { "field": "address.postalCode", "code": "INVALID_FORMAT", "message": "Must be a 5-digit postal code" }
  ]
}
```

### 2.3 Business-rule rejection (single field)

```http
HTTP/1.1 409 Conflict
Content-Type: application/json
x-correlation-id: c3f1d9c4-...-...

{
  "success": false,
  "error": "Subscriber with this email already exists",
  "rejectionReasons": [
    {
      "field": "email",
      "code": "ALREADY_EXISTS",
      "message": "A subscriber with this email already exists for this provider"
    }
  ]
}
```

### 2.4 Endpoint-specific `errorCode`

Some operations attach a top-level `errorCode` for business rejections that have no natural single-field code. Example — `DELETE /v1/plans/:id` with a mismatched `confirm` parameter:

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json
x-correlation-id: c3f1d9c4-...-...

{
  "success": false,
  "error": "Plan disable requires the `confirm` query parameter to echo the plan name (case-insensitive, whitespace trimmed).",
  "errorCode": "CONFIRMATION_MISMATCH",
  "details": { "expectedPlanName": "Home 50 Mbps" }
}
```

The full catalog of `errorCode` values, the operations that emit them, and their meaning is in [Error codes](/errors/error-codes.md).

### 2.5 Server error

```http
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
x-correlation-id: c3f1d9c4-...-...

{
  "success": false,
  "error": "Internal server error",
  "rejectionReasons": [
    { "field": "_", "code": "INTERNAL_ERROR", "message": "Internal server error" }
  ]
}
```

The `field: "_"` sentinel marks errors that are not tied to a specific input field (server failures, auth failures, rate-limit, timeout, etc.). Render those as a top-level message rather than attaching them to a form field.

***

## 3. HTTP status → semantic mapping

The HTTP status indicates the broad category. The `error` / `errorCode` / `rejectionReasons[].code` fields carry the precise meaning — branch on those.

| Status                      | Meaning                                                                                                                                              | Retry?                                                                                             |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `400 Bad Request`           | Validation failure or business-rule rejection. Inspect `rejectionReasons[]` or top-level `errorCode`.                                                | No — fix the request and try again.                                                                |
| `401 Unauthorized`          | Missing or invalid `X-Access-Id` / `X-Access-Secret`.                                                                                                | No — rotate or check credentials.                                                                  |
| `403 Forbidden`             | Credentials valid but caller is not allowed to act on this resource (typically a cross-provider ID).                                                 | No — adjust the request to reference resources the caller owns.                                    |
| `404 Not Found`             | Resource does not exist, or is not visible to the caller's provider (cross-provider IDs are not leaked).                                             | No — verify the ID.                                                                                |
| `408 Request Timeout`       | A downstream Kafka send timed out before producing a result (`code: TIMEOUT`).                                                                       | **Yes — with idempotency key.** Same request shape, exponential backoff.                           |
| `409 Conflict`              | State conflict: idempotency-key collision, terminal state, uniqueness violation, or endpoint-specific business rule (e.g. `CHARGE_NOT_CANCELLABLE`). | No — inspect the body. Some conflicts (idempotency replay) are benign.                             |
| `422 Unprocessable Entity`  | Reserved for `PRECONDITION` failures (the request was well-formed but the system is not in the right state to accept it).                            | Sometimes — retry after the precondition is satisfied.                                             |
| `429 Too Many Requests`     | Rate limit exceeded (`code: RATE_LIMITED`).                                                                                                          | **Yes — after the `Retry-After` interval.** Apply jitter on the client.                            |
| `500 Internal Server Error` | Unhandled exception. Always emits `rejectionReasons: [{ field: "_", code: "INTERNAL_ERROR" }]`.                                                      | **Yes — with idempotency key, exponential backoff.** Escalate persistent 500s via `correlationId`. |
| `502 Bad Gateway`           | A required downstream call (partner adapter, BSS) failed in a way the caller can retry (`code: UPSTREAM_FAILURE`).                                   | **Yes — exponential backoff.**                                                                     |
| `503 Service Unavailable`   | A required dependency is misconfigured or temporarily offline (e.g. inbound-webhook secret not configured).                                          | **Yes — wait and retry.**                                                                          |

***

## 4. Retry guidance

Retries are safe **only** when the operation is idempotent on the server side. Two protections are available:

1. **`Idempotency-Key` header** — supply a client-generated UUID on every POST that creates state (`POST /v1/installations`, `POST /v1/subscribers/:subscriberId/charges`). The backend persists the key against the (provider, resource) pair and short-circuits duplicate POSTs to return the original response without re-emitting downstream events. The key is honored for the lifetime of the target resource — pick a fresh UUID for unrelated requests.
2. **Read-then-act** — for non-creating operations (PUT/PATCH/DELETE), the server is idempotent by virtue of the operation itself: re- submitting the same PATCH yields the same result. Retry freely on 408/500/502/503 as long as the request body is unchanged.

`GET` is always retry-safe.

Do **not** retry on 4xx other than 408 / 429 / 503 — the request will fail again.

***

## 5. Using `correlationId` when escalating

Every response carries a `correlationId` (also returned in the `x-correlation-id` header). It is the trace ID Share uses internally to follow a single API call through every microservice it touches — the API gateway, the CRM service, billing, the SasaPay adapter, and the outbound webhook queue.

When opening a support ticket about a failed call, include:

* The full request URL (method + path + relevant headers, redacted).
* The `correlationId` from the response (or `x-correlation-id` header).
* The approximate request time (Share-side logs are partitioned by day; a 24-hour window is sufficient).

With those three, Share can pull every log line, audit row, integration log, and outbound webhook attempt tied to that one request in a single query (`npm run trace -- --correlationId <id>` internally). Without them, triage is materially slower.

***

## 6. Code catalogue

The full set of codes — generic (`rejectionReasons[].code`), endpoint-specific (`errorCode`), and non-coded message-only rejections — is enumerated in [Error codes](/errors/error-codes.md). Update that file whenever a new code is added; this document only carries the contract.
