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.
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.
Successful POST requests that create a new resource
204 No Content
Success, no body
Successful 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.
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": "..."}
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 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:
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)
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.
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 } ]}
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)
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.
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.
const response = await fetch( "https://api.jtl-cloud.com/erp/v2/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, "X-Tenant-ID": tenantId, }, body: JSON.stringify({ operationName: "GetERPItems", query: ITEMS_QUERY, variables: { first: 10 }, }), });// HTTP-level errors (network, auth, server)if (!response.ok) { throw new Error(`HTTP error (${response.status})`);}const result = await response.json();// GraphQL-level errors (query errors, validation, field errors)if (result.errors && result.errors.length > 0) { for (const error of result.errors) { console.error( `GraphQL error: ${error.message}`, error.path ? `at ${error.path.join(".")}` : "" ); } // Decide whether to use partial data or treat as a full failure if (!result.data) { throw new Error("GraphQL request failed completely"); }}// Process data (may be partial if errors were present)const items = result.data.QueryItems;
What this does: Checks two layers of errors. First, HTTP-level errors (network failures, auth issues). Second, GraphQL-level errors inside the response body. If data is present alongside errors, the response is a partial success and your app can decide whether to use the available data or treat it as a failure.
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.
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:
Attempt
Wait time
1st retry
1 second
2nd retry
2 seconds
3rd retry
4 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.