***

title: "PunchOutGatewayPlugin"
generated: true
---------------

<GenerationInfo sourceFile="packages/punchout-gateway-plugin/src/punchout-gateway-plugin.ts" sourceLine="31" packageName="@vendure-community/punchout-gateway-plugin" />

A plugin that integrates Vendure with [PunchCommerce](https://www.punchcommerce.com/) to enable
PunchOut/cXML procurement gateway functionality. This allows procurement systems to redirect users
to your Vendure storefront, where they can browse and add items to a cart, then transfer the cart
back to the procurement system.

## How It Works

1. **Buyer clicks PunchOut link** in their ERP → PunchCommerce redirects to your **storefront** with `sID` and `uID` query params
2. **Storefront authenticates the buyer** by calling Vendure's `authenticate` mutation with the `punchout` strategy
3. **Buyer shops normally** — all order mutations use `activeOrderInput` to scope the cart to the PunchOut session
4. **On checkout**, storefront calls `transferPunchOutCart(sID)` to send the cart back to PunchCommerce

## Installation

```shell
npm install @vendure-community/punchout-gateway-plugin
```

## Configuration

```ts
import { PunchOutGatewayPlugin } from '@vendure-community/punchout-gateway-plugin';

export const config: VendureConfig = {
    plugins: [
        PunchOutGatewayPlugin.init({
            // All options are optional — defaults work out of the box
        }),
    ],
};
```

### Options

| Option | Required | Default | Description |
| --- | --- | --- | --- |
| `apiUrl` | No | `https://www.punchcommerce.de` | Base URL of the PunchCommerce gateway. Override for staging or self-hosted instances. |
| `shippingCostMode` | No | `'nonZero'` | Controls shipping line item in the basket: `'all'` = always include, `'nonZero'` = only when > 0, `'none'` = never include. |
| `productFieldMapping` | No | — | Maps PunchCommerce product fields to static values or ProductVariant custom field names. See below. |

### Product Field Mapping

By default, all products are sent as pieces (`unit: 'PCE'`). If your catalog includes products with different units (weight, volume, etc.), you can map PunchCommerce fields to ProductVariant custom fields or static values.

Each field accepts either a static value or a `{ customField, default }` object that reads from the variant at transfer time:

```ts
PunchOutGatewayPlugin.init({
    productFieldMapping: {
        // Static value for all products
        packaging_unit: 'Bag',

        // Read from ProductVariant custom field, fall back to 'PCE' if empty
        unit: { customField: 'punchOutUnit', default: 'PCE' },
        unit_name: { customField: 'punchOutUnitName', default: 'Piece' },
        weight: { customField: 'productWeight', default: 0 },
    },
})
```

Available fields:

| Field | Default | Description |
| --- | --- | --- |
| `unit` | `'PCE'` | OCI unit code (e.g. `'PCE'`, `'KG'`, `'LTR'`) |
| `unit_name` | `'Piece'` | Human-readable unit name |
| `packaging_unit` | `'Piece'` | Packaging unit description |
| `purchase_unit` | `1` | Purchase unit quantity |
| `reference_unit` | `1` | Reference unit quantity |
| `weight` | `0` | Product weight |

## Customer Setup

Customers are linked to PunchCommerce via a custom field on the Customer entity.

1. **In PunchCommerce**: create a customer and set the "Customer identification" (this becomes the `uID`)
2. **In Vendure admin**: open the customer, set the **"PunchOut Customer ID (uID)"** custom field to the same value

## PunchCommerce Configuration

In the PunchCommerce dashboard, configure your customer:

* **Entry address**: your storefront's PunchOut landing page URL (e.g. `https://my-store.com/punchout`)
* **Customer identification**: a unique identifier matching the Vendure customer's custom field

PunchCommerce will redirect buyers to your Entry address with `?sID={UUID}&uID={identifier}` appended.

## Storefront Requirements

Since Vendure is headless, your storefront must handle the PunchOut flow. A full working example is available at [vendurehq/punchcommerce-storefront-demo](https://github.com/vendurehq/punchcommerce-storefront-demo).

Here's what needs to be implemented:

### 1. PunchOut Landing Page

Create a route (e.g. `/punchout`) that PunchCommerce redirects to. This page must:

1. Extract `sID` and `uID` from the query params
2. Store the `sID` for the duration of the session (e.g. in `sessionStorage`)
3. Call the `authenticate` mutation
4. Redirect to the shop homepage on success

```ts
// e.g. https://my-store.com/punchout?sID=abc-123&uID=test-customer
const params = new URLSearchParams(window.location.search);
const sID = params.get('sID');
const uID = params.get('uID');

// Store sID for the session — needed for all order operations
sessionStorage.setItem('punchoutSID', sID);

const { authenticate } = await graphqlClient.mutate({
    mutation: gql`
        mutation PunchOutLogin($sID: String!, $uID: String!) {
            authenticate(input: { punchout: { sID: $sID, uID: $uID } }) {
                ... on CurrentUser { id }
                ... on InvalidCredentialsError { message }
            }
        }
    `,
    variables: { sID, uID },
});
```

### 2. Session-Scoped Cart (activeOrderInput)

All order operations (queries and mutations) must include `activeOrderInput: { punchout: { sID } }` to scope the cart to the PunchOut session. This enables parallel sessions for the same customer.

```ts
const sID = sessionStorage.getItem('punchoutSID');

await graphqlClient.mutate({
    mutation: gql`
        mutation AddItem($variantId: ID!, $qty: Int!, $activeOrderInput: ActiveOrderInput) {
            addItemToOrder(
                productVariantId: $variantId
                quantity: $qty
                activeOrderInput: $activeOrderInput
            ) {
                ... on Order { id totalWithTax }
                ... on ErrorResult { message }
            }
        }
    `,
    variables: {
        variantId: '42',
        qty: 1,
        activeOrderInput: { punchout: { sID } },
    },
});
```

Pass `activeOrderInput` on **all** order operations: `activeOrder`, `addItemToOrder`, `adjustOrderLine`, `removeOrderLine`, `setOrderShippingAddress`, `setOrderShippingMethod`, `eligibleShippingMethods`, etc.

To display the cart, query `activeOrder` with the same input:

```ts
const sID = sessionStorage.getItem('punchoutSID');

const { activeOrder } = await graphqlClient.query({
    query: gql`
        query PunchOutCart($activeOrderInput: ActiveOrderInput) {
            activeOrder(activeOrderInput: $activeOrderInput) {
                id
                totalWithTax
                totalQuantity
                lines {
                    id
                    quantity
                    unitPriceWithTax
                    linePriceWithTax
                    productVariant { name sku }
                }
            }
        }
    `,
    variables: {
        activeOrderInput: { punchout: { sID } },
    },
});
```

### 3. Transfer Cart (replaces Checkout)

Replace the normal checkout flow with a "Transfer Cart" / "Back to Procurement" button that sends the cart to PunchCommerce:

```ts
const { transferPunchOutCart } = await graphqlClient.mutate({
    mutation: gql`
        mutation TransferCart($sID: String!) {
            transferPunchOutCart(sID: $sID) { success message }
        }
    `,
    variables: { sID: sessionStorage.getItem('punchoutSID') },
});

if (transferPunchOutCart.success) {
    // Cart transferred — show confirmation to the buyer
}
```

### 4. iFrame Support (if applicable)

If PunchCommerce is configured for iFrame PunchOut (embedding the shop inside the ERP), your storefront must:

* Set `SameSite=None; Secure` on all session cookies
* Remove the `X-Frame-Options` header during PunchOut sessions
* These are typically configured in your web server or storefront framework

## GraphQL API Reference

### Authentication (built-in mutation)

```graphql
mutation {
    authenticate(input: { punchout: { sID: "...", uID: "..." } }) {
        ... on CurrentUser { id }
        ... on InvalidCredentialsError { message }
    }
}
```

### Transfer Cart

```graphql
mutation {
    transferPunchOutCart(sID: "...") {
        success
        message
    }
}
```

Requires an authenticated PunchOut session.

## Cart Mapping

The plugin maps Vendure order lines to PunchCommerce basket positions:

* **Prices** use gross/net pattern: `price` = gross (with tax), `price_net` = net (without tax)
* **All monetary values** are converted from Vendure's integer cents to decimal (÷ 100)
* **Shipping** is included as a separate position with `type: 'shipping-costs'` (controlled by `shippingCostMode`)
* **Product descriptions**: `description` is plain text (HTML stripped), `description_long` preserves HTML
* **Basket** is sent as `multipart/form-data` to PunchCommerce's `/gateway/v3/return` endpoint

## Order Lifecycle

After a successful cart transfer, the order transitions to a custom **`Transferred`** state:

```
AddingItems → Transferred
```

* The order becomes **inactive** (`active = false`), so a new PunchOut session creates a fresh cart
* The order and all its line items are **preserved** in Vendure for record-keeping
* The order is visible in the Vendure admin under Orders with state `Transferred`
* Re-transferring the same session returns an error since no active order exists

The actual purchase order (PO) comes later through a separate channel — either manually or via cXML order transmission (future scope). The `Transferred` state represents "cart handed off to procurement system, awaiting PO."

## Parallel Sessions

The plugin uses a custom `ActiveOrderStrategy` to scope orders by PunchOut session ID (`sID`). At the API level:

* Each PunchOut session gets its own empty cart
* The same customer can have multiple concurrent PunchOut sessions
* Carts are isolated — items added in one session don't appear in another

### Storefront considerations

Browser cookies are scoped per-domain, not per-tab. If your storefront stores the `sID` in a cookie, only one PunchOut session can be active at a time — starting a new session overwrites the cookie and the previous session's cart becomes inaccessible from the UI.

To support truly parallel sessions, store the `sID` in `sessionStorage` (which is tab-scoped) and pass it explicitly to server actions. This way each browser tab/iframe maintains its own independent PunchOut session.

When a new PunchOut session starts and replaces the previous `sID`, make sure to revalidate any cached cart data so the UI reflects the new (empty) cart instead of showing stale items from the previous session.

```ts title="Signature"
class PunchOutGatewayPlugin {
    static options: PunchOutGatewayPluginOptions;
    init(options: PunchOutGatewayPluginOptions) => Type<PunchOutGatewayPlugin>;
}
```

<div className="members-wrapper">

### options

\<MemberInfo kind="property" type={`<a href='/current/community-plugins/reference/punch-out-gateway-plugin/punch-out-gateway-plugin-options#punchoutgatewaypluginoptions'>PunchOutGatewayPluginOptions</a>`}   />

### init

\<MemberInfo kind="method" type={`(options: <a href='/current/community-plugins/reference/punch-out-gateway-plugin/punch-out-gateway-plugin-options#punchoutgatewaypluginoptions'>PunchOutGatewayPluginOptions</a>) => Type<<a href='/current/community-plugins/reference/punch-out-gateway-plugin/#punchoutgatewayplugin'>PunchOutGatewayPlugin</a>>`}   />

</div>
