PunchOutGatewayPlugin
A plugin that integrates Vendure with PunchCommerce 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
- Buyer clicks PunchOut link in their ERP → PunchCommerce redirects to your storefront with
sIDanduIDquery params - Storefront authenticates the buyer by calling Vendure's
authenticatemutation with thepunchoutstrategy - Buyer shops normally — all order mutations use
activeOrderInputto scope the cart to the PunchOut session - On checkout, storefront calls
transferPunchOutCart(sID)to send the cart back to PunchCommerce
Installation
Configuration
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:
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.
- In PunchCommerce: create a customer and set the "Customer identification" (this becomes the
uID) - 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.
Here's what needs to be implemented:
1. PunchOut Landing Page
Create a route (e.g. /punchout) that PunchCommerce redirects to. This page must:
- Extract
sIDanduIDfrom the query params - Store the
sIDfor the duration of the session (e.g. insessionStorage) - Call the
authenticatemutation - Redirect to the shop homepage on success
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.
Pass activeOrderInput on all order operations: activeOrder, addItemToOrder, adjustOrderLine, removeOrderLine, setOrderShippingAddress, setOrderShippingMethod, eligibleShippingMethods, etc.
To display the cart, query activeOrder with the same input:
3. Transfer Cart (replaces Checkout)
Replace the normal checkout flow with a "Transfer Cart" / "Back to Procurement" button that sends the cart to PunchCommerce:
4. iFrame Support (if applicable)
If PunchCommerce is configured for iFrame PunchOut (embedding the shop inside the ERP), your storefront must:
- Set
SameSite=None; Secureon all session cookies - Remove the
X-Frame-Optionsheader during PunchOut sessions - These are typically configured in your web server or storefront framework
GraphQL API Reference
Authentication (built-in mutation)
Transfer Cart
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 byshippingCostMode) - Product descriptions:
descriptionis plain text (HTML stripped),description_longpreserves HTML - Basket is sent as
multipart/form-datato PunchCommerce's/gateway/v3/returnendpoint
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.
options
PunchOutGatewayPluginOptionsinit
(options: PunchOutGatewayPluginOptions) => Type<PunchOutGatewayPlugin>