# Write Managed API Extensions

**Declare, configure, test, and publish Managed API Extension handlers.**

This page covers how to author [Managed API Extension](/commerce-integration-layer/managed-api-extensions.md) handlers: declaring them, the handler contract, configuration, conditional triggers, payload options, chaining, local testing, and publishing. Managed API Extensions are authored in the same bundle and sandbox as [schema extensions](/commerce-integration-layer/schema-extensions.md); see [Write schema extensions](/commerce-integration-layer/development/writing-schema-extensions.md) for the shared sandbox and configuration model. You can use the `cart-sku-blocker` template in the [Commerce Integration Layer extension examples](https://github.com/commercetools/commerce-integration-layer-extensions) repository to get started.

## Declare a handler

Each handler declares which resource and actions it intercepts and provides a `handler` function. The received payload is the commercetools SDK's own `ExtensionInput`, so you narrow on `input.resource.typeId` to get a fully typed resource:

```ts
import type { Cart, ExtensionInput } from '@commercetools/platform-sdk';

export const apiExtensions = [
  {
    key: 'cart-sku-blocker',            // becomes the commercetools extension key
    resourceTypeId: 'cart',             // cart | order | customer | payment | quote | business-unit | shopping-list | …
    actions: ['Create', 'Update'],
    handler: (input: ExtensionInput, ctx: { config: Record<string, string> }) => {
      const blockedSku = ctx.config.BLOCKED_SKU || 'BLOCKED-SKU';
      const cart: Cart | undefined =
        input.resource.typeId === 'cart' ? input.resource.obj : undefined;
      return (cart?.lineItems ?? []).some((li) => li.variant.sku === blockedSku)
        ? { errors: [{ code: 'InvalidInput', message: `SKU "${blockedSku}" cannot be added to the cart.` }] }
        : {};
    },
  },
];
```

Handler inputs include `input` (the resource being written, before it is persisted) and `ctx` (containing [config](/commerce-integration-layer/development/writing-schema-extensions.md#configuration)). Handlers lack access to the session, shopper identity, or presentment context, as writes may originate from clients without a shopper context (for example, Merchant Center or imports).

### The handler contract

A handler returns a plain object describing its decision:

| Return value | Effect |
| --- | --- |
| `{}` | Approves the write unchanged. |
| `{ actions: [...] }` | Modifies the write by applying update actions before it is saved. |
| `{ errors: [...] }` | Blocks the write and returns a validation error to the caller. |

> Unhandled exceptions cause the write to fail. Return explicit validation errors rather than throwing exceptions.

### Configuration fields

| Field | Description |
| --- | --- |
| `key` | Identifies the handler; becomes the commercetools extension key. |
| `resourceTypeId` | The resource to intercept: `cart`, `order`, `customer`, `payment`, `quote`, `business-unit`, `shopping-list`, and others. |
| `actions` | The write actions to intercept, such as `Create` and `Update`. |
| `condition` | Optional [query predicate](/api/predicates/query.md#use-predicates-in-conditional-api-extensions). commercetools evaluates it on each matching write and calls the handler only when it is true. Omit to run on every create or update for the resource. |
| `timeoutInMs` | The maximum time, in milliseconds, that commercetools waits for this handler. Omit to use the default of **2** seconds. |
| `includeOldResource` | Optional. Set to `true` to also receive the state of the resource before the update. See [Previous state of the resource](/commerce-integration-layer/development/writing-managed-api-extensions.md#previous-state-of-the-resource). |
| `expansionPaths` | Optional list of [expansion paths](/api/general-concepts.md#expansion-paths) that commercetools resolves before it delivers the payload. See [Expand references in the payload](/commerce-integration-layer/development/writing-managed-api-extensions.md#expand-references-in-the-payload). |
| `dependsOn` | Optional list of handler keys that must complete before this handler runs. See [Chain handlers](/commerce-integration-layer/development/writing-managed-api-extensions.md#chain-handlers). |
| `handler` | The function called with `(input, ctx)`. |

### Conditional triggers

Without a `condition`, commercetools calls the handler on every create or update operation for the configured `resourceTypeId`. Set `condition` when the handler should run only for specific writes (for example, to skip cart updates that do not alter the resource state, such as setting a line item quantity to its existing value). The platform evaluates the predicate and skips the callback when the expression evaluates to `false`, preventing unrelated write operations from making unnecessary platform requests.

```ts
{
  key: 'cart-quantity-cap',
  resourceTypeId: 'cart',
  actions: ['Create', 'Update'],
  condition: 'lineItems is not empty',
  handler: (input, ctx) => { /* … */ },
}
```

The `cart-quantity-cap` template in the [extension examples](https://github.com/commercetools/commerce-integration-layer-extensions) repository uses `condition` together with a `{ actions }` response to cap line quantities. For predicate syntax and evaluation rules, see [Conditional triggers](/api/projects/api-extensions.md#conditional-triggers) in the HTTP API reference.

> If a predicate references optional fields, use the `is defined` operator so a missing field does not cause evaluation to fail. See the commercetools HTTP reference for details.

### Previous state of the resource

By default, the payload carries only the resource as it is after the update, which is not enough for a handler that must know what changed, such as whether a Line Item quantity went up or down. Set `includeOldResource` to `true` and commercetools adds an `oldResource` field holding the state before the update:

```ts
{
  key: 'cart-quantity-delta',
  resourceTypeId: 'cart',
  actions: ['Update'],
  includeOldResource: true,
  handler: (input, ctx) => {
    const before = input.oldResource?.obj;
    const after = input.resource.obj;
    /* compare the two */
  },
}
```

commercetools sends `oldResource` for `Update` actions only, so it is never present on a `Create`, and it is absent entirely when the handler does not ask for it. The Commerce Integration Layer registers the flag as the Extension's `additionalContext`; for the underlying behavior, see [Previous state of the updated resource](/api/projects/api-extensions.md#previous-state-of-the-updated-resource) in the HTTP API reference.

### Expand references in the payload

Payloads include references (such as resource IDs) to related resources rather than full resource objects by default. If a handler requires details from a referenced resource, it must send a separate API request to the platform, consuming time against the callback deadline. To avoid additional API calls, define `expansionPaths` so commercetools resolves and embeds the referenced resources directly into the payload before delivery.

```ts
{
  key: 'cart-customer-group-pricing',
  resourceTypeId: 'cart',
  actions: ['Create', 'Update'],
  expansionPaths: ['customerGroup'],
  handler: (input, ctx) => { /* input.resource.obj.customerGroup.obj is populated */ },
}
```

You can declare up to **3** paths per handler, and they apply to `oldResource` as well when `includeOldResource` is set. For the path syntax and the payload-size trade-off, see [Reference Expansion in API Extensions](/api/api-extensibility-overview.md#reference-expansion-in-api-extensions).

### Chain handlers

By default, handlers that match the same write run concurrently, and each sees the resource before those handlers run. If a handler must build on another handler's update actions, declare that order with `dependsOn`:

```ts
export const apiExtensions = [
  {
    key: 'cart-validate',
    resourceTypeId: 'cart',
    actions: ['Update'],
    handler: (input, ctx) => { /* … */ },
  },
  {
    key: 'cart-enrich',
    resourceTypeId: 'cart',
    actions: ['Update'],
    dependsOn: ['cart-validate'],
    handler: (input, ctx) => { /* sees cart-validate's actions already applied */ },
  },
];
```

`dependsOn` names `key` values, not the generated commercetools Extension keys; the Commerce Integration Layer resolves keys declared in the bundle and passes undeclared keys through as-is for Project-owned API Extensions. Each referenced Extension must exist and be applicable to every trigger and action of this handler, or registration fails. A handler cannot depend on itself.

Platform dependency rules apply: commercetools enforces a maximum of **5** direct dependencies per Extension and a maximum chain depth of **3**, and it rejects circular dependencies. Chained handlers run sequentially, so their timeouts add up against the request budget. See [Extension Chaining](/api/projects/api-extensions.md#extension-chaining) for the execution model and the [performance considerations](/api/projects/api-extensions.md#performance-considerations).

## Configuration and the sandbox

Handlers run in the [same sandbox as resolvers](/commerce-integration-layer/development/writing-schema-extensions.md#runtime-constraints). No ambient authority exists, and outbound calls only use the allowlist-gated global `fetch`. Read per-project settings from `ctx.config`, exactly like a resolver, and secrets are decrypted host-side. For more information, see [Configuration](/commerce-integration-layer/development/writing-schema-extensions.md#configuration).

A single bundle can export both `apiExtensions` and `typeDefs`/`resolvers`. The `cart-sku-blocker` example does exactly this: it blocks certain SKUs from Carts and exposes a `Query.blockedSkus` field returning those same SKUs, both reading one shared `ctx.config` value.

### Handler deadline

commercetools sets the deadline for the callback. If a handler runs past it, commercetools fails the write itself, so keep handlers fast.

If a handler declares no `timeoutInMs`, the deadline is your Project's own timeout limit, which is **2** seconds by default. See [Time limits](/api/projects/api-extensions.md#time-limits) for API Extensions.

A handler that needs longer, for example because it calls a vendor API, can declare its own timeout:

```ts title="Configure a handler timeout"
{
  key: 'cart-vendor-pricing',
  resourceTypeId: 'cart',
  actions: ['Create', 'Update'],
  timeoutInMs: 5000,
  handler: (input, ctx) => { /* … */ },
}
```

Each declaration registers its own commercetools Extension, so handlers can carry different timeouts. Values above your Project's [maximum](/api/projects/api-extensions.md#time-limits) are rejected.

## Develop locally

Try testing Managed API Extension handlers offline, without credentials or a deployment.

First, scaffold a realistic commercetools callback payload with `create-api-extension-input`. Then send it to your bundle's handlers with `invoke-api-extension`:

```bash
commercetools integration-layer extension create-api-extension-input --resource-type cart --action Create --out ./payloads/cart-create.json
commercetools integration-layer extension invoke-api-extension --input ./payloads/cart-create.json
commercetools integration-layer extension invoke-api-extension --input ./payloads/cart-update.json --config BLOCKED_SKU=NO-SELL
commercetools integration-layer extension invoke-api-extension --input ./payloads/order-create.json --key order-tagger
```

`invoke-api-extension` prints each handler's decision, which can be `APPROVE`, `MODIFY` with update actions, or blocking errors. A handler runs only when its `resourceTypeId` and `actions` match the payload. Non-matching handlers are reported as skipped. The local invoke does not evaluate `condition`, instead, commercetools applies that predicate in production. Use `--key` (repeatable) to invoke only specific handlers. In a monorepo, add `--all` to invoke the merged bundle under `extensions/`.

`ctx.config` comes from `EXTENSION_CONFIG_*` in a project `.env` file or the shell and `--config KEY=VALUE` overrides a given key. For the full flag reference for local development, see [Develop locally](/commerce-integration-layer/development/writing-schema-extensions.md#develop-locally) in Write schema extensions.

Then `validate` and `push` as usual. A Managed-API-Extensions-only bundle skips the GraphQL composition check, since there's no schema to compose. For the full publish flow, see [Validate and publish](/commerce-integration-layer/development/writing-schema-extensions.md#validate-and-publish).

## Related pages

- [Area overview page with navigation](/commerce-integration-layer.md)
- [Previous page: Write Schema Extensions](/commerce-integration-layer/development/writing-schema-extensions.md)
- [Next page: Configure and deploy](/commerce-integration-layer/development/configure-and-deploy.md)
- [Search documentation and API specs](/search.md)