Call the JTL Cloud and JTL-Wawi APIs from your backend using REST for authentication and GraphQL for ERP data.
The JTL platform exposes two API surfaces, each serving a different purpose:
API
Purpose
Format
REST API
Authentication, token management, and account operations
Standard REST (JSON over HTTP)
GraphQL API
Querying and mutating ERP data (items, categories, customers, orders, stock)
GraphQL (single endpoint)
In practice, your backend uses the REST API to obtain access tokens, then uses those tokens to query and mutate ERP data through GraphQL.For the conceptual foundations (pagination, error handling, versioning), see the Essentials section.
The REST API handles authentication and account-level operations. You already use it to obtain access tokens (see Authentication & Login). It also provides endpoints for account management, JWKS key retrieval, and other platform services.
Fetch the public keys used to verify session tokens:
curl -X GET https://api.jtl-cloud.com/account/.well-known/jwks.json \ -H "Authorization: Bearer <access_token>"
What this does: Returns the JSON Web Key Set (JWKS) containing the public keys your backend uses to verify session tokens from the AppBridge. Cache this response and refresh it when key verification fails.
All ERP data operations (reading items, creating categories, updating customers, querying orders) go through the GraphQL API. It provides a single endpoint for both queries (reads) and mutations (writes).
What this does: Sends a GraphQL operation to the ERP API with your access token and tenant ID. The response contains a data object with the results, or an errors array if something went wrong.
What this does: Fetches the first 10 items sorted alphabetically by name. The response includes a nodes array with each item’s id, sku, and name, plus a totalCount of all matching items.
Opaque cursor string to pass as after for the next page
totalCount
Total number of matching items across all pages
What this does: Fetches a page of results and tells you whether more pages exist. To paginate, keep passing the endCursor from the previous response as the after variable until hasNextPage is false.
What this does: Creates a new top-level category named “Summer Collection”. The response returns the categoryId of the newly created resource. Set parentId to an existing category ID to create a subcategory.
What this does: Updates the category’s name and sort order. The id field is required to identify which resource to update. Only the fields you include in the request are modified; omitted fields remain unchanged. The mutation returns true on success.
GraphQL errors are returned in the errors array, even when the HTTP status is 200. Always check for errors in the response body, not just the status code.
const { data, errors } = await response.json();if (errors && errors.length > 0) { console.error('GraphQL errors:', errors); // Handle errors (log, retry, or surface to the user)}if (data) { // Process successful response}
What this does: Checks for errors in the GraphQL response before processing data. Unlike REST APIs where errors are indicated by HTTP status codes, GraphQL can return partial data alongside errors. Always inspect both fields.For a complete reference on error formats and retry strategies, see Error Handling.
As your app grows, you’ll want a helper function that handles headers, authentication, and error checking in one place.
// lib/jtl-graphql.tsimport { getCachedAccessToken } from './token-cache';const GRAPHQL_ENDPOINT = 'https://api.jtl-cloud.com/erp/v2/graphql';interface GraphQLResponse<T> { data: T | null; errors?: Array<{ message: string }>;}export async function query<T>( tenantId: string, operationName: string, queryString: string, variables?: Record<string, unknown>,): Promise<T> { const accessToken = await getCachedAccessToken(); const response = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, 'X-Tenant-Id': tenantId, }, body: JSON.stringify({ operationName, query: queryString, variables, }), }); if (!response.ok) { throw new Error(`API request failed (${response.status})`); } const result: GraphQLResponse<T> = await response.json(); if (result.errors && result.errors.length > 0) { throw new Error( `GraphQL error: ${result.errors.map((e) => e.message).join(', ')}`, ); } if (!result.data) { throw new Error('No data returned from GraphQL API'); } return result.data;}
What this does: Wraps the common GraphQL request pattern into a typed, reusable function. It handles access token caching, sets all required headers, checks for both HTTP-level and GraphQL-level errors, and returns typed data. Use it throughout your app instead of repeating the fetch boilerplate.