Skip to main content

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.

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.
C# samples target .NET 8 with implicit usings enabled and use raw string literals (C# 11+). PHP samples target PHP 8.1+ and require the guzzlehttp/guzzle Composer dependency. TypeScript samples assume a modern Node.js or browser environment with fetch available globally. These examples use raw HTTP requests. If your project already uses HTTP or GraphQL client libraries, the same headers and payload structure apply.

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. The shape is identical across languages, only the syntax for sending the HTTP request differs.
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: `query MyOperation { ... }`,
    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.
Throughout this guide, GraphQL query and mutation strings are shown in plain GraphQL syntax. The string itself is the same across languages; only how you embed it (a template literal in TypeScript, a verbatim string in C#, a heredoc in PHP) differs. The variables object follows the JSON serialization conventions of your language.

Querying Data

Queries read data from the ERP without modifying it. Use them to fetch items, customers, orders, categories, and other resources. The examples in this section use the query() / QueryAsync() / JtlGraphQL::query() helper built in Building a Reusable API Client. The GraphQL operation itself is language-agnostic. Only the variables object differs depending on your language’s JSON syntax and conventions.

Basic Query

Fetch a list of products with their SKU and name:
query GetERPItems($first: Int, $order: [ItemListItemSortInput!]) {
  QueryItems(first: $first, order: $order) {
    nodes {
      id
      sku
      name
    }
    totalCount
  }
}
Call it with variables for page size and sort order:
const items = await query(tenantId, "GetERPItems", ITEMS_QUERY, {
  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. A minimal query with just IDs and names:
query GetItemsMinimal {
  QueryItems(first: 50) {
    nodes {
      id
      name
    }
    totalCount
  }
}
A detailed query with pricing and stock data:
query GetItemsDetailed {
  QueryItems(first: 50) {
    nodes {
      id
      sku
      name
      gtin
      salesPriceNet
      averagePurchasePriceNet
      stockTotal
      stockInOrders
      manufacturerName
      productGroupName
      taxClassName
    }
    totalCount
  }
}
The same query() helper handles both, only the operation string changes.

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.
// Alphabetical by name
const variables = { first: 20, order: [{ name: "ASC" }] };

// Reverse alphabetical by SKU
const variables = { first: 20, order: [{ sku: "DESC" }] };
In C#, build the variables with new { first = 20, order = new[] { new { name = "ASC" } } }. In PHP, use ['first' => 20, 'order' => [['name' => 'ASC']]].

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" },
  },
};
Filter operators vary by field type, such as contains and eq for strings, gte and lte for numbers, in for enum-like fields. See the GraphQL Playground for the full schema of available operators on each query.

Pagination

The GraphQL API uses cursor-based pagination. Use first to set the page size and after to fetch subsequent pages. Add pageInfo to your query to get pagination metadata:
query GetERPItems($first: Int, $after: String) {
  QueryItems(first: $first, after: $after) {
    nodes {
      id
      sku
      name
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}
Then loop by passing the previous response’s endCursor as after until hasNextPage is false:
let cursor: string | null = null;

while (true) {
  const page = await query<ItemsResponse>(tenantId, "GetERPItems", ITEMS_QUERY, {
    first: 20,
    after: cursor,
  });

  // Process page.QueryItems.nodes here

  if (!page.QueryItems.pageInfo.hasNextPage) break;
  cursor = page.QueryItems.pageInfo.endCursor;
}
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
For the full pagination pattern including REST-style page-based pagination, see Pagination.

Mutating Data

Mutations create, update, or delete ERP data. They follow the same request structure as queries but use the mutation keyword. Use the same query() helper as it handles both.

Creating a Resource

Create a new category in the ERP:
mutation CreateCategory($request: CreateCategoryCommandRequestInput) {
  CreateCategory(request: $request) {
    categoryId
  }
}
Call it with the category details:
const result = await query(tenantId, "CreateCategory", CREATE_CATEGORY_MUTATION, {
  request: {
    name: "Summer Collection",
    sortNumber: 1,
    parentId: null,
  },
});

const categoryId = result.CreateCategory.categoryId;
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:
mutation UpdateCategory($request: UpdateCategoryCommandRequestInput) {
  UpdateCategory(request: $request)
}
Call it with the resource ID and the fields you want to change:
await query(tenantId, "UpdateCategory", UPDATE_CATEGORY_MUTATION, {
  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?.length) {
  // Handle errors (log, retry, or surface to the user)
  throw new Error(`GraphQL error: ${errors.map(e => e.message).join(", ")}`);
}

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 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?.length) {
    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 reusable function. It handles access token caching, sets all required headers, checks for both HTTP-level and GraphQL-level errors, and returns parsed data. Use it throughout your app instead of repeating the request boilerplate.

Using the Client

// Querying items
interface ItemsResponse {
  QueryItems: {
    nodes: Array<{ id: string; sku: string; name: string }>;
    totalCount: number;
  };
}

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

// items.QueryItems.nodes is fully typed

// Creating a category
interface CreateCategoryResponse {
  CreateCategory: { categoryId: string };
}

const result = await query<CreateCategoryResponse>(
  tenantId,
  'CreateCategory',
  `mutation CreateCategory($request: CreateCategoryCommandRequestInput) {
    CreateCategory(request: $request) { categoryId }
  }`,
  { request: { name: 'Summer Collection', sortNumber: 1 } },
);

const categoryId = 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.