Skip to main content
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

CodeMeaningWhen you’ll see it
200 OKRequest succeededSuccessful GET, PUT, PATCH, or GraphQL requests
201 CreatedResource createdSuccessful POST requests that create a new resource
204 No ContentSuccess, no bodySuccessful DELETE requests or updates with no response body
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 section below.

Client Error Codes (4xx)

These indicate a problem with your request. Fix the request before retrying.
CodeMeaningCommon cause
400 Bad RequestInvalid request format or parametersMalformed JSON, missing required fields, invalid data types
401 UnauthorizedAuthentication failedMissing X-Tenant-ID header, missing or expired access token, invalid credentials
403 ForbiddenInsufficient permissionsValid token but lacking the required scope for this endpoint
404 Not FoundResource does not existWrong URL, deleted resource, or incorrect ID
409 ConflictResource conflictAttempting to create a resource that already exists, or a concurrent modification
412 Precondition FailedRequired precondition missingMissing X-Tenant-ID header or other required headers
422 Unprocessable EntityValidation failedRequest is well-formed but the data does not pass business rules
429 Too Many RequestsRate limit exceededToo 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.
CodeMeaningWhat to do
500 Internal Server ErrorUnexpected server failureRetry with exponential backoff. If persistent, contact support.
502 Bad GatewayUpstream service unavailableRetry after a short delay. Usually transient.
503 Service UnavailableService temporarily downRetry with exponential backoff.
504 Gateway TimeoutRequest timed out upstreamRetry 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:
{
  "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": "..."
}
FieldDescription
errorCodeA machine-readable error identifier (e.g., VALIDATION_ERROR, NOT_FOUND)
validationErrorsAn object mapping field names to validation error messages. Empty {} if no validation errors.
errorsAdditional error details. Empty {} in most cases.
errorMessageA human-readable description of the error
stacktraceServer-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:
{
  "data": {
    "QueryItems": {
      "nodes": [ ... ],
      "totalCount": 674
    }
  }
}
An error response looks like this:
{
  "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:
{
  "data": {
    "QueryItems": {
      "nodes": [ ... ],
      "totalCount": 674
    },
    "CreateCategory": null
  },
  "errors": [
    {
      "message": "Category name is required",
      "path": ["CreateCategory"]
    }
  ]
}
FieldDescription
dataThe result of the operation. May be null if the entire operation failed, or contain partial results if some fields succeeded.
errorsAn 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[].messageA human-readable description of the error
errors[].pathArray of field names tracing the error to a specific part of the query
errors[].extensions.codeA machine-readable error code (e.g., VALIDATION_ERROR, UNAUTHORIZED)
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.

SCX Channel API

The SCX Channel API returns errors as an errorList array, which can contain multiple errors in a single response:
{
  "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
    }
  ]
}
FieldDescription
codeAn error code identifying the type of error (e.g., VAL100 for validation errors)
messageA human-readable description, including the field path
severityThe severity level (e.g., error)
hintAn optional hint for resolving the error (may be null)
Key differences between the three formats:
FeatureCloud-ERP RESTCloud-ERP GraphQLSCX
Error locationHTTP status code + bodyerrors array in body (HTTP is 200)HTTP status code + body
Multiple errorsSingle errorMessageMultiple items in errors arrayMultiple items in errorList
Field mappingvalidationErrors object keyed by fieldpath array on each errorField path embedded in message
Machine-readable codeerrorCode fieldextensions.codecode 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

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.
const response = await fetch(
  "https://api.jtl-cloud.com/erp/v2/invoices",
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "X-Tenant-ID": tenantId,
    },
  }
);

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

  if (
    error.validationErrors &&
    Object.keys(error.validationErrors).length > 0
  ) {
    for (const [field, message] of Object.entries(
      error.validationErrors
    )) {
      console.error(`  - ${field}: ${message}`);
    }
  }

  return;
}

const data = await response.json();
What this does: Checks the HTTP status first. If the request failed, it parses the error body and logs both the top-level message and any field-level validation errors.

2. Handle Auth Errors (401) with Token Refresh

A 401 typically means your access token has expired. Refresh the token and retry once:
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;
}
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.

3. Handle SCX Batch Errors

SCX responses can contain multiple errors in a single response. Always iterate the full errorList:
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:
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:
AttemptWait time
1st retry1 second
2nd retry2 seconds
3rd retry4 seconds
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.

5. Log Errors with Context

Include enough context to reproduce the failure:
console.error("API request failed", {
  endpoint: url,
  status: response.status,
  errorCode: errorBody.errorCode,
  errorMessage: errorBody.errorMessage,
  validationErrors: errorBody.validationErrors,
  tenantId,
  timestamp: new Date().toISOString(),
});

Quick Reference

SituationWhat to do
200 OK with GraphQL errorsCheck errors array. Process partial data if available, or treat as failure.
400 Bad RequestCheck your request body, headers, and parameters. Fix and resend.
401 UnauthorizedMissing or expired access token, invalid credentials
403 ForbiddenCheck your app’s scopes. You may need additional permissions.
404 Not FoundVerify the URL and resource ID. The resource may have been deleted.
409 ConflictThe resource was modified by another request. Re-fetch and retry.
422 Validation errorRead the error details. Fix the data and resend.
429 Rate limitedWait and retry with exponential backoff.
5xx Server errorNot your fault. Retry with exponential backoff.

Next Steps

Rate Limiting

Understand request quotas and how to handle 429 responses.

Pagination

Navigate large result sets with page-based and cursor-based pagination.

Using Platform APIs

Full guide to calling REST and GraphQL APIs from your Cloud App.

Webhooks

Handle real-time events from the JTL platform.