# Salesforce B2C Commerce API (SCAPI) Client

Storefront Next includes a type-safe API client for calling Salesforce B2C Commerce Shopper APIs, including custom APIs, from your storefront. This client provides full TypeScript support with autocomplete, type checking, and semantic operation names.

## What Is SCAPI?

The B2C Commerce API (SCAPI) is a collection of RESTful APIs for interacting with B2C Commerce instances. Also known as Salesforce Commerce API or simply Commerce API, it provides endpoints for products, search, baskets, customers, orders, and so on. Create custom APIs to modify the built-in SCAPI endpoints. For example, add filters or modify responses before they get sent back to the calling application. See [B2C Commerce API (SCAPI)](/docs/commerce/b2c-commerce/guide/scapi-get-started.md) and [Custom APIs](/docs/commerce/b2c-commerce/guide/custom-apis.md).

The SCAPI client in `@salesforce/storefront-next-runtime/scapi` wraps these APIs with:

- Minimal bundle size: Built on [openapi-fetch](https://openapi-ts.dev/openapi-fetch/), a lightweight fetch client with near-zero runtime overhead.
- Type-safe operations: Full TypeScript inference from OpenAPI specifications.
- Semantic method names: Call `getProduct()` instead of `GET /products/{id}`.
- Automatic parameter injection: Common parameters like `organizationId` and `siteId` are handled for you.
- Built-in authentication helpers: Guest login, registered login, social login, passwordless login, token refresh, and password reset.

## Creating API Clients

In a Storefront Next project, use the template `createApiClients()` wrapper in route loaders and actions. The wrapper reads site configuration, credentials, and locale from context. It also registers template middleware, including non-personalized response classification.

```typescript
import { createApiClients } from "@/lib/api-clients.server";

export async function loader({ context }: LoaderFunctionArgs) {
  const clients = createApiClients(context);
  // Call SCAPI with clients.
}
```

When you create a storefront in Business Manager, a private SLAS client ID and secret are created for you. See [Create Storefront Next in Business Manager with a GitHub Repository](/docs/commerce/b2c-commerce/guide/sfnext-quick-start-create-bm-github.md) or [Create Storefront Next in Business Manager with Local Code](/docs/commerce/b2c-commerce/guide/sfnext-quick-start-create-bm.md).

### Configuration Options

The template `createApiClients()` wrapper reads these values from the Storefront Next configuration and request context. If you create clients directly, pass the values to `createCommerceApiClients()`.

| Parameter        | Required | Description                                                                              |
| ---------------- | -------- | ---------------------------------------------------------------------------------------- |
| `baseUrl`        | Yes      | Commerce API base URL                                                                    |
| `organizationId` | Yes      | Your organization ID (automatically added to path parameters)                            |
| `siteId`         | Yes      | Your site ID (automatically added to query parameters)                                   |
| `clientId`       | Yes      | Shopper Login and Session API (SLAS) client ID                                           |
| `redirectUri`    | Yes      | OAuth redirect URI                                                                       |
| `locale`         | No       | Locale for requests (for example, `en-US`)                                               |
| `clientSecret`   | No       | SLAS client secret. See [Public vs Private SLAS Client](#public-vs-private-slas-client). |

### Public vs Private SLAS Client

The `clientSecret` parameter determines which SLAS authentication flow the client uses.

- Public client (no secret): When `clientSecret` is omitted, the auth helpers use the public client flow with Proof Key for Code Exchange (PKCE). This behavior is suitable for client-side apps where the secret can't be kept secure.

- Private client (with secret): When `clientSecret` is provided, the auth helpers use the private client flow. This behavior enables extra features, such as passwordless login, and is suitable for server-side apps where the secret can be kept secure.

## Replace the Template API Layer

Use `createCommerceApiClients()` directly only when you replace or bypass the template API layer. Configure the clients and required middleware yourself.

```typescript
import { createCommerceApiClients } from "@salesforce/storefront-next-runtime/scapi";

const clients = createCommerceApiClients({
  baseUrl: "https://shortcode.api.commercecloud.salesforce.com",
  organizationId: "f_ecom_xxx",
  siteId: "RefArch",
  clientId: "your-slas-client-id",
  redirectUri: "https://yoursite.com/callback",
  locale: "en-US", // Optional
  clientSecret: process.env.COMMERCE_API_SLAS_SECRET, // Optional, for private SLAS
});
```

### Adding the Access Token to Requests

Clients created outside the template `createApiClients()` wrapper don't receive the template authentication middleware. After you get an access token, add it to API requests using middleware. Skip SLAS authentication endpoints because they handle their own authorization.

```typescript
import { SLAS_AUTH_ENDPOINTS } from "@salesforce/storefront-next-runtime/scapi";

clients.use({
  onRequest({ request }) {
    // Skip auth header for SLAS endpoints (they handle their own auth)
    const url = new URL(request.url);
    const isSlasAuthEndpoint = SLAS_AUTH_ENDPOINTS.some((path) => url.pathname.includes(path));
    if (isSlasAuthEndpoint) {
      return request;
    }

    request.headers.set("Authorization", `Bearer ${accessToken}`);
    return request;
  },
});
```

## Authentication

The Storefront Next template uses server middleware to manage guest login, token refresh, registered sessions, social login, cookies, and access-token injection. For the normal template workflow, use the template authentication services and routes. See [Authentication and Session Management](/docs/commerce/b2c-commerce/guide/sfnext-storage-sessions.md).

The client also includes an `auth` namespace with low-level SLAS helpers. These helpers are intentionally **stateless**. They don't store tokens, update template sessions, write cookies, or perform related cleanup. Use them as building blocks only when you implement a custom server-side authentication workflow.

### Guest Login

The template creates guest sessions automatically. In a custom authentication workflow, call the helper and store the returned session with your server-side session implementation.

```typescript
const tokens = await clients.auth.loginAsGuest();
// Returns: { access_token, refresh_token, usid, ... }
```

### Registered User Login

After a successful login, store the returned session with your server-side session implementation.

```typescript
const tokens = await clients.auth.loginWithCredentials({
  username: "user@example.com",
  password: "password123",
});
```

### Token Refresh

The template refreshes expired access tokens automatically. In a custom authentication workflow, persist the refreshed session returned by this helper.

```typescript
const newTokens = await clients.auth.refreshToken({
  refreshToken: storedRefreshToken,
});
```

### Logout

This helper revokes SLAS tokens. A complete logout must also clear the application session and related shopper state.

```typescript
await clients.auth.logout({
  accessToken: currentAccessToken,
  refreshToken: currentRefreshToken,
});
```

### Social Login

The template social-login workflow stores the code verifier in a secure, HTTP-only cookie and persists the returned tokens. A custom workflow must provide equivalent secure storage and session updates.

```typescript
// Step 1: Get authorization URL
const { url, codeVerifier } = await clients.auth.social.getAuthorizationUrl({
  hint: "google", // or 'facebook', 'apple', etc.
});

// Store codeVerifier securely, then redirect user to url

// Step 2: Exchange code (after user returns)
const tokens = await clients.auth.social.exchangeCode({
  code: authorizationCode,
  codeVerifier: storedCodeVerifier,
  redirectUri: "https://yoursite.com/callback",
});
```

## Fetching Data

The client automatically includes `organizationId` and `siteId` in all requests, so you only provide operation-specific parameters.

### Fetching a Product

```typescript
const { data: product } = await clients.shopperProducts.getProduct({
  params: {
    path: { id: "product-123" },
    query: {
      expand: ["availability", "prices", "variations", "images"],
      allImages: true,
    },
  },
});
```

### Search Products

```typescript
const { data: searchResults } = await clients.shopperSearch.productSearch({
  params: {
    query: {
      q: "shoes",
      limit: 24,
      offset: 0,
      sort: "best-matches",
      expand: ["prices", "images", "variations"],
    },
  },
});
```

### Fetching Categories

```typescript
const { data: category } = await clients.shopperProducts.getCategory({
  params: {
    path: { id: "mens-clothing" },
    query: { levels: 2 },
  },
});
```

## Mutations

Use the appropriate client method to perform mutations like adding items to a basket or updating customer information.

### Adding an Item to Basket

```typescript
const { data: updatedBasket } = await clients.shopperBasketsV2.addItemToBasket({
  params: {
    path: { basketId: "basket-123" },
  },
  body: [
    {
      productId: "product-456",
      quantity: 2,
    },
  ],
});
```

### Updating a Basket Item

```typescript
const { data: updatedBasket } = await clients.shopperBasketsV2.updateItemInBasket({
  params: {
    path: {
      basketId: "basket-123",
      itemId: "item-789",
    },
  },
  body: {
    quantity: 3,
  },
});
```

## Classify Non-Personalized Responses

Use non-personalized response classification in Storefront Next to explicitly tell SCAPI that you expect a request’s response to contain no shopper-specific data and be safe to share across shoppers. With page caching enabled for the site in Business Manager, SCAPI can use this classification to cache eligible responses and reuse them for equivalent requests, reducing response times and helping storefront pages load faster. Classification alone doesn’t enable caching or guarantee that a response is cached.

A non-personalized response has the same HTTP status and response body for equivalent authorized requests against the same underlying commerce state, regardless of shopper identity or Shopper Context. Its fields and headers contain no shopper-specific data or personally identifiable information (PII). Non-sensitive diagnostic metadata can differ.

The template `createApiClients()` client factory registers the classification middleware on each active client in `src/lib/api-clients.server.ts`. The middleware module is `src/lib/scapi/non-personalized-response.server.ts`. The middleware evaluates server-side SCAPI requests for non-personalized response classification. Requests that use `createCommerceApiClients()` directly don't use the template policy unless you register the middleware yourself. Existing projects must adopt the policy and classification middleware modules, then add client registration and diagnostics to `src/lib/api-clients.server.ts`. Updating the `@salesforce/storefront-next-runtime` package alone doesn't add the policy.

### Prerequisites

To use SCAPI caching with automatic non-personalized response classification:

- Storefront Next version 1.4.0 or later. See [storefront-next-template](https://github.com/SalesforceCommerceCloud/storefront-next-template) GitHub repository.
- B2C Commerce 26.9 or later.
- [Enable page caching](https://developer.salesforce.com/docs/commerce/commerce-api/guide/server-side-web-tier-caching.md#enable-caching) for the site in Business Manager. Classification alone doesn't enable caching.

### How Classification Works

With page caching enabled, SCAPI can automatically store eligible `GET` responses in the web-tier cache and the content delivery network (CDN) cache. For information about cache eligibility, time-to-live values, and cache behavior, see [SCAPI Caching](https://developer.salesforce.com/docs/commerce/commerce-api/guide/server-side-web-tier-caching.md).

The `personalized=none` query parameter is a separate instruction. It tells SCAPI to treat the request as non-personalized. SCAPI skips Shopper Context. SCAPI suppresses hook calls to `setVaryBy()`. The parameter doesn't prevent hooks from changing the response.

Storefront Next keeps this safety decision in the customer-owned policy at `src/lib/scapi/non-personalized-response-policy.server.ts`. This policy runs only on the server. The classification middleware examines the request after Storefront Next applies defaults and serializes parameters. It also examines changes from template request middleware. The supported explicit value is `personalized=none`. If the serialized request already contains a `personalized` value, including `none`, the middleware keeps the value and doesn't run automatic classification.

![SCAPI flow to mark a response as non-personalized based on the policy](https://a.sfdcstatic.com/developer-website/sfdocs/commerce-cloud/media/sfnext/sfnext-non-personalized-scapi.png)

When the policy returns `true`, the middleware adds `personalized=none`. When the policy returns `false` or fails, the middleware forwards the request unchanged and doesn't add `personalized=none`. The unchanged request is **unclassified**: Storefront Next makes no assertion that its response is personalized or non-personalized. SCAPI then applies its normal implicit-caching rules to the request. An eligible request can still use implicit caching when page caching is enabled.

Classification doesn't enable caching or select a cache time-to-live. It doesn't prove that a cache stored the response.

### Review Response Safety

Before you rely on the default policy, review each built-in transport that the policy classifies. Review all hooks, custom response fields, response headers, custom request headers, and other inputs that can change the response. Narrow or disable a default classification when a customization can vary the response by shopper.

Classify a response as non-personalized only if it is safe for all shoppers.

For an automatically evaluated request whose response contains PII, shopper identifiers, or other shopper-specific data, return `false` from the policy. The middleware then forwards the request without adding `personalized=none`. If a call site sets `personalized=none` explicitly, remove that value because the explicit value bypasses the policy. This rule applies to customer, basket, order, payment, and hook-added data. The policy can't identify every custom hook, custom field, custom header, override, or custom API. Review these customizations before you change the policy.

:::note
A client override replaces a built-in Storefront Next client with one generated from a different schema. A custom client adds a client for a new [SCAPI Custom API](https://developer.salesforce.com/docs/commerce/commerce-api/guide/custom-apis.md).
:::

For example, the default Product policy accepts an explicit list of reviewed non-personalized expansions. It rejects the same request when the list includes `prices`.

```typescript
// The default policy can classify this request as non-personalized.
await clients.shopperProducts.getProduct({
  params: {
    path: { id: "product-123" },
    query: { expand: ["images", "variations"] },
  },
});

// The default policy leaves this request unclassified.
await clients.shopperProducts.getProduct({
  params: {
    path: { id: "product-123" },
    query: { expand: ["images", "prices"] },
  },
});
```

The default policy fails closed. It classifies only exact, reviewed built-in transports. It applies these additional rules:

- Product and Product Search requests require an explicit list of reviewed non-personalized expansions. Requests with omitted, empty, unknown, or case-mismatched expansions stay unclassified. Requests with `prices` or `promotions` expansions also stay unclassified. The policy accepts the `none` expansion only by itself.
- Page Designer requests stay unclassified because visibility rules and component logic can vary by shopper.
- SCAPI schema overrides and custom clients stay unclassified, including overrides that keep a built-in client key or path.
- Personal, transactional, non-`GET`, alternate-destination, unknown, and future operations stay unclassified.

Review the policy module for the current transport identities and expansion lists. A template update can change the default policy. Your existing project keeps its policy until you adopt the update.

### Customize the Policy

Edit or replace `src/lib/scapi/non-personalized-response-policy.server.ts` to approve an override or custom client. Match the exact client key and provenance. Match the expected base path, the final request origin and path, the HTTP method, and the schema path. Also match all request inputs that can change the response. Return `false` for unknown values. Don't use a policy that returns `true` for every `GET` request. The middleware also processes personal, transactional, overridden, custom, and future operations.

For example, an exact predicate for a reviewed custom client can check its provenance, path, method, and destination. This predicate is only a starting point. Also check each query value, site, locale, custom header, hook, and response field that can change the response.

```typescript
import type { NonPersonalizedResponseClassifier } from "@/lib/scapi/non-personalized-response.server";

const isReviewedLoyaltyRequest: NonPersonalizedResponseClassifier = (input) => {
  const destination = new URL(input.destination);
  const request = new URL(input.request.url);

  return (
    input.client === "loyalty" &&
    input.provenance === "custom" &&
    input.clientBasePath === "/custom/loyalty/v1" &&
    input.schemaPath === "/offers" &&
    input.method === "GET" &&
    request.origin === destination.origin &&
    request.pathname === `${destination.pathname.replace(/\/+$/, "")}${input.schemaPath}`
  );
};
```

To disable automatic classification, return `false` from the policy or remove the middleware registration from `src/lib/api-clients.server.ts`.

An explicit `personalized=none` value bypasses automatic classification. Apply the same safety review before you set this value on an individual request. Don't use an explicit value to override a policy rejection. When you adopt the central policy, review existing call sites and remove explicit values that the policy replaces.

Classification runs after template request middleware. Per-call middleware runs after classification. Don't use per-call middleware to change a destination, path, query parameter, or header that can change the response. If you can't make the change before classification, leave the request unclassified.

When you change the policy, update `src/lib/scapi/non-personalized-response-policy.server.test.ts`. For each custom rule, test at least one request that the policy accepts and one request that it rejects. Run the focused tests from the Storefront Next project root.

```bash
pnpm test src/lib/scapi/non-personalized-response-policy.server.test.ts
```

### Verify Classification and Cache Status

1. Run `pnpm dev:log`.
2. Find the `[ApiClients] fetch` debug entry.
3. Check `personalizationMode`. The value `automatic-none` means that classification added `personalized=none`. The value `explicit` means that the request contained `personalized` at classification time. The value `absent` means that the middleware didn't record an automatic or explicit value.
4. If an expected classification is absent, find `[ApiClients] non-personalized response classification failed`. A classifier failure leaves the request unchanged.
5. If no failure entry exists, compare the request with the policy. Check the client key, provenance, base path, destination, HTTP method, schema path, and expansions. An `absent` value is often the expected fail-closed result.
6. Check `apiParams` for the serialized query. The query includes values such as `personalized`, `expand`, `siteId`, and `locale`. These values explain classification only if per-call middleware didn't change them. If the values don't agree with `personalizationMode`, inspect per-call middleware.
7. Inspect `sfdc_cache_status` for web-tier cache behavior. Inspect `cf-cache-status` for CDN cache behavior.

For example, this sanitized debug entry shows that automatic classification added `personalized=none`. Actual `apiParams` values can contain sensitive shopper data.

```text
[ApiClients] fetch GET /search/shopper-search/v1/.../product-search
status: 200
apiParams: { q: "example", expand: "images", personalized: "none" }
personalizationMode: "automatic-none"
```

Serialized query parameter values in debug logs can contain sensitive shopper data. Use debug logging briefly in a controlled environment. Cache-status headers report cache behavior. They don't prove that a response is safe for all shoppers.

## Available API Clients

The client provides access to all SCAPI endpoints.

| Client                    | Description                                                    |
| ------------------------- | -------------------------------------------------------------- |
| `shopperAvailability`     | Product availability and inventory                             |
| `shopperProducts`         | Products, categories, and catalog data                         |
| `shopperSearch`           | Product search and suggestions                                 |
| `shopperBasketsV1`        | Basket operations (legacy)                                     |
| `shopperBasketsV2`        | Basket operations (current)                                    |
| `shopperCustomers`        | Customer accounts and profiles                                 |
| `shopperOrders`           | Order history and details                                      |
| `shopperLogin`            | SLAS authentication                                            |
| `shopperPromotions`       | Promotions and campaigns                                       |
| `shopperContext`          | Shopper session context                                        |
| `shopperStores`           | Store locator                                                  |
| `shopperExperience`       | Page Designer content                                          |
| `shopperGiftCertificates` | Gift certificate operations                                    |
| `shopperSeo`              | SEO metadata                                                   |
| `shopperConsents`         | Customer consent management                                    |
| `auth`                    | Authentication helpers. See [Authentication](#authentication). |

## TypeScript Types

The client exports TypeScript types for all API schemas and operations.

```typescript
import type { ShopperProducts, ShopperSearch } from "@salesforce/storefront-next-runtime/scapi";

// Use schema types for data
type Product = ShopperProducts.schemas["Product"];
type Category = ShopperProducts.schemas["Category"];
type SearchResult = ShopperSearch.schemas["ProductSearchResult"];

// Use operation types for parameters
type SearchParams = ShopperSearch.operations["productSearch"]["parameters"]["query"];
```

## Error Handling

API errors throw an `ApiError` with detailed information:

```typescript
import { ApiError } from "@salesforce/storefront-next-runtime/scapi";

try {
  const { data } = await clients.shopperProducts.getProduct(options);
} catch (error) {
  if (error instanceof ApiError) {
    console.error("Status:", error.status); // 404
    console.error("Message:", error.body.detail); // "Product not found"
    console.error("URL:", error.url); // Request URL
    console.error("Method:", error.method); // "GET"
  }
}
```

### ApiError Properties

| Property     | Type          | Description           |
| ------------ | ------------- | --------------------- |
| `status`     | `number`      | HTTP status code      |
| `statusText` | `string`      | HTTP status message   |
| `body`       | `ErrorDetail` | Parsed error response |
| `rawBody`    | `string`      | Raw response text     |
| `headers`    | `Headers`     | Response headers      |
| `url`        | `string`      | Request URL           |
| `method`     | `string`      | HTTP method           |

## Response Format

All operations return both the parsed data and the raw response.

```typescript
const { data, response } = await clients.shopperProducts.getProduct(options);

// Typed data from the API
console.log(data.name);
console.log(data.price);

// Raw response for headers, status, etc.
console.log(response.headers.get("etag"));
console.log(response.status);
```

## Middleware

Add custom behavior to requests and responses:

```typescript
import type { Middleware } from "@salesforce/storefront-next-runtime/scapi";

const loggingMiddleware: Middleware = {
  onRequest({ request }) {
    console.log(`[API] ${request.method} ${request.url}`);
    return request;
  },
  onResponse({ response }) {
    console.log(`[API] ${response.status}`);
    return response;
  },
};

// Add to all clients
clients.use(loggingMiddleware);

// Or add to a specific client
clients.shopperProducts.use(loggingMiddleware);
```

## Custom APIs

[Custom APIs](/docs/commerce/b2c-commerce/guide/custom-apis.md) let you extend SCAPI with your own endpoints. Storefront Next supports two ways to call a custom API from your storefront, and both reuse the same authentication and middleware patterns as the built-in shopper clients.

### Generate a Typed Client (Recommended)

Generate a fully typed client from your custom API's OpenAPI schema. You get autocomplete, parameter validation, and operation-name calls like `loyaltyClient.getLoyaltyInfo()` instead of hand-rolled paths.

Storefront Next's template app ships a `sfnext scapi` CLI that takes an OpenAPI 3 schema and generates the type definitions and operation map for you, then registers the client alongside the built-in shopper clients. See the template's [SCAPI guide](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/main/docs/README-SCAPI.md) for the project-level workflow.

An external project follows the same pattern with three components:

1. **Types and operation map** generated from the OpenAPI schema. The types are produced with [openapi-typescript](https://openapi-ts.dev/); the operation map records each operation's HTTP method and path so the client can dispatch by `operationId`.
2. **A typed client** built with `createOpenApiFetchClient` and wrapped with the runtime's `createClient` helper to expose operation-name methods.
3. **Auth middleware** registered with `.use()` — the same registration pattern shown in [Adding the Access Token to Requests](#adding-the-access-token-to-requests) earlier in this guide.

Declare the operation map `as const` so its `m` / `b` / `s` values stay literal — the proxy uses those literals to type each operation method's parameters and return value.

```typescript
// ./generated/loyalty-info.operations.ts (generated alongside the types)
export const BASE_PATH = "/organizations/{organizationId}" as const;
export const operations = {
  getLoyaltyInfo: { m: "GET" as const, b: BASE_PATH, s: "/customers" },
} as const;
```

```typescript
// loyalty-client.ts
import { createOpenApiFetchClient, createClient } from "@salesforce/storefront-next-runtime/scapi";
import type { paths } from "./generated/loyalty-info";
import { operations } from "./generated/loyalty-info.operations";

const base = createOpenApiFetchClient<paths>({
  // Stop before /organizations — createClient appends the operation map's
  // base path, which already includes /organizations/{organizationId}.
  baseUrl: "https://shortcode.api.commercecloud.salesforce.com/custom/loyalty-info/v1",
});

const loyaltyClient = createClient(base, operations, {
  organizationId: "f_ecom_xxx",
  siteId: "RefArch",
});

// Register an auth middleware the same way the built-in clients do — see
// [Adding the Access Token to Requests](#adding-the-access-token-to-requests).
loyaltyClient.use(authMiddleware);

const { data } = await loyaltyClient.getLoyaltyInfo({
  params: { query: { c_customer_id: "customer1" } },
});
// GET https://shortcode.api.commercecloud.salesforce.com/custom/loyalty-info/v1
//     /organizations/f_ecom_xxx/customers?siteId=RefArch&c_customer_id=customer1
```

### Call with `fetch` Directly

For one-off calls, or projects without a codegen step, call the endpoint directly with `fetch`. Pull `accessToken`, `organizationId`, `siteId`, and any other dynamic values from the same auth/session context the typed clients use, and let the `URL` API encode user-supplied query values for you.

```typescript
const url = new URL(
  `https://shortcode.api.commercecloud.salesforce.com/custom/loyalty-info/v1/organizations/${organizationId}/customers`,
);
url.searchParams.set("c_customer_id", customerId);
url.searchParams.set("siteId", siteId);

const response = await fetch(url, {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const data = await response.json();
```

The native-fetch path skips the typed-client features above (TypeScript types, automatic `organizationId`/`siteId` injection, `ApiError` wrapping) — prefer the generated client for any custom API you call from more than one place.

## See Also

- [B2C Commerce API (SCAPI)](/docs/commerce/b2c-commerce/guide/scapi-get-started.md) - Official SCAPI documentation
- [Custom APIs](/docs/commerce/b2c-commerce/guide/custom-apis.md) - Define your own SCAPI endpoints
- [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) - The underlying fetch client library
- [openapi-typescript](https://openapi-ts.dev/) - Generate TypeScript types from an OpenAPI schema
