Documentation
Feedback
Guides
API Reference

Guides
Guides
Integration Guides

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 caseExtension pointHooksPurpose
Transfer cartpunchout.order-summary.ctauseRedirectRedirect 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
_26
import { useState } from 'react';
_26
import { useRedirect } from '@vtex/checkout';
_26
import styles from './transfer-cart.module.css';
_26
_26
export 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:

  • useCartPunchout hook: Access cart data and perform mutations that are reflected in other entities within the checkout data layer.
  • useCartItem hook: Access detailed information about an individual cart item.

Example: Add protection plan (per-item checkbox)


_72
// AddProtectionPlan.tsx
_72
import { useState } from 'react';
_72
import styles from './add-protection-plan.module.css';
_72
import { useCartPunchout } from '@vtex/checkout';
_72
_72
export 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

Contributors
2
Photo of the contributor
Photo of the contributor
Was this helpful?
Yes
No
Suggest Edits (GitHub)
See also
Punchout
Guides
Punchout login integration
Guides
Contributors
2
Photo of the contributor
Photo of the contributor
Was this helpful?
Suggest edits (GitHub)
On this page