@jtl-software/cloud-apps-rest-sdk
v0.0.1
Published
TypeScript REST client for the JTL Wawi ERP Cloud API, generated with Microsoft Kiota.
Readme
@jtl-software/cloud-apps-rest-sdk
TypeScript REST client for the JTL ERP API. Generated with Microsoft Kiota from the official OpenAPI specification.
Installation
npm install @jtl-software/cloud-apps-rest-sdkThat's it. The SDK bundles the Kiota HTTP transport (@microsoft/kiota-http-fetchlibrary), JSON serializer (@microsoft/kiota-serialization-json) and abstractions as direct dependencies. You don't need to install or wire them yourself.
Requires Node.js 20 or later. ESM only ("type": "module").
Quick start
import {
createJtlClient,
createTenantContext,
} from "@jtl-software/cloud-apps-rest-sdk";
const { getCurrentTenant, withTenant } = createTenantContext();
const client = createJtlClient({
auth: {
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
},
tenantId: getCurrentTenant,
});
// In your request handler, pass the tenant as the first argument of the withTenant scope for the SDK to pick up.
await withTenant(tenantIdFromRequest, async () => {
const info = await client.v2.info.get();
console.log(info);
});That's the whole setup. The SDK handles the OAuth 2.0 client-credentials flow internally: it fetches a token on first use, caches it, refreshes it ~60 seconds before expires_in, and de-duplicates concurrent fetches. Every outgoing request gets Authorization: Bearer <token> and X-Tenant-ID (resolved per request, see Tenant ID).
Configuration
createJtlClient(options) accepts:
| Option | Type | Description |
| --- | --- | --- |
| auth | ClientCredentialsOptions \| string \| (() => string \| Promise<string>) | Required. See Authentication below. |
| tenantId | () => string \| undefined | Resolver called on every request, injects the result as X-Tenant-ID. See Tenant ID. |
| baseUrl | string | Optional. Overrides the API base URL. Defaults to https://api.jtl-cloud.com/erp. |
Authentication
auth accepts three shapes:
1. Client-credentials (recommended)
Pass { clientId, clientSecret } and the SDK runs the OAuth 2.0 client-credentials flow for you:
createJtlClient({
auth: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
// Optional:
// tokenUrl: "https://auth.jtl-cloud.com/oauth2/token", // default shown
// scope: "...",
// renewBeforeExpiry: 60_000, // ms before expires_in to fetch a new token
},
});2. Static bearer token
If you already have a token (e.g. fetched by your auth gateway):
createJtlClient({ auth: process.env.ACCESS_TOKEN! });The SDK won't refresh this. Useful for short-lived scripts and testing.
3. Custom async resolver
For any other auth flow, pass a function. Return a token, the SDK calls it on every request, you decide what to cache:
createJtlClient({
auth: async () => {
return await myAuthService.getToken();
},
});Tenant ID
A backend using this SDK proxies requests from many users on different tenants to the JTL ERP API. The same cloud-app OAuth credentials authenticate against all of them, only the X-Tenant-ID header on each outbound request changes. Share one JtlClient (and its token cache) across all tenants, and resolve the tenant per request via createTenantContext(), which wraps Node's AsyncLocalStorage.
createTenantContext() returns { withTenant, getCurrentTenant }:
withTenant(tenantId, fn): executesfnwithtenantIdas the active value for any async work it triggers.getCurrentTenant(): returns the active tenant ID, orundefinedif called outside awithTenantscope.
Pass getCurrentTenant as the tenantId option on createJtlClient, then call withTenant from your request middleware. If getCurrentTenant() returns undefined at request time the SDK omits the header. The ERP API rejects most v2 endpoints without X-Tenant-ID, so make sure your middleware sets it before any handler that calls the SDK.
This mirrors the C# SDK's AddHttpContextTenantContext.
Express example
How your backend obtains a tenant ID is entirely your app's concern, the SDK doesn't care. It could come from:
- a JTL session token your frontend forwards to you (the example below),
- your own app's JWT (cookie or
Authorizationheader) that you signed yourself, - a database lookup keyed by your own user ID,
- any other mechanism that yields a string at request time.
The example below shows the JTL-session-token shape because it's the common case for cloud apps. It uses jose for JWT verification, but if you already have your own auth/session middleware that knows the tenant, just pass that value into withTenant instead, the rest of the wiring is the same.
import express, { type NextFunction, type Request, type Response } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
import {
createJtlClient,
createTenantContext,
} from "@jtl-software/cloud-apps-rest-sdk";
const { getCurrentTenant, withTenant } = createTenantContext();
const client = createJtlClient({
auth: {
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
},
tenantId: getCurrentTenant, // resolved per request
});
// JTL signs session tokens with EdDSA. Cache the JWKS once at startup;
// jose handles refresh internally.
const JWKS = createRemoteJWKSet(
new URL("https://api.jtl-cloud.com/account/.well-known/jwks.json"),
);
async function tenantFromSessionToken(token: string): Promise<string> {
const { payload } = await jwtVerify(token, JWKS);
return (payload as { tenantId: string }).tenantId;
}
const app = express();
// Verify the inbound session token from your frontend, pull the tenant
// ID out of its payload, and scope it for the downstream handlers.
// The header name (`X-Session-Token` here) is your app's choice;
// don't confuse it with the ERP API's `X-Tenant-ID`, which the SDK
// sets for you.
app.use(async (req: Request, res: Response, next: NextFunction) => {
const sessionToken = req.header("x-session-token");
if (!sessionToken) {
res.status(401).json({ error: "Missing X-Session-Token header" });
return;
}
try {
const tenantId = await tenantFromSessionToken(sessionToken);
withTenant(tenantId, next);
} catch {
res.status(401).json({ error: "Invalid session token" });
}
});
app.get("/companies", async (_req, res) => {
// X-Tenant-ID injected automatically by the SDK from the current scope.
res.json(await client.v2.companies.get());
});
app.listen(3000);Only the middleware changes if you obtain the tenant ID differently, the createJtlClient setup and the handlers stay identical. The same pattern works with Fastify (fastify.addHook("onRequest", ...)), Hono (app.use(async (c, next) => withTenant(id, next))), or any other framework with per-request middleware.
API shape
The client exposes the v2 surface of the JTL ERP API as a fluent builder. Examples:
await client.v2.companies.get();
await client.v2.salesOrders.get({ queryParameters: { top: 50 } });
await client.v2.salesOrders.byId("ORDER-1").get();
await client.v2.salesOrders.post(newOrder);Refer to the generated .d.ts files in your editor for the full surface.
Advanced
The high-level factory above suits the common case. If you need more control, the SDK also exports the building blocks:
createClientCredentialsTokenProvider(options): returns the cached() => Promise<string>used internally. Useful if you want to share one token cache across multiple clients or pass the function as a customauthresolver.createDefaultAdapter(options): returns the underlyingRequestAdapterso you can construct the generated client yourself or chain additional middleware.createApiClient(adapter): the bare-bones constructor from the generated code; takes anyRequestAdapterand gives you the typed client.createTenantContext(): AsyncLocalStorage helper for per-request tenant scoping. See Tenant ID.
Documentation
- Source and full integration guide: github.com/jtl-software/jtl-platform-erp-sdk
- Companion C# packages:
JTL.Platform.Erp,JTL.Platform.Auth - Microsoft Kiota documentation: learn.microsoft.com/openapi/kiota
Support
Issues and feature requests: github.com/jtl-software/jtl-platform-erp-sdk/issues
License
Copyright (c) JTL-Software GmbH. See the repository for license terms.
