Punchout cart integration
Learn how to enable and customize the Punchout cart screen, allowing users to transfer their cart back to the eprocurement system and add per-item options or attachments.
This feature is only available for stores using B2B Buyer Portal, which is currently available to selected accounts.
In a Punchout scenario, buyers must transfer their cart back to their organization’s eprocurement system for approval and compliance validation before completing the purchase.
This guide shows you how to customize the Punchout cart screen, where buyers transfer their cart to eprocurement systems.
This integration applies only to stores using FastCheckout.
How it works
The Punchout cart integration extends the Punchout workflow after the login step. After the user logs in via Punchout and proceeds to checkout, FastCheckout detects the Punchout session, and the user is redirected to the dedicated Punchout cart screen.
On this screen, the user reviews the cart and clicks Transfer cart, which sends the cart data to the external eprocurement system and redirects the user back to it for approval.
Setup
Prerequisites
To implement the Punchout cart flow, you must contact our Support team to enable Checkout extension points in FastCheckout.
Implementing extension points
After enabling the extension points, create a storefront monorepo to customize it. Once you have set up your storefront monorepo, you’re ready to set up Checkout extension points.
To implement the Punchout cart flow, you must use the punchout.order-summary.cta extension point to send the cart data to the eprocurement system and redirect to its URL (e.g., via a “Transfer cart” or “Checkout” button).
The following extension point lets you implement this behavior:
| Use case | Extension point | Hooks | Purpose |
|---|---|---|---|
| Transfer cart | punchout.order-summary.cta | useRedirect | Redirect the user to a URL (e.g., the eprocurement system) with the cart. |
punchout.order-summary.cta extension point
The punchout.order-summary.cta extension point is rendered in the Punchout screen sidebar (order summary). Use it to add a primary action button that sends the user back to the eprocurement system with the cart.
The useRedirect hook provides a redirect method that navigates the user to the eprocurement system using the target URL (for example, the eprocurement system’s cart or checkout URL) during the “transfer cart” step.
Example: Transfer cart button
_26// TransferCart.tsx_26import { useState } from 'react';_26import { useRedirect } from '@vtex/checkout';_26import styles from './transfer-cart.module.css';_26_26export const TransferCart = () => {_26 const [isLoading, setIsLoading] = useState(false);_26 const { redirect } = useRedirect();_26_26 const handleSubmit = () => {_26 setIsLoading(true);_26 redirect('https://your-eprocurement-system.com/cart');_26 };_26_26 return (_26 <button_26 data-loading={isLoading}_26 disabled={isLoading}_26 className={styles.button}_26 onClick={handleSubmit}_26 >_26 Transfer cart_26 <span className={styles.spinner}></span>_26 </button>_26 );_26};
_77/* transfer-cart.module.css */_77.button {_77 background-color: var(--fc-colors-brand-primary);_77 border-color: var(--fc-colors-brand-primary);_77 color: #ffffff;_77 display: inline-flex;_77 justify-content: center;_77 align-items: center;_77 transition: background-color 100ms;_77 padding: 16px 24px;_77 border-radius: 999px;_77 min-height: 5rem;_77 font-family: var(--fc-fonts-sans);_77 font-size: var(--fc-button-font-size);_77 font-weight: var(--fc-button-font-weight);_77 line-height: var(--fc-button-line-height);_77 cursor: pointer;_77 position: relative;_77 border: 1px solid;_77}_77_77.button:disabled {_77 cursor: default;_77 background-color: var(--fc-colors-light-gray-300);_77 color: #ffffff;_77 border-color: var(--fc-colors-light-gray-300);_77}_77_77.button[data-loading='true'] {_77 background-color: var(--fc-colors-light-gray-300);_77 color: #ffffff;_77 border-color: var(--fc-colors-light-gray-300);_77}_77_77.button:focus:not(:disabled):not([data-loading]) {_77 outline: 1px solid var(--fc-colors-brand-primary-40);_77 outline-offset: 4px;_77}_77_77.button:hover:not(:disabled):not([data-loading]) {_77 background-color: var(--fc-colors-brand-primary-80);_77}_77_77.button:active:not(:disabled):not([data-loading]) {_77 background-color: var(--fc-colors-brand-primary-darker);_77}_77_77@keyframes loading {_77 to {_77 transform: rotate(360deg);_77 }_77}_77_77.spinner {_77 user-select: none;_77 border: 2px solid transparent;_77 border-radius: 50%;_77 width: 1.8rem;_77 height: 1.8rem;_77 animation: loading 1s linear infinite;_77 display: none;_77 position: absolute;_77 right: 12px;_77}_77_77.button .spinner {_77 border-left-color: #ffffff;_77 border-bottom-color: #ffffff;_77 border-top-color: #ffffff;_77}_77_77.button[data-loading='true'] .spinner {_77 display: block;_77 border-left-color: var(--fc-colors-brand-primary);_77 border-bottom-color: var(--fc-colors-brand-primary);_77 border-top-color: var(--fc-colors-brand-primary);_77}
Per-item extension points (optional)
The punchout.cart-item.after extension point is optional and renders below each item on the Punchout cart screen. Use it to add additional options or attachments to a line item (e.g., a protection plan or warranty).
The following hooks provide the necessary actions for the extension points:
useCartPunchouthook: Access cart data and perform mutations that are reflected in other entities within the checkout data layer.useCartItemhook: Access detailed information about an individual cart item.
Example: Add protection plan (per-item checkbox)
_72// AddProtectionPlan.tsx_72import { useState } from 'react';_72import styles from './add-protection-plan.module.css';_72import { useCartPunchout } from '@vtex/checkout';_72_72export const AddProtectionPlan = () => {_72 const { sync } = useCartPunchout();_72 const [isChecked, setIsChecked] = useState(false);_72 const [focus, setFocus] = useState(false);_72 const [disabled, setDisabled] = useState(false);_72_72 const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {_72 setIsChecked(event.target.checked);_72 setDisabled(true);_72_72 // Call the Checkout API to add or remove an attachment for this item; on success, sync the cart_72 // Example: update item with attachment (implementation depends on your API)_72 try {_72 // await yourCheckoutApiUpdateItem(...);_72 await sync();_72 } finally {_72 setDisabled(false);_72 }_72 };_72_72 return (_72 <div className={styles.container}>_72 <div>_72 <label className={styles.label} data-disabled={disabled}>_72 <input_72 className={styles.hide}_72 type="checkbox"_72 checked={isChecked}_72 onChange={handleChange}_72 onFocus={() => setFocus(true)}_72 onBlur={() => setFocus(false)}_72 disabled={disabled}_72 />_72 <div_72 className={styles.checkbox}_72 data-checked={isChecked}_72 data-focus-visible={focus}_72 data-disabled={disabled}_72 >_72 <svg_72 className={styles.icon}_72 width="16"_72 height="16"_72 viewBox="0 0 24 24"_72 fill="none"_72 xmlns="http://www.w3.org/2000/svg"_72 >_72 <path_72 d="M8.80001 15.9L4.60001 11.7L3.20001 13.1L8.80001 18.7L20.8 6.69999L19.4 5.29999L8.80001 15.9Z"_72 fill="currentColor"_72 />_72 </svg>_72 </div>_72 Add Protection Plan_72 <span className={styles.price}>($212.50)</span>_72 </label>_72 </div>_72 <button_72 type="button"_72 className={styles.button}_72 onClick={() => console.log('Info button clicked')}_72 >_72 Info_72 </button>_72 </div>_72 );_72};
If the Info button triggers asynchronous work (for example, opening a modal or fetching details), you can add a loading state and a spinner using the same pattern as the Transfer cart example (data-loading, .spinner class, and matching CSS). The snippet below defines a simple .hide utility and adjusts the styles so the UI clearly reflects loading and disabled states.
_93/* add-protection-plan.module.css */_93.container {_93 display: flex;_93 justify-content: space-between;_93 align-items: center;_93 width: 100%;_93 background: var(--fc-colors-light-gray-100);_93 padding: 12px;_93 border-radius: 4px;_93}_93_93.button {_93 background-color: transparent;_93 border-color: transparent;_93 color: var(--fc-colors-brand-primary);_93 text-decoration: none;_93 display: inline-flex;_93 justify-content: center;_93 align-items: center;_93 transition: background-color 100ms;_93 border-radius: 999px;_93 font-family: var(--fc-fonts-sans);_93 font-size: var(--fc-button-font-size);_93 font-weight: 600;_93 line-height: var(--fc-button-line-height);_93 cursor: pointer;_93 border: none;_93 min-height: auto;_93}_93_93.button:disabled {_93 cursor: default;_93 color: var(--fc-colors-light-gray-300);_93}_93_93.label {_93 font-family: var(--fc-fonts-sans);_93 font-size: 1.4rem;_93 font-weight: 400;_93 line-height: 1.4rem;_93 color: #000000;_93 display: flex;_93 align-items: center;_93 gap: 4px;_93 cursor: pointer;_93}_93_93.label[data-disabled='true'],_93.label[data-disabled='true'] .price {_93 color: var(--fc-colors-light-gray-400);_93 cursor: progress;_93}_93_93.checkbox {_93 flex-shrink: 0;_93 width: 20px;_93 height: 20px;_93 border: 1px solid var(--fc-colors-light-gray-400);_93 border-radius: 4px;_93 padding: 2px;_93 margin-right: 8px;_93}_93_93.checkbox[data-focus-visible='true'] {_93 outline-offset: 4px;_93 outline: 1px solid var(--fc-colors-brand-primary);_93}_93_93.checkbox[data-checked='true'] {_93 background: var(--fc-colors-brand-primary);_93 border: none;_93}_93_93.checkbox[data-disabled='true'] {_93 background: var(--fc-colors-light-gray-300);_93}_93_93.checkbox[data-disabled='true'][data-checked='false'] {_93 background: var(--fc-colors-light-gray-100);_93 border: 1px solid var(--fc-colors-light-gray-400);_93}_93_93.checkbox[data-disabled='true'][data-checked='false'] .icon {_93 display: none;_93}_93_93.icon {_93 color: #ffffff;_93}_93_93.hide {_93 display: none;_93}
You've now configured the Punchout cart integration to transfer cart data back to eprocurement systems and buyers can seamlessly send their carts to platforms for approval workflows. 6830896ca1b0e0b667f3c5b2f5c7e3e59606a5