> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jtl-software.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Error handling in JTL APIs

Every JTL API uses standard HTTP status codes to indicate success or failure. This page covers the error formats returned by the JTL-Wawi API (REST and GraphQL) and SCX Channel API, the status codes you'll encounter, and strategies for handling failures.

## HTTP Status Codes

Use the HTTP status code as the first signal for how to handle a response. These codes apply to both REST and GraphQL requests at the HTTP transport level.

### Success Codes

| Code             | Meaning           | When you'll see it                                          |
| ---------------- | ----------------- | ----------------------------------------------------------- |
| `200 OK`         | Request succeeded | Successful GET, PUT, PATCH, or GraphQL requests             |
| `201 Created`    | Resource created  | Successful POST requests that create a new resource         |
| `204 No Content` | Success, no body  | Successful DELETE requests or updates with no response body |

<Warning>
  For GraphQL requests, a `200 OK` status does **not** guarantee success. GraphQL can return errors inside the response body while the HTTP status remains 200. Always check the `errors` array in the response. See the [GraphQL errors](#graphql-api) section below.
</Warning>

### Client Error Codes (4xx)

These indicate a problem with **your request**. Fix the request before retrying.

| Code                       | Meaning                              | Common cause                                                                       |
| -------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------- |
| `400 Bad Request`          | Invalid request format or parameters | Malformed JSON, missing required fields, invalid data types                        |
| `401 Unauthorized`         | Authentication failed                | Missing `X-Tenant-ID` header, missing or expired access token, invalid credentials |
| `403 Forbidden`            | Insufficient permissions             | Valid token but lacking the required scope for this endpoint                       |
| `404 Not Found`            | Resource does not exist              | Wrong URL, deleted resource, or incorrect ID                                       |
| `409 Conflict`             | Resource conflict                    | Attempting to create a resource that already exists, or a concurrent modification  |
| `412 Precondition Failed`  | Required precondition missing        | Missing `X-Tenant-ID` header or other required headers                             |
| `422 Unprocessable Entity` | Validation failed                    | Request is well-formed but the data does not pass business rules                   |
| `429 Too Many Requests`    | Rate limit exceeded                  | Too many requests in a given time window.                                          |

### Server Error Codes (5xx)

These indicate a problem on **JTL's side**. You cannot fix the underlying cause, but your app should handle them with retries and backoff.

| Code                        | Meaning                      | What to do                                                                       |
| --------------------------- | ---------------------------- | -------------------------------------------------------------------------------- |
| `500 Internal Server Error` | Unexpected server failure    | Retry with exponential backoff. If persistent, contact support.                  |
| `502 Bad Gateway`           | Upstream service unavailable | Retry after a short delay. Usually transient.                                    |
| `503 Service Unavailable`   | Service temporarily down     | Retry with exponential backoff.                                                  |
| `504 Gateway Timeout`       | Request timed out upstream   | Retry once. If persistent, simplify your request (fewer items, smaller payload). |

## Error Response Formats

The error response format differs between the JTL-Wawi REST API, the JTL-Wawi GraphQL API, and the SCX Channel API.

### JTL-Wawi REST API

REST endpoints return errors as JSON with an error code, message, and optional validation details:

```json theme={null}
{
  "errorCode": "VALIDATION_ERROR",
  "validationErrors": {
    "sku": "SKU is required and cannot be empty",
    "price": "Price must be a positive number"
  },
  "errors": {},
  "errorMessage": "One or more validation errors occurred.",
  "stacktrace": "..."
}
```

| Field              | Description                                                                                     |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| `errorCode`        | A machine-readable error identifier (e.g., `VALIDATION_ERROR`, `NOT_FOUND`)                     |
| `validationErrors` | An object mapping field names to validation error messages. Empty `{}` if no validation errors. |
| `errors`           | Additional error details. Empty `{}` in most cases.                                             |
| `errorMessage`     | A human-readable description of the error                                                       |
| `stacktrace`       | Server-side stack trace. Do not rely on this in production. May be absent.                      |

### GraphQL API

GraphQL requests return HTTP `200 OK` for both successful and failed operations. Errors are reported inside the response body in an `errors` array.

A successful response looks like this:

```json theme={null}
{
  "data": {
    "QueryItems": {
      "nodes": [ ... ],
      "totalCount": 674
    }
  }
}
```

An error response looks like this:

```json theme={null}
{
  "data": null,
  "errors": [
    {
      "message": "Field 'invalidField' not found on type 'ItemListItem'",
      "locations": [{ "line": 4, "column": 7 }],
      "path": ["QueryItems"],
      "extensions": {
        "code": "VALIDATION_ERROR"
      }
    }
  ]
}
```

A partial success (some data, some errors) looks like this:

```json theme={null}
{
  "data": {
    "QueryItems": {
      "nodes": [ ... ],
      "totalCount": 674
    },
    "CreateCategory": null
  },
  "errors": [
    {
      "message": "Category name is required",
      "path": ["CreateCategory"]
    }
  ]
}
```

| Field                      | Description                                                                                                                                                                                                       |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data`                     | The result of the operation. May be `null` if the entire operation failed, or contain partial results if some fields succeeded.                                                                                   |
| `errors`                   | An array of error objects. Each error has a `message`, optional `locations` (line/column in the query), optional `path` (which field caused the error), and optional `extensions` with a machine-readable `code`. |
| `errors[].message`         | A human-readable description of the error                                                                                                                                                                         |
| `errors[].path`            | Array of field names tracing the error to a specific part of the query                                                                                                                                            |
| `errors[].extensions.code` | A machine-readable error code (e.g., `VALIDATION_ERROR`, `UNAUTHORIZED`)                                                                                                                                          |

<Note>
  The most important difference from REST: **never assume a `200 OK` means success** when using GraphQL. Always check for the `errors` array in the response body before processing `data`.
</Note>

### SCX Channel API

The SCX Channel API returns errors as an `errorList` array, which can contain **multiple errors** in a single response:

```json theme={null}
{
  "errorList": [
    {
      "code": "VAL100",
      "message": "orderList[0].sellerId: SellerId must not be empty and may contain a maximum of 50 alphanumeric characters",
      "severity": "error",
      "hint": null
    },
    {
      "code": "VAL100",
      "message": "orderList[0].orderStatus: Invalid or unknown order status.",
      "severity": "error",
      "hint": null
    }
  ]
}
```

| Field      | Description                                                                        |
| ---------- | ---------------------------------------------------------------------------------- |
| `code`     | An error code identifying the type of error (e.g., `VAL100` for validation errors) |
| `message`  | A human-readable description, including the field path                             |
| `severity` | The severity level (e.g., `error`)                                                 |
| `hint`     | An optional hint for resolving the error (may be `null`)                           |

**Key differences between the three formats:**

| Feature                   | Cloud-ERP REST                           | Cloud-ERP GraphQL                    | SCX                              |
| ------------------------- | ---------------------------------------- | ------------------------------------ | -------------------------------- |
| **Error location**        | HTTP status code + body                  | `errors` array in body (HTTP is 200) | HTTP status code + body          |
| **Multiple errors**       | Single `errorMessage`                    | Multiple items in `errors` array     | Multiple items in `errorList`    |
| **Field mapping**         | `validationErrors` object keyed by field | `path` array on each error           | Field path embedded in `message` |
| **Machine-readable code** | `errorCode` field                        | `extensions.code`                    | `code` field                     |

### Error Message Language

The SCX Channel API returns error messages in **German by default**. To receive error messages in English, set the `Accept-Language` header:

```
Accept-Language: en
```

***

## Handling Errors Effectively

Examples are in TypeScript. The patterns translate directly to any language with an HTTP client.

### 1. Check the Status Code and the Response Body

For REST and SCX requests, the HTTP status code tells you if something went wrong. For GraphQL, the status code is almost always `200`, so you must check the body.

**REST.** Check `response.ok` first, then parse the error body for the top-level message and any field-level validation errors:

```typescript theme={null}
const response = await fetch(url, { headers });

if (!response.ok) {
  const error = await response.json();
  console.error(`API error (${response.status}): ${error.errorMessage}`);

  for (const [field, message] of Object.entries(error.validationErrors ?? {})) {
    console.error(`  - ${field}: ${message}`);
  }

  return;
}

const data = await response.json();
```

**GraphQL.** Check two layers: HTTP-level errors first, then the `errors` array inside the response body. If `data` is present alongside errors, the response is a partial success, decide whether to use the available data or treat it as a failure:

```typescript theme={null}
const response = await fetch(graphqlUrl, { method: "POST", headers, body });

if (!response.ok) {
  throw new Error(`HTTP error (${response.status})`);
}

const result = await response.json();

if (result.errors?.length) {
  for (const error of result.errors) {
    const at = error.path ? ` at ${error.path.join(".")}` : "";
    console.error(`GraphQL error: ${error.message}${at}`);
  }

  if (!result.data) {
    throw new Error("GraphQL request failed completely");
  }
}

// Process data (may be partial if errors were present)
const items = result.data.QueryItems;
```

### 2. Handle Auth Errors (401) with Token Refresh

A `401` typically means your access token has expired. Refresh the token and retry once:

```typescript theme={null}
async function fetchWithAuth(
  url: string,
  tenantId: string
): Promise<Response> {
  let accessToken = getCachedToken();
 
  let response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "X-Tenant-ID": tenantId,
    },
  });
 
  // If token expired, refresh and retry once
  if (response.status === 401) {
    accessToken = await getNewAccessToken();
    cacheToken(accessToken);
 
    response = await fetch(url, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "X-Tenant-ID": tenantId,
      },
    });
  }
 
  return response;
}
```

<Note>
  Only retry **once** on a 401. If the second request also returns 401, the issue is likely invalid credentials rather than an expired token. Retrying indefinitely will not resolve it.
</Note>

### 3. Handle SCX Batch Errors

SCX responses can contain multiple errors in a single response. Always iterate the full `errorList`:

```typescript theme={null}
const response = await fetch(
  "https://scx.api.jtl-software.com/v1/seller/channel/orders",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${authToken}`,
      "Accept-Language": "en",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ orderList }),
  }
);
 
if (!response.ok) {
  const body = await response.json();
 
  if (body.errorList) {
    for (const error of body.errorList) {
      console.error(`[${error.code}] ${error.message}`);
      // Handle each error, e.g., flag the specific item that failed
    }
  }
}
```

### 4. Retry with Exponential Backoff for Server Errors

For 5xx errors and 429 (rate limit), increase the wait time between each retry:

```typescript theme={null}
async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3
): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);
 
    // Don't retry client errors (except 429)
    if (
      response.status >= 400 &&
      response.status < 500 &&
      response.status !== 429
    ) {
      return response;
    }
 
    // Success: return immediately
    if (response.ok) {
      return response;
    }
 
    // Server error or rate limit: retry with backoff
    if (attempt < maxRetries) {
      const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
      console.warn(
        `Request failed (${response.status}). Retrying in ${delay}ms...`
      );
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
 
  throw new Error(`Request failed after ${maxRetries + 1} attempts`);
}
```

**Backoff schedule:**

| Attempt   | Wait time |
| --------- | --------- |
| 1st retry | 1 second  |
| 2nd retry | 2 seconds |
| 3rd retry | 4 seconds |

<Tip>
  Add **jitter** (a small random delay) to prevent multiple clients from retrying at the same time. Replace `Math.pow(2, attempt) * 1000` with `Math.pow(2, attempt) * 1000 + Math.random() * 500`.
</Tip>

### 5. Log Errors with Context

When an API call fails, log enough information to reproduce the failure later. At minimum, capture:

* The endpoint that was called (URL or operation name)
* The HTTP status code
* The error code and message from the response body
* Any field-level validation errors
* The tenant ID the request was made on behalf of
* A timestamp

Avoid logging access tokens, session tokens, client secrets, or personally identifiable information from request payloads.

***

## Quick Reference

| Situation                      | What to do                                                                      |
| ------------------------------ | ------------------------------------------------------------------------------- |
| `200 OK` with GraphQL `errors` | Check `errors` array. Process partial `data` if available, or treat as failure. |
| `400` Bad Request              | Check your request body, headers, and parameters. Fix and resend.               |
| `401` Unauthorized             | Missing or expired access token, invalid credentials                            |
| `403` Forbidden                | Check your app's scopes. You may need additional permissions.                   |
| `404` Not Found                | Verify the URL and resource ID. The resource may have been deleted.             |
| `409` Conflict                 | The resource was modified by another request. Re-fetch and retry.               |
| `422` Validation error         | Read the error details. Fix the data and resend.                                |
| `429` Rate limited             | Wait and retry with exponential backoff.                                        |
| `5xx` Server error             | Not your fault. Retry with exponential backoff.                                 |

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Rate Limiting" icon="gauge" href="/guides/essentials/common-patterns/rate-limiting">
    Understand request quotas and how to handle 429 responses.
  </Card>

  <Card title="Pagination" icon="arrow-right" href="/guides/essentials/common-patterns/pagination">
    Navigate large result sets with page-based and cursor-based pagination.
  </Card>

  <Card title="Using Platform APIs" icon="database" href="/guides/cloud-apps/using-platform-apis">
    Full guide to calling REST and GraphQL APIs from your Cloud App.
  </Card>

  <Card title="Webhooks" icon="bell" href="/guides/essentials/common-patterns/webhooks">
    Handle real-time events from the JTL platform.
  </Card>
</CardGroup>
