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.
sID and uID query paramsauthenticate mutation with the punchout strategyactiveOrderInput to scope the cart to the PunchOut sessiontransferPunchOutCart(sID) to send the cart back to PunchCommerce| 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. |
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 |
Customers are linked to PunchCommerce via a custom field on the Customer entity.
uID)In the PunchCommerce dashboard, configure your customer:
https://my-store.com/punchout)PunchCommerce will redirect buyers to your Entry address with ?sID={UUID}&uID={identifier} appended.
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:
Create a route (e.g. /punchout) that PunchCommerce redirects to. This page must:
sID and uID from the query paramssID for the duration of the session (e.g. in sessionStorage)authenticate mutationAll 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:
Replace the normal checkout flow with a "Transfer Cart" / "Back to Procurement" button that sends the cart to PunchCommerce:
If PunchCommerce is configured for iFrame PunchOut (embedding the shop inside the ERP), your storefront must:
SameSite=None; Secure on all session cookiesX-Frame-Options header during PunchOut sessionsRequires an authenticated PunchOut session.
The plugin maps Vendure order lines to PunchCommerce basket positions:
price = gross (with tax), price_net = net (without tax)type: 'shipping-costs' (controlled by shippingCostMode)description is plain text (HTML stripped), description_long preserves HTMLmultipart/form-data to PunchCommerce's /gateway/v3/return endpointAfter a successful cart transfer, the order transitions to a custom Transferred state:
AddingItems → Transferred
active = false), so a new PunchOut session creates a fresh cartTransferredThe 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."
The plugin uses a custom ActiveOrderStrategy to scope orders by PunchOut session ID (sID). At the API level:
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.
PunchOutGatewayPluginOptions(options: PunchOutGatewayPluginOptions) => Type<PunchOutGatewayPlugin>