Skip to main content
The JTL platform exposes two API surfaces, each serving a different purpose:
APIPurposeFormat
REST APIAuthentication, token management, and account operationsStandard REST (JSON over HTTP)
GraphQL APIQuerying 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.

Required Headers

Every API request to the JTL Cloud Platform requires a specific set of headers. Missing or incorrect headers result in 401 or 400 errors.
HeaderValueRequiredDescription
AuthorizationBearer <access_token>YesJWT access token from the client credentials flow
X-Tenant-ID<tenantId>YesIdentifies which merchant’s data to access. Get this from the verified session token.
Content-Typeapplication/jsonYes (for POST/mutations)Request body format
The X-Tenant-ID is what scopes your request to a specific merchant. The tenant ID determines whose data you’re reading or writing.

REST API

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.

Base URL

REST endpoints use URL-path versioning:
https://api.jtl-cloud.com/erp/v2/{endpoint}
Always target /erp/v2/ for new integrations. See Versioning for the full version history.

Example: Token Request

This is the most common REST call your app makes. See Authentication & Login for the full implementation with caching.
curl -X POST https://auth.jtl-cloud.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -d "grant_type=client_credentials"

Example: JWKS Retrieval

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.

GraphQL API

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).

Endpoint

POST https://api.jtl-cloud.com/erp/v2/graphql
All GraphQL requests are POST requests to this single URL, regardless of whether you’re reading or writing data.

Making a Request

Every GraphQL request has the same structure: an operationName, a query string, and optional variables.
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: "MyOperation",
    query: `...`,
    variables: { ... },
  }),
});
 
const { data, errors } = await response.json();
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.

Querying Data

Queries read data from the ERP without modifying it. Use them to fetch items, customers, orders, categories, and other resources.

Basic Query

Fetch a list of products with their SKU and name:
const query = `
  query GetERPItems($first: Int, $order: [ItemListItemSortInput!]) {
    QueryItems(first: $first, order: $order) {
      nodes {
        id
        sku
        name
      }
      totalCount
    }
  }
`;
 
const variables = {
  first: 10,
  order: [{ name: "ASC" }],
};
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.
{
  "data": {
    "QueryItems": {
      "nodes": [
        {
          "id": "895d2d4d-4ed4-44c7-ac61-ebec01000000",
          "sku": "AR2016041-VKO",
          "name": "Men's T-shirt"
        },
        {
          "id": "895d2d4d-4ed4-44c7-ac61-ebec06000000",
          "sku": "AR2016041-002",
          "name": "Men's T-shirt orange S"
        }
      ],
      "totalCount": 674
    }
  }
}

Requesting Specific Fields

One of GraphQL’s key advantages is that you only fetch the fields you need. This reduces payload size and improves performance.
# Minimal: just IDs and names
query {
  QueryItems(first: 50) {
    nodes {
      id
      name
    }
    totalCount
  }
}
 
# Detailed: include pricing and stock data
query {
  QueryItems(first: 50) {
    nodes {
      id
      sku
      name
      gtin
      salesPriceNet
      averagePurchasePriceNet
      stockTotal
      stockInOrders
      manufacturerName
      productGroupName
      taxClassName
    }
    totalCount
  }
}

Sorting

Control the order of results using the order variable. Pass an array of sort objects with the field name as the key and ASC or DESC as the value.
const variables = {
  first: 20,
  order: [{ name: "ASC" }],   // Alphabetical by name
};
const variables = {
  first: 20,
  order: [{ sku: "DESC" }],   // Reverse alphabetical by SKU
};

Filtering

Use the where variable to filter results based on field conditions. Filters use a structured input type specific to each query.
const variables = {
  first: 20,
  where: {
    name: { contains: "T-shirt" },
  },
};

Pagination

The GraphQL API uses cursor-based pagination. Use first to set the page size and after to fetch subsequent pages.
// First page
const firstPage = {
  first: 20,
};
 
// Next page (use endCursor from the previous response)
const nextPage = {
  first: 20,
  after: "eyJza2lwIjoyMH0=",
};
The response includes a pageInfo object with pagination metadata:
query GetERPItems($first: Int, $after: String) {
  QueryItems(first: $first, after: $after) {
    nodes {
      id
      sku
      name
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}
FieldDescription
hasNextPagetrue if more results exist beyond this page
endCursorOpaque cursor string to pass as after for the next page
totalCountTotal 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.

Mutating Data

Mutations create, update, or delete ERP data. They follow the same request structure as queries but use the mutation keyword.

Creating a Resource

Create a new category in the ERP:
const query = `
  mutation CreateCategory($request: CreateCategoryCommandRequestInput) {
    CreateCategory(request: $request) {
      categoryId
    }
  }
`;
 
const variables = {
  request: {
    name: "Summer Collection",
    sortNumber: 1,
    parentId: null,
  },
};
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.

Updating a Resource

Update an existing category:
const query = `
  mutation UpdateCategory($request: UpdateCategoryCommandRequestInput) {
    UpdateCategory(request: $request)
  }
`;
 
const variables = {
  request: {
    id: "cat-abc-123",
    name: "Winter Collection",
    sortNumber: 2,
  },
};
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.

Handling Responses

Every GraphQL response has the same top-level structure:
{
  "data": { ... },
  "errors": [ ... ]
}

Success

A successful response contains the data object with your requested fields. The errors field is either absent or an empty array.

Errors

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.

Building a Reusable API Client

As your app grows, you’ll want a helper function that handles headers, authentication, and error checking in one place.
// lib/jtl-graphql.ts

import { 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.

Using the Client

// Querying items
const items = await query<{
	QueryItems: { nodes: Item[]; totalCount: number };
}>(
	tenantId,
	'GetERPItems',
	`query GetERPItems($first: Int) {
    QueryItems(first: $first) {
      nodes { id sku name }
      totalCount
    }
  }`,
	{ first: 10 },
);

console.log(items.QueryItems.nodes);

// Creating a category
const result = await query<{ CreateCategory: { categoryId: string } }>(
	tenantId,
	'CreateCategory',
	`mutation CreateCategory($request: CreateCategoryCommandRequestInput) {
    CreateCategory(request: $request) { categoryId }
  }`,
	{ request: { name: 'New Category', sortNumber: 1 } },
);

console.log(result.CreateCategory.categoryId);

What’s Next

GraphQL Playground

Try queries and mutations interactively with autocomplete and schema docs.

Handling Webhooks

Respond to lifecycle events and AppBridge messages in your app.

Error Handling

Understand error formats, retry strategies, and validation errors.

Rate Limiting

Stay within request limits and handle throttling gracefully.

Best Practices

Production patterns for API usage, caching, and performance.